Replacing abstract factory with Guice?
Asked Answered
M

3

4

I am new to Guice and I was wondering how far I can take it. I have an interface UserInfo with multiple implementing classes GoogleUserInfo, FacebookUserInfo, TwitterUserInfo etc. These classes are created using a factory

public class UserInfoFactory {
  public UserInfo createFromJsonString(String jsonspec) {
    .
    .
    .

  }
}

The creation is controlled by the JSON string jsonspec which controls which of the implementing classes of UserInfo is returned. Specifically, there is a JSON string element domain which controls the creation. The creation really is a function of the deserialization of jsonspec using GSON.
I was wondering if there is a good way to replace this creation with a Guice dependency injection?

Marinmarina answered 14/4, 2013 at 18:6 Comment(0)
M
7

You can integrate Guice into the factory, but your code is probably better off exactly as it is in this case.

This is actually one of those Factories that can't be replaced easily, because it has to contain the logic to parse out the jsonSpec and change which concrete type it returns based on that. Let's say the slightly-less-simplified version of the factory looks like this:

public class UserInfoFactory {
  public UserInfo createFromJsonString(String jsonspec) {
    if(getUserType(jsonSpec) == TWITTER) {
      return new TwitterUserInfo(jsonSpec);
    } else { /* ... */ }
  }

  private UserInfoType getUserType(String jsonSpec) { /* ... */ }
}

That logic has to live somewhere, and your own UserInfoFactory seems like a perfect home. However, because you use new, you would not be able to inject any of TwitterUserInfo's dependencies, or its dependencies' dependencies--and that is the type of problem that Guice solves nicely.

You can inject TwitterUserInfo as a Provider, which will give you access to as many of fully-injected TwitterUserInfo objects as you'd like:

public class UserInfoFactory {
  @Inject Provider<TwitterUserInfo> twitterUserInfoProvider;

  public UserInfo createFromJsonString(String jsonspec) {
    if(getUserType(jsonSpec) == TWITTER) {
      TwitterUserInfo tui = twitterUserInfoProvider.get();
      tui.initFromJson(jsonSpec);
      return tui;
    } else { /* ... */ }
  }
}

...and, of course, that also allows you to inject a @Twitter Provider<UserInfo> if you only need the interface and want to vary the concrete class sometime in the future. If you want TwitterUserInfo to accept constructor parameters, Assisted injection will help you create a TwitterUserInfoFactory though, which will help it toward immutability:

public class UserInfoFactory {
  @Inject TwitterUserInfo.Factory twitterUserInfoFactory;

  public UserInfo createFromJsonString(String jsonspec) {
    if(getUserType(jsonSpec) == TWITTER) {
      return twitterUserInfoFactory.create(jsonSpec);
    } else { /* ... */ }
  }
}

// binder.install(new FactoryModuleBuilder().build(TwitterUserInfoFactory.class));
public class TwitterUserInfo implements UserInfo {
  public interface Factory {
    TwitterUserInfo create(String jsonSpec);
  }

  public TwitterUserInfo(@Assisted String jsonSpec, OtherDependency dep) { /* ... */ }
}

A final note: TwitterUserInfo likely doesn't have any dependencies--it sounds like a data object to me--so leaving your class exactly how you found it (with new) is probably the best approach. While it's nice to have decoupled interfaces and classes to ease testing, there is a cost in maintenance and understanding. Guice is a very powerful hammer, but not everything is actually a nail to be hammered.

Martineau answered 15/4, 2013 at 23:32 Comment(0)
S
1

You'll probably want to use assisted injection.

You'll need to annotate your constructors in GoogleUserInfo, FacebookUserInfo, and TwitterUserInfo with (@Assisted String jsonspec) (and of course @Inject)

Then you'll need to configure your factory class

binder.install(new FactoryModuleBuilder().build(UserInfoFactory.class));

And then appropriately bind whichever Info provider you want to use.

I think. I'm pretty new to guice myself.

Skyscraper answered 14/4, 2013 at 18:12 Comment(3)
Assisted injection is a good thing to research here but is not smart enough to return different concrete types based on the JSON class result. As specified here, Guice will fail to provide an implementation for UserInfoFactory because it can't instantiate the interface UserInfo, and without calling FactoryModuleBuilder.implement, the interface is all the FactoryModuleBuilder could return.Martineau
@JeffBowman - so if you have in Module.configure binder.install(new FactoryModuleBuilder().build(UserInfoFactory.class)); adding a function to the module public Class<? extends UserInfo> bindUserInfo(){ return GoogleUserInfo.class; } would not work? (Forgive me, I'm trying to get a grasp on Guice coming from a different perspective than normal. I've got an existing application I'm working with that uses Guice and what I do know comes from trying to analyze the exiting codebase.)Skyscraper
FactoryModuleBuilder works by inspecting the interface (not concrete class!) passed into build and providing an injectable implementation for it. When you call that implementation's method, FactoryModuleBuilder takes the return type of the method and instantiates it, filling in @Assisted parameters with the method arguments and everything else from the Injector. If the return type is an interface, like UserInfo, you also need to call implement to tell it which concrete type to create. This is why it won't work here--the concrete type is fixed at configuration time.Martineau
K
1

Here's my variation of the abstract factory pattern with Guice - using multibindings to bind the concrete factories.

Here I'm using a set of factories and a loop to select the suitable factory class, although you could also use a Map and get the concrete factory by some key if possible.

public class AbstractFactory {
  @Inject Set<UserInfoFactory> factories;

  public UserInfoFactory createFor(String jsonSpec) {
    for (UserInfoFactory factory : factories) {
      if (factory.handles(jsonSpec))
        return factory;
    }
    throw new IllegalArgumentException("No factory for a given spec!");
  }
}

This technique doesn't require you to open and modify the AbstractFactory class to add a new concrete factory - just write the new factory and add the appropriate binding to your Guice Module.

Each concrete factory is responsible for handling the specific data format and for parsing the json string properly:

public class FacebookUserInfoFactory implements UserInfoFactory {
  public boolean handles(String jsonSpec) {
    return jsonSpec.contains("facebook");
  }

  public UserInfo createFromJsonString(String jsonspec) {
    return new FacebookUserInfo(jsonspec);
  }
}

public class TwitterUserInfoFactory implements UserInfoFactory {
  public boolean handles(String jsonSpec) {
    return jsonSpec.contains("twitter");
  }

  public UserInfo createFromJsonString(String jsonspec) {
    return new TwitterUserInfo(jsonspec);
  }
}

Now, bind your concrete factories with Multibinder. Example usage:

AbstractFactory abstractFactory = Guice
        .createInjector(new AbstractModule() {
          protected void configure() {
            Multibinder<UserInfoFactory> factoryBindings = Multibinder
                    .newSetBinder(binder(), UserInfoFactory.class);
            factoryBindings.addBinding().to(FacebookUserInfoFactory.class);
            factoryBindings.addBinding().to(TwitterUserInfoFactory.class);
          }
        }).getInstance(AbstractFactory.class);

UserInfoFactory factory1 = abstractFactory.createFor("twitter");
UserInfoFactory factory2 = abstractFactory.createFor("facebook");
UserInfo user1 = factory1.createFromJsonString("twitter user json string");
UserInfo user2 = factory2.createFromJsonString("facebook user json string");
System.out.println(user1.toString());
System.out.println(user2.toString());
}

The output would be:

TwitterUserInfo{twitter user json string}
FacebookUserInfo{facebook user json string}

UserInfo is just an interface or an abstract class (omitted here for clarity).

Karissakarita answered 7/5, 2019 at 13:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.