Dagger 开关模块

Jos*_*son 1 java dagger-2

我对依赖注入的理解是它允许某人快速切换实现或使用测试实现。我试图了解你应该如何用匕首做到这一点。对我来说,您似乎应该能够切换模块实现,但这似乎不受 dagger 支持。什么是惯用的方式来做到这一点。

例如:

@component{modules = UserStoreModule.class}
class ServerComponent {
  Server server();

}


class UserStoreModule {
    @Provides
    UserStore providesUserStore() {
      return // Different user stores depending on the application
   }
}
Run Code Online (Sandbox Code Playgroud)

假设用户存储是一个接口,如果我希望能够根据情况使用 mysql UserStore 或 redis UserStore 怎么办。我需要两个不同的服务器组件吗?直觉上,我觉得我应该能够切换出我在 DaggerServerComponent.builder() 中使用的用户存储,因为这比多个组件要少得多的代码。

Jef*_*ica 7

从概念上讲,确实依赖注入“允许某人切换实现或使用测试实现”:您已经编写了类来接受 UserStore 的任何实现,并且可以在构造函数调用中提供任意一个以进行测试。无论您是否使用 Dagger,情况都是如此,这是设计上的一大优势。

然而,Dagger 最突出的特性——它的编译时代码生成——使它在这里比 Spring 或 Guice 等替代品更受限制。因为 Dagger 在编译时生成它需要的类,所以它需要确切地知道它可能遇到哪些实现,以便它可以准备这些实现的依赖项。因此,您不能Class<? extends UserStore>在运行时接受任意内容并期望 Dagger 填补其余部分。

这让您有几个选择:

单独的模块,单独的组件

创建两个单独的 Module 类,每个实现一个,并使用它们让 Dagger 生成两个单独的组件。这将生成最高效的代码,尤其是在使用 时@Binds,因为 Dagger 不需要为您未绑定的实现生成任何代码。当然,虽然这允许您重用您的类和一些模块,但它不允许在运行时做出实现之间的决定(缺少在整个 Dagger 组件实现之间进行选择)。

此选项只需要很少量的手写代码,但会在组件实现中生成大量额外代码。它可能不是您要查找的内容,但包含它以突出其与其他内容的差异,并且仍应在可能的情况下使用。

@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(); }
Run Code Online (Sandbox Code Playgroud)

子类化模块

创建具有不同行为的模块的子类。这会阻止您使用@Binds或 静态/最终@Provides方法,导致您的@Provides方法采用(并为其生成代码)不必要的额外依赖项,并要求您在依赖项可能更改时显式地进行和更新构造函数调用。由于其脆弱性和优化机会成本,我不会在大多数情况下推荐此选项,但对于有限的情况,例如在测试中替换依赖轻假货,它可能很方便。

@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();
Run Code Online (Sandbox Code Playgroud)

当然,你的 Module 甚至可以委托给一个任意的 external Provider<UserStore>,此时它就等同于一个组件依赖。但是,如果您想使用 Dagger 来生成您所依赖的 Provider 或 Component,那么除了将图形分解成更小的部分之外,这种技术对您无济于事。

在多个提供商中进行选择

在编译时连接这两种类型,并在运行时只使用一种。这需要 Dagger 为所有选项准备注入代码,但允许您通过提供 Module 参数进行切换,甚至允许您更改提供的对象(如果您使用可变参数或从对象图中读取值) . 请注意,与 with 相比,您仍然会有更多的开销@Binds,并且 Dagger 仍会为您的选项的依赖项生成代码,但这里的选择过程清晰、高效且对 Proguard 友好。

这可能是最好的通用解决方案,但对于测试实现来说并不理想;让特定于测试的代码潜入生产环境通常是不受欢迎的。对于这种情况,您将需要模块覆盖或单独的组件。

@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");
  }
}
Run Code Online (Sandbox Code Playgroud)

总之

当您确实需要在运行时做出决定时,请注入多个 Provider 并从中进行选择。如果只需要在编译时或测试时进行选择,则应使用模块覆盖或单独的组件。