Search code examples
javadagger-2dagger

How to create abstract factory for many concrete types


Let's assume we have interface named IAction, and a lot of classes (more than 20) implementing this interface: ConreteAction1, ConreteAction2, etc. All these classes have constructors with parameters. Also all of these classes, as well as their dependencies, are registered in Dagger module (as transient objects or singletons). Our task is to implement (or auto-generate) abstract factory implementing interface similar to:

public interface IActionFactory{
    IAction createByClass(Class clazz); // create action by type
    // or
    IAction createByName(String name); // create action by custom name
}

I know how I'm able to implement this factory using Dagger's support for Provider<T> annotation:

public class ActionFactory implements IActionFactory{

    @Inject Provider<ConcreteAction1> concreteAction1Provider;
    @Inject Provider<ConcreteAction2> concreteAction2Provider;
    (...)

    @Override
    public IAction createByClass(Class clazz){

        if(ConcreteAction1.class.equals(clazz)){
            return concreteAction1Provider.get()
        }

        if(ConcreteAction2.class.equals(clazz)){
            return concreteAction2Provider.get()
        }
        (...)
    }

    @Override
    public IAction createByName(String name){

        if(name.equals("name_1")){
            return concreteAction1Provider.get()
        }

        if(name.equals("name_2")){
            return concreteAction2Provider.get()
        }
        (...)
    }
}

Unfortunately, this approach involves a lot of boilerplate code (I have more than 20 concrete classes), and factory above has to be modified each time I create another implementation of IAction interface (violation of Open-Close principle).

Is there any other way in Dagger to create such a factory implementation in more elegant and extendable way?


Solution

  • You can use Multibindings for this.

    @Module public interface ActionModule {
      @Binds @IntoMap @ClassKey(ConcreteAction1.class)
      IAction bindActionClass1(ConcreteAction1 action1);
    
      @Binds @IntoMap @ClassKey(ConcreteAction2.class)
      IAction bindActionClass2(ConcreteAction2 action2);
    
      // ...
    
      @Binds @IntoMap @StringKey("name_1")
      IAction bindActionName1(ConcreteAction1 action1);
    
      @Binds @IntoMap @StringKey("name_2")
      IAction bindActionName2(ConcreteAction2 action2);
    
      // ...
    }
    
    public class ActionFactory implements IActionFactory{
      @Inject Map<Class<?>, Provider<IAction>> classActionFactory;
      @Inject Map<String, Provider<IAction>> stringActionFactory;
    
      @Override
      public IAction createByClass(Class<?> clazz) {
        // TODO: handle missing entries gracefully
        return classActionFactory.get(clazz).get();
      }
    
      @Override
      public IAction createByName(String name) {
        return stringActionFactory.get(name).get();
      }
    }
    

    At this point the main difficulty is that you are binding each action twice, once in each map. If this is a problem, you could use a Set binding to aggregate a set of configurations and then use the map binding to retrieve the right Provider.