My understanding of dependency injection is it quickly allows someone to switch out implementations or use test implementations. I'm trying to understand how you're expected to do it in dagger. For me it seems like you should be able to switch out Module implementations, but that doesn't seem to be supported by dagger. What is the idiomatic way to do it.
For example:
@component{modules = UserStoreModule.class}
class ServerComponent {
Server server();
}
class UserStoreModule {
@Provides
UserStore providesUserStore() {
return // Different user stores depending on the application
}
}
Assuming user store is an interface, what if I want to be able to use a mysql UserStore or a redis UserStore depending on the situation. Would I need to have two different server components? Intuitively I feel like I should be able to switch out which user store I use in The DaggerServerComponent.builder() since that'd be a lot less code than multiple components.
Conceptually, it is true that dependency injection "allows someone to switch out implementations or use test implementations": You've written your classes to accept any implementation of UserStore, and can supply an arbitrary one in a constructor call for tests. This is the case whether or not you use Dagger, and is a big advantage in design.
However, Dagger's most prominent feature--its compile-time code generation--makes it somewhat more limited here than alternatives such as Spring or Guice. Because Dagger generates the classes it needs at compile time, it needs to know exactly which implementations it might encounter, so that it can prepare those implementations' dependencies. Consequently, you couldn't take in an arbitrary Class<? extends UserStore>
at runtime and expect Dagger to fill in the rest.
This leaves you with a few options:
Create two separate Module classes, one for each implementation, and use them to let Dagger generate two separate components. This will generate the most efficient code, particularly when using @Binds
, because Dagger will not need to generate any code for the implementation you're not binding. Of course, though this allows you to reuse your classes and some of your modules, it doesn't allow the decision between implementations to be made at runtime (short of choosing between entire Dagger component implementations).
This option entails a very small amount of handwritten code, but does generate a lot of extra code in the component implementations. It probably isn't what you're looking for, but it's included to highlight its differences from the others, and should still be used when possible.
@Module public interface MySqlModule {
@Binds UserStore bindUserStore(MySqlUserStore mySqlUserStore);
}
@Module public interface RedisModule {
@Binds UserStore bindUserStore(RedisUserStore redisUserStore);
}
@Component(modules = {MySqlModule.class, OtherModule.class})
public interface MySqlServerComponent { Server server(); }
@Component(modules = {RedisModule.class, OtherModule.class})
public interface RedisServerComponent { Server server(); }
Create a subclass of your Module with different behavior. This precludes you from using @Binds
or static/final @Provides
methods, causes your @Provides
method to take (and generate code for) unnecessary extra dependencies, and requires you to explicitly make and update constructor calls as dependencies may change. Due to its fragility and optimization opportunity-cost, I wouldn't recommend this option in most cases, but it can be handy for limited cases like substituting dependency-light fakes in tests.
@Module public class UserStoreModule {
@Provides public abstract UserStore bindUserStore(Dep1 dep1, Dep2 dep2, Dep3 dep3);
}
public class MySqlUserStoreModule extends UserStoreModule {
@Override public UserStore bindUserStore(Dep1 dep1, Dep2 dep2, Dep3 dep3) {
return new MySqlUserStore(dep1, dep2);
}
}
public class RedisUserStoreModule extends UserStoreModule {
@Override public UserStore bindUserStore(Dep1 dep1, Dep2 dep2, Dep3 dep3) {
return new RedisUserStore(dep1, dep3);
}
}
DaggerServerComponent.builder()
.userStoreModule(
useRedis
? new RedisUserStoreModule()
: new MySqlUserStoreModule())
.build();
Of course, your Module could even delegate to an arbitrary external Provider<UserStore>
, at which point it would become tantamount to a component dependency. If you use want to use Dagger to generate the Provider or Component you depend on, though, this technique won't help you other than to break your graph into smaller pieces.
Wire up both types at compile time, and only use one at runtime. This requires Dagger to prepare injection code for all your options, but allows you to switch by providing a Module parameter, and even allows you to change which object is provided (if you use a mutable parameter or read a value out of the object graph). Note that you'll still have a slight bit more overhead than with @Binds
, and Dagger will still generate code for your options' dependencies, but the selection process here is clear, efficient, and Proguard-friendly.
This is probably the best general solution, but not ideal for test implementations; it's generally frowned-upon to let test-specific code sneak into production. You'll want module overrides or separate components for that kind of case instead.
@Module public class UserStoreModule {
private final StoreType storeType;
UserStoreModule(StoreType storeType) { this.storeType = storeType; }
@Provides UserStore provideUserStore(
Provider<MySqlUserStore> mySqlProvider,
Provider<RedisUserStore> redisProvider,
Provider<FakeUserStore> fakeProvider) {
switch(storeType) {
case MYSQL: return mySqlProvider.get();
case REDIS: return redisProvider.get();
case FAKE: return fakeProvider.get(); // you probably don't want this in prod
}
throw new AssertionError("Unknown store type requested");
}
}
When you truly need to decide at runtime, inject multiple Providers and choose from there. If you only need to select at compile time or test time, you should use module overrides or separate Components.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With