【问题标题】:Dagger2 - How to conditionally choose modules at runtimeDagger2 - 如何在运行时有条件地选择模块
【发布时间】:2018-02-19 21:29:02
【问题描述】:

我有一个大型 Android 应用,它需要根据操作系统版本、制造商和许多其他因素运行不同的代码。但是,此应用程序必须是单个 APK。它需要在运行时足够聪明才能确定要使用的代码。到目前为止,我们一直在使用 Guice,但性能问题导致我们考虑迁移到 Dagger。但是,我无法确定我们是否可以实现相同的用例。

我们的主要目标是在启动时运行一些代码以提供兼容模块的列表。然后将该列表传递给 Dagger 以连接所有内容。

这是我们要迁移的 Guice 当前实现的一些伪代码

import com.google.inject.AbstractModule;

@Feature("Wifi")
public class WifiDefaultModule extends AbstractModule {
  @Override
  protected void configure() {
    bind(WifiManager.class).to(WifiDefaultManager.class);
    bind(WifiProcessor.class).to(WifiDefaultProcessor.class);
  }
}

@Feature("Wifi")
@CompatibleWithMinOS(OS > 4.4)
class Wifi44Module extends WifiDefaultModule {
  @Override
  protected void configure() {
    bind(WifiManager.class).to(Wifi44Manager.class);
    bindProcessor();
  }

  @Override
  protected void bindProcessor() {
    (WifiProcessor.class).to(Wifi44Processor.class);
  }
}  

@Feature("Wifi")
@CompatibleWithMinOS(OS > 4.4)
@CompatibleWithManufacturer("samsung")
class WifiSamsung44Module extends Wifi44Module {
  @Override
  protected void bindProcessor() {
    bind(WifiProcessor.class).to(SamsungWifiProcessor.class);
}

@Feature("NFC")
public class NfcDefaultModule extends AbstractModule {
  @Override
  protected void configure() {
    bind(NfcManager.class).to(NfcDefaultManager.class);
  }
}

@Feature("NFC")
@CompatibleWithMinOS(OS > 6.0)
class Nfc60Module extends NfcDefaultModule {
  @Override
  protected void configure() {
    bind(NfcManager.class).to(Nfc60Manager.class);
  }
}

public interface WifiManager {
  //bunch of methods to implement
}

public interface WifiProcessor {
  //bunch of methods to implement
}

public interface NfcManager {
  //bunch of methods to implement
}

public class SuperModule extends AbstractModule {
  private final List<Module> chosenModules = new ArrayList<Module>();

  public void addModules(List<Module> features) {
    chosenModules.addAll(features);
  }

  @Override
  protected void configure() {
    for (Module feature: chosenModules) {
      feature.configure(binder())
    }
  }  
}

所以在启动时应用程序会这样做:

SuperModule superModule = new SuperModule();
superModule.addModules(crazyBusinessLogic());
Injector injector = Guice.createInjector(Stage.PRODUCTION, superModule);

其中 crazyBusinessLogic() 读取所有模块的注释,并根据设备属性确定用于每个功能的单个模块。例如:

  • OS = 5.0 的三星设备将有 crazyBusinessLogic() 返回列表 { new WifiSamsung44Module(), new NfcDefaultModule() }
  • OS = 7.0 的三星设备将有 crazyBusinessLogic() 返回列表 { new WifiSamsung44Module(), new Nfc60Module() }
  • OS = 7.0 的 Nexus 设备将有 crazyBusinessLogic() 返回列表 { new Wifi44Module(), new Nfc60Module() }
  • 等等....

有没有办法对 Dagger 做同样的事情? Dagger 似乎要求您在 Component 注释中传递模块列表。

我阅读了一篇博客,该博客似乎在做一个小型演示,但它看起来很笨拙,额外的 if 语句和额外的组件接口可能会导致我的代码膨胀。

https://blog.davidmedenjak.com/android/2017/04/28/dagger-providing-different-implementations.html

有什么方法可以像在 Guice 中那样使用从函数返回的模块列表?如果没有,最接近的方法是什么可以最大程度地减少重写注释和 crazyBusinessLogic() 方法?

【问题讨论】:

    标签: android dagger-2


    【解决方案1】:

    Dagger 在编译时生成代码,因此您不会像在 Guice 中那样拥有那么多的模块灵活性;而不是 Guice 能够反射性地发现 @Provides 方法并运行反射性 configure() 方法,Dagger 需要知道如何在运行时创建它可能需要的每个实现,它会在编译时需要知道。因此,无法传递任意模块数组并让 Dagger 正确连接您的图表;它破坏了 Dagger 旨在提供的编译时检查和性能。

    也就是说,您似乎可以接受包含所有可能实现的单个 APK,因此唯一的问题是在运行时在它们之间进行选择。这在 Dagger 中是很有可能的,并且可能属于以下四种解决方案之一:David 的基于组件依赖的解决方案、Module 子类、有状态的模块实例或基于@BindsInstance 的重定向。

