Search code examples
javadependency-injectioninversion-of-controlguice

Can Guice automatically create instances of different classes based on a parameter?


A standard object factory may look like this:

interface I { ... }
class A implements I { ... }
class B implements I { ... }

class IFactory {
    I getI(int i) {
        switch (i) {
        case 1: return new A();
        default: return new B();
        }
    }
}

Is it possible to set up bindings so that switch is done for me, i.e. all I do is call getInstance or inject? I was looking at assisted injection but that seems to be different topic: https://code.google.com/p/google-guice/wiki/AssistedInject


Solution

  • It sounds like you're looking for a MapBinder, which is part of the Multibindings feature. Note that you'll still need to put in some kind of IFactory or other factory interface, because getInstance doesn't take a parameter the way your getI does, and you'll still need to establish a mapping from integer to class implementation somewhere.

    MapBinder-style

    class IModule extends AbstractModule {
      @Override public void configure() {
        MapBinder<Integer, I> myBinder =
            MapBinder.newMapBinder(binder(), Integer.class, I.class);
        myBinder.addBinding(1).to(A.class);
        // Add more here.
      }
    }
    
    // You can even split the MapBinding across Modules, if you'd like.
    class SomeOtherModule extends AbstractModule {
      @Override public void configure() {
        // MapBinder.newMapBinder does not complain about duplicate bindings
        // as long as the keys are different.
        MapBinder<Integer, I> myBinder =
            MapBinder.newMapBinder(binder(), Integer.class, I.class);
        myBinder.addBinding(3).to(C.class);
        myBinder.addBinding(4).to(D.class);
      }
    }
    

    An injector configured with those modules will provide an injectable Map<Integer, I> that has an instance of everything bound; here it would be a three-entry map from 1 to a fully-injected A instance, from 3 to a C instance, and from 4 to a D instance. This is actually an improvement over your switch example, which used the new keyword and thus didn't inject any dependencies into A or B.

    For a better option that doesn't create so many wasted instances, inject a Map<Integer, Provider<I>> that MapBinder also provides automatically. Use it like this:

    class YourConsumer {
      @Inject Map<Integer, Provider<I>> iMap;
    
      public void yourMethod(int iIndex) {
        // get an I implementor
        I i = iMap.get(iIndex).get();
        // ...
      }
    }
    

    To provide a "default" implementation (and opaque interface) the way you did, though, you'll want to implement your own short wrapper on top of the MapBinder map:

    class IFactory {
      @Inject Map<Integer, Provider<I>> iMap;
      @Inject Provider<B> defaultI; // Bound automatically for every Guice key
    
      I getI(int i) {
        return iMap.containsKey(i) ? iMap.get(i).get() : defaultI.get();
      }
    }
    

    Simpler, factory-style

    If the above looks like overkill, remember that you can inject an Injector and create a local Map from key to implementation. (You can also use ImmutableMap like I did here).

    class IFactory {
      @Inject Injector injector; // This is a bad idea, except for times like this
      @Inject Provider<B> defaultI;
      static final ImmutableMap<Integer, Class<? extends I>> map = ImmutableMap.of(
          1, A.class,
          3, C.class,
          4, D.class);
    
      I getI(int i) {
        return map.containsKey(i)
            ? injector.getInstance(map.get(i))
            : defaultI.get();
      }
    }