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:
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.
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.