Search code examples
javadependency-injectionguiceassisted-inject

@Assisted \ @Provider usage in objects creation in hierarchical design


This question is about a correct usage of Guice @Assisted and @Provides and also, how to do it.

The current design I refer to is something like this: The class in the top of the hierarchy is also the only class that is exposed to the client (basically, the public API), its looks something like that:

public class Manager{
public Manager(int managerId, ShiftFactory sf, WorkerFactory wf);
 // methods ...
}

As you probably understand, id is provided by the user on creation time (@Assisted?) but the others are not, they are just factories.

The class Manager creates instances of the class Shift. The class Shift creates instances of the class Worker.

Now, in order to create the class Shift we use its constructor:

public Shift(int managerId, int shiftId, WorkerFactory wf);

shiftId provided by the Manager and the rest are the same objects from Manager's constructor.

In order to create Worker we use 2 static factory methods (but it can be changed..):

  public Worker createWorkerTypeA(int shiftId, int workerId)
  public Worker createWorkerTypeB(int shiftId, int workerId)

workerId provided by the Shift class. and the rest is delegated from Shift constructor.

What is the correct, Guice-y way to do it? Where should I put @Assisted? @Provides?

I really would like a code example of that, including the abstract module, because the code exmaples i've seen so far are not understandable to me just yet.

Thanks


Solution

  • At a high level, what you want is for your factories to hide the predictable dependencies so you only have to specify the ones that change. Someone who has an instance of the Factory should only have to pass in data, not factories or dependencies. I picture the interface like this.

    interface ManagerFactory {
      Manager createManager(int managerId);
    }
    interface ShiftFactory {
      Shift createShift(int managerId, int shiftId);
    }
    interface WorkerFactory { // The two methods here might be difficult to automate.
      Worker createWorkerA(int managerId, int shiftId, int workerId);
      Worker createWorkerB(int managerId, int shiftId, int workerId);
    }
    
    class Manager {
      @Inject ShiftFactory shiftFactory;  // set by Guice, possibly in constructor
      private final int managerId;        // set in constructor
    
      Shift createShift(int shiftId) {
        shiftFactory.createWorkerA(this.managerId, shiftId);  // or B?
      }
    }
    
    class Shift {
      @Inject WorkerFactory workerFactory;  // set by Guice, possibly in constructor
      private final int managerId;          // set in constructor
      private final int shiftId;            // set in constructor
    
      Worker createWorker(int workerId) {
        shiftFactory.createShift(this.managerId, this.shiftId, workerId);
      }
    }
    

    Note here that Manager doesn't care at all about workers—it doesn't create them, so unlike in your question, you don't have to accept a WorkerFactory just to pass it along to your Shift. That's part of the appeal of dependency injection; you don't have to concern a middle-manager (middle-Manager?) with its dependencies' dependencies.

    Note also that none of the Factory interfaces or implementations are even slightly visible to your public API outside of constructors. Those are implementation details, and you can follow along the object hierarchy without ever calling one from outside.

    Now, what would a ManagerFactory implementation look like? Maybe like this:

    class ManualManagerFactory {
      // ShiftFactory is stateless, so you don't have to inject a Provider,
      // but if it were stateful like a Database or Cache this would matter more.
      @Inject Provider<ShiftFactory> shiftFactoryProvider;
    
      @Override public Manager createManager(int managerId) {
        return new Manager(managerId, shiftFactoryProvider.get());
      }
    }
    

    ...but that's largely boilerplate, and possibly much more so when there are a lot of injected or non-injected parameters. Guice can do it for you, instead, as long as you still provide your ManagerFactory interface and you annotate a constructor:

    class Manager {
      private final ShiftFactory shiftFactory;  // set in constructor
      private final int managerId;              // set in constructor
    
      @Inject Manager(ShiftFactory shiftFactory, @Assisted int managerId) {
        this.shiftFactory = shiftFactory;
        this.managerId = managerId;
      }
    
      // ...
    }
    
    // and in your AbstractModule's configure method:
    new FactoryModuleBuilder().build(ManagerFactory.class);
    

    That's it. Guice creates its own reflection-based ManagerFactory implementation by reading the return type of the Manager method, matching that to the @Inject and @Assisted annotations and the interface method parameters, and figuring it out from there. You don't even need to call the implement method on FactoryModuleBuilder unless Manager were an interface; then you'd have to tell Guice which concrete type to create.


    For kicks and grins, let's see the same thing with Google's code-generating AutoFactory package:

    @AutoFactory(
        className = "AutoManagerFactory", implementing = {ManagerFactory.class})
    class Manager {
      private final ShiftFactory shiftFactory;  // set in constructor
      private final int managerId;              // set in constructor
    
      @Inject Manager(@Provided ShiftFactory shiftFactory, int managerId) {
        this.shiftFactory = shiftFactory;
        this.managerId = managerId;
      }
    
      // ...
    }
    

    Almost identical, right? This will generate a Java class (with source code you can read!) that inspects the Manager class and its constructors, reads the @Provided annotations (n.b. @Provided is the opposite of FactoryModuleBuilder's @Assisted), and delegates to the constructor with its combination of parameters and injected fields. Two other advantages to Auto, which works with Guice as well as Dagger and other JSR-330 Dependency Injection frameworks:

    1. This is normal Java code free of the reflection in Guice and its FactoryModuleBuilder; reflection performance is poor on Android, so this can be a nice performance gain there.

    2. With code generation, you don't even need to create a ManagerFactory interface--without any parameters to @AutoFactory you would wind up with a final class ManagerFactory { ... } that has exactly the behavior Guice would wire up through FactoryModuleBuilder. Of course, you can customize the name and interfaces yourself, which might also help your developers as generated code sometimes doesn't appear well to tools and IDEs.


    UPDATE to answer comments:

    • Regarding createWorker: Yes, sorry, copypaste error.
    • Regarding automation: It's because neither Assisted Inject nor AutoFactory has a great way to delegate to static methods, or to work with constructors that have identical assisted (user-provided) arguments. This is a case where you might have to write a Factory of your own.
    • Regarding Manager not needing a WorkerFactory: The only reason Manager would require WorkerFactory is if it's creating either a ShiftFactory or a Shift itself by calling the constructor. Note that my example does neither of those: You're letting the dependency injection framework (Guice) provide the dependencies, which means that the WorkerFactory is hiding in the ShiftFactory that Guice is already providing.