How to use Guice AssistedInject with multiple implementations of an interface?

2.4k views Asked by At

I'm having trouble finding how to have a "dynamic AssistedInject". What i mean by that is i would like to provide to the factory the implementing class name it needs to use at runtime based on a parameter. Here's what i currently have:

interface Letter {}

abstract class ALetter implements Letter {
    public ALetter(T1 t1, T2 t2) {...} 
}

class A implements Letter {
    public static final NAME = "A";

    @Inject
    public A(@Assisted T1 t1, T2 t2) { super(t1, t2); }
}

class B implements Letter {
    public static final NAME = "B";

    @Inject
    public B(@Assisted T1 t1, T2 t2) { super(t1, t2); }
}

I would like to be able to load either A or B based on their name considering I would have T1 but I would need T2 injected. It may look something like:

class MyClass {
    @Inject
    public MyClass(RequestData data, MyConfig config, ILetterFactory letterFactory) {
        Letter letter = letterFactory.get(data.getLetterName(), config.getT2());
    }
}

And i would have configured ILetterFactory using something like:

install(new FactoryModuleBuilder().build(ILetterFactory.class));

But I know this currently would not work as the lettername isn't a real parameter of my constructor and Guice just doesn't work like that. It should give you an idea of what I want though.

The only solution I've currently found does not use Guice: I have my own factory that resolves the class constructor based on the name (through a Map<String, Constructor>), and calls newInstance providing it with the appropriate params (no injection at all besides in the factory itself).

Is there a way to use Guice to avoid creating that factory myself ? Thanks

1

There are 1 answers

4
Jeff Bowman On BEST ANSWER

FactoryModuleBuilder isn't as powerful as you think it is—it has no provision to switch between implementations, and is only used for mixing injected dependencies with other constructor arguments. Luckily, Guice makes your factory easy to write. What it sounds like you're looking for is an implementation of this interface:

public interface ILetterFactory {
  /** This assumes that T2 is a Guice-satisfied dependency,
    * and not meant to be @Assisted, so leave it out of the interface. */
  Letter get(String which);
}

Choice one is to let Guice provide your instances with a Provider, which is most useful if your Letters have widely differing deps, or if your deps list is long or changes often. If A and B need assisted dependencies, replace Provider<A> with A.Factory that you've bound through FactoryModuleBuilder.

public class LetterFactory implements ILetterFactory {
  @Inject Provider<A> aProvider;
  @Inject Provider<B> bProvider;

  @Override public Letter get(String which) {
    if (A.NAME.equals(which)) {
      return aProvider.get();
    } else if (B.NAME.equals(which)) {
      return bProvider.get();
    } else {
      throw new IllegalArgumentException("Letter does not exist");
    }
  }
}

With choice two, you take the responsibility of creating a new instance from Guice. It requires more maintenance if your deps list changes, and doesn't play as nicely with AOP and other similar features, but may be a little less work if your Letters need assisted parameters and a small set of similar deps:

public class LetterFactory implements ILetterFactory {
  @Inject Provider<T2> t2Provider;

  @Override public Letter get(String which) {
    if (A.NAME.equals(which)) {
      return new A(t2Provider.get());
    } else if (B.NAME.equals(which)) {
      return new B(t2Provider.get());
    } else {
      throw new IllegalArgumentException("Letter does not exist");
    }
  }
}