    组件依赖

    David's blog you linked 一样,您可以定义一个带有一组您需要传入的绑定的接口,然后通过传递给构建器的该接口的实现来提供这些绑定。虽然接口的结构使得这个设计很好,可以将 Dagger @Component 实现传递给其他 Dagger @Component 实现,但该接口可以由任何东西实现。

    但是,我不确定此解决方案是否适合您:此结构也最适合继承独立实现,而不是在您的各种 WifiManager 实现都具有您的图形需要满足的依赖关系的情况下。如果您需要支持“插件”架构,或者如果您的 Dagger 图太大以至于单个图不应包含应用程序中的所有类,您可能会被这种类型的解决方案所吸引,但除非您有这些限制您可能会发现此解决方案冗长且限制性强。

    模块子类

    Dagger 允许非final 模块,并允许将实例传递到模块中,因此您可以通过将 模块的子类 传递到组件的 Builder 来模拟您的方法.因为替换/覆盖实现的能力经常与测试相关联,这在Dagger 2 Testing page under the heading "Option 1: Override bindings by subclassing modules (don’t do this!)" 中有描述——它清楚地描述了这种方法的注意事项,特别是虚拟方法调用将比静态@Provides 方法慢,并且任何被覆盖的 @Provides 方法都必然需要采用 any 实现使用的 all 参数。

    // Your base Module
    @Module public class WifiModule {
      @Provides WifiManager provideWifiManager(Dep1 dep1, Dep2 dep2) {
        /* abstract would be better, but abstract methods usually power
         * @Binds, @BindsOptionalOf, and other declarative methods, so
         * Dagger doesn't allow abstract @Provides methods. */
        throw new UnsupportedOperationException();
      }
    }
    
    // Your Samsung Wifi module
    @Module public class SamsungWifiModule {
      @Override WifiManager provideWifiManager(Dep1 dep1, Dep2 dep2) {
        return new SamsungWifiManager(dep1);  // Dep2 unused
      }
    }
    
    // Your Huawei Wifi module
    @Module public class HuaweiWifiModule {
      @Override WifiManager provideWifiManager(Dep1 dep1, Dep2 dep2) {
        return new HuaweiWifiManager(dep1, dep2);
      }
    }
    
    // To create your Component
    YourAppComponent component = YourAppComponent.builder()
        .baseWifiModule(new SamsungWifiModule())   // or name it anything
                                                   // via @Component.Builder
        .build();
    

    这是可行的,因为您可以提供单个 Module 实例并将其视为 abstract factory pattern,但是通过不必要地调用 new,您并没有充分发挥 Dagger 的潜力。此外,维护所有可能依赖项的完整列表的需要可能会使这比它的价值更麻烦,特别是考虑到您希望所有依赖项都在同一个 APK 中发布。 (如果您需要某些类型的插件架构,或者您希望避免完全基于编译时标志或条件发布实现,这可能是一种更轻量级的替代方案。)

    模块实例

    提供可能是虚拟模块的能力实际上更多地用于使用构造函数参数传递模块实例,然后您可以使用它来在实现之间进行选择。

    // Your NFC module
    @Module public class NfcModule {
      private final boolean useNfc60;
    
      public NfcModule(boolean useNfc60) { this.useNfc60 = useNfc60; }
    
      @Override NfcManager provideNfcManager() {
        if (useNfc60) {
          return new Nfc60Manager();
        }
        return new NfcDefaultManager();
      }
    }
    
    // To create your Component
    YourAppComponent component = YourAppComponent.builder()
        .nfcModule(new NfcModule(true))  // again, customize with @Component.Builder
        .build();
    

    同样,这并没有充分发挥 Dagger 的潜力;您可以通过手动委派给您想要的正确 Provider 来做到这一点。

    // Your NFC module
    @Module public class NfcModule {
      private final boolean useNfc60;
    
      public NfcModule(boolean useNfc60) { this.useNfc60 = useNfc60; }
    
      @Override NfcManager provideNfcManager(
          Provider<Nfc60Manager> nfc60Provider,
          Provider<NfcDefaultManager> nfcDefaultProvider) {
        if (useNfc60) {
          return nfc60Provider.get();
        }
        return nfcDefaultProvider.get();
      }
    }
    

    更好!现在你不需要创建任何实例,除非你需要它们,并且 Nfc60Manager 和 NfcDefaultManager 可以采用 Dagger 提供的任意参数。这就引出了第四个解决方案:

    注入配置

    // Your NFC module
    @Module public abstract class NfcModule {
      @Provides static NfcManager provideNfcManager(
          YourConfiguration yourConfiguration,
          Provider<Nfc60Manager> nfc60Provider,
          Provider<NfcDefaultManager> nfcDefaultProvider) {
        if (yourConfiguration.useNfc60()) {
          return nfc60Provider.get();
        }
        return nfcDefaultProvider.get();
      }
    }
    
