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

1

10

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

Castorina answered 16/7, 2013 at 6:23 Comment(2)
If you want switch based on object type, Enum should be the correct wayOringas
Right. That's just typed in for simplicity.Castorina
G
19

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();
  }
}
Grisons answered 16/7, 2013 at 17:27 Comment(2)
It is possible to call injector and get instance of that map? Might be a better question is where to create injector for a servlet class?Castorina
You can inject the Map using @Inject; getting it from the injector is possible, but requires an ugly TypeLiteral for the Map parameters. There are several ways to inject into servlets; search Google or SO for them--too long for a comment.Grisons

© 2022 - 2024 — McMap. All rights reserved.