    // To create your Component
    YourAppComponent component = YourAppComponent.builder()
        // Use @Component.Builder and @BindsInstance to make this easy
        .yourConfiguration(getConfigFromBusinessLogic())
        .build();
    

    这样你可以将你的业务逻辑封装在你自己的配置对象中,让 Dagger 提供你需要的方法,并返回到带有静态@Provides 的抽象模块以获得最佳性能。此外,您不需要为您的 API 使用 Dagger @Module 实例,它隐藏了实现细节,并且如果您的需求发生变化,以后可以更轻松地从 Dagger 中移除。对于您的情况,我推荐此解决方案;这需要一些重组,但我认为您最终会得到一个更清晰的结构。

    关于 Guice Module#configure(Binder) 的附注

    打电话feature.configure(binder()) 不习惯;请改用install(feature);。这使 Guice 能够更好地描述代码中发生错误的位置,发现模块中的 @Provides 方法,并在模块安装多次时对模块实例进行重复数据删除。

    【讨论】:

    • 我更喜欢多组件/多模块解决方案。一方面,您不受组件依赖项的约束(例如,不能设置为 @Nullableamd 某些配置可能会或不能使用它们),并且不需要将所有内容延迟注入您的 @Provides 方法。另一方面,Dagger 不允许您延迟注入组件本身(例如,当您想在较高范围内实例化较低范围的组件时需要)。这些是通过延迟注入的组合配置对象对配置的主要缩减。
    【解决方案2】:

    有没有办法只使用从 就像我们在 Guice 中所做的那样?如果不是,最接近的是什么 最大限度地减少重写注释和 crazyBusinessLogic() 方法?

    不确定这是否是您正在寻找的答案,但以防万一您有其他选择,对于其他社区成员,我将描述完全不同的方法。

    我想说,到目前为止,您使用 Guice 的方式是对 DI 框架的滥用,您最好利用这个机会消除这种滥用,而不是在 Dagger 中实现它。

    让我解释一下。

    依赖注入架构模式的主要目标是使构造逻辑与功能逻辑分离。

    您基本上想要实现的是标准多态性 - 基于一组参数提供不同的实现。

    如果您为此目的使用模块和组件,您最终将根据管理这些多态实现需求的业务规则来构建您的 DI 代码。

    这种方法不仅需要更多样板文件,而且还可以防止出现具有有意义结构并提供对应用程序设计和架构的洞察力的内聚模块。

    此外,我怀疑您能否在依赖注入逻辑中对这些“编码”的业务规则进行单元测试。

    恕我直言,有两种方法要好得多。

    第一种方法仍然不是很干净,但至少它不会损害依赖注入代码的大规模结构:

    @Provides
    WifiManager wifiManager(DeviceInfoProvider deviceInfoProvider) {
        if (deviceInfoProvider.isPostKitKat() ) {
            if (deviceInfoProvider.isSamsung()) {
                return new WifiMinagerSamsungPostKitKat();
            } else {
                return new WifiMinagerPostKitKat();                
            }
        } else {
            return new WifiMinagerPreKitKat();
        }
    }
    

    在实现之间进行选择的逻辑仍然存在于 DI 代码中,但至少它没有进入该部分的大规模结构中。

    但在这种情况下,最好的解决方案是进行适当的面向对象设计,而不是滥用 DI 框架。

    我很确定所有这些类的源代码都非常相似。它们甚至可能在仅覆盖一个方法的同时相互继承。

    在这种情况下,正确的方法不是复制/继承,而是使用策略设计模式进行组合。

    您可以将“策略”部分提取到一个独立的类层次结构中,并定义一个工厂类,该类根据系统参数构造它们。然后,你可以这样做:

    @Provides
    WiFiStrategyFactory wiFiStrategyFactory(DeviceInfoProvider deviceInfoProvider) {
        return new WiFiStrategyFactory(deviceInfoProvider);
    }
    
    @Provides
    WifiManager wifiManager(WiFiStrategyFactory wiFiStrategyFactory) {
        return new WifiMinager(WiFiStrategyFactory.newWiFiStrategy());
    }
    

    现在构造逻辑简单明了。封装在WiFiStrategyFactory 中的策略之间的区别,可以进行单元测试。

    这种正确方法的最佳部分是,当需要实施新策略时(因为我们都知道 Android 碎片是不可预测的),您不需要实施新的模块和组件,或对DI结构。只需提供该策略的另一个实现并将实例化逻辑添加到工厂,即可处理此新要求。

    所有这些都通过单元测试保持安全。

    【讨论】:

      猜你喜欢
      • 2021-09-06
      • 1970-01-01
      • 1970-01-01
      • 2023-04-09
      • 1970-01-01
      • 2020-12-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多