代码之家  ›  专栏  ›  技术社区  ›  Paul Nogas

Dagger2-如何在运行时有条件地选择模块

  •  14
  • Paul Nogas  · 技术社区  · 7 年前

    我有一个很大的Android应用程序,它需要运行不同的代码,这取决于操作系统版本、制造商和许多其他东西。但是,此应用程序需要是单个APK。它需要在运行时足够智能,以确定要使用的代码。到目前为止,我们一直在使用Guice,但性能问题导致我们考虑迁移到Dagger。然而,我无法确定我们是否可以实现相同的用例。

    我们的主要目标是在启动时运行一些代码,以提供兼容模块的列表。然后把这张单子传给Dagger,把所有的东西都连接起来。

    下面是我们要迁移的GUI中当前实现的一些伪代码

    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似乎要求您传递组件注释中的模块列表。

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

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

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

    2 回复  |  直到 7 年前
        1
  •  16
  •   Jeff Bowman    7 年前

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

    也就是说,您似乎可以使用包含所有可能实现的单个APK,所以唯一的问题是在运行时在它们之间进行选择。这在Dagger中非常可能,并且可能会分为四种解决方案之一:David的基于组件依赖关系的解决方案、模块子类、有状态模块实例或 @BindsInstance -基于重定向。

    组件依赖关系

    如中所示 David's blog you linked ,可以使用需要传入的一组绑定定义一个接口,然后通过传入生成器的该接口的实现提供这些绑定。虽然界面的结构使得这个精心设计的 @Component 其他匕首的实现 @组件 实现时,接口可以由任何东西实现。

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

    模块子类

    匕首允许非- final 模块,并允许将实例传递到模块中,因此您可以通过传递 模块的子类 进入组件的生成器。由于替代/覆盖实现的能力通常与测试相关,因此在 Dagger 2 Testing page under the heading "Option 1: Override bindings by subclassing modules (don’t do this!)" 它清楚地描述了这种方法的注意事项,特别是虚拟方法调用将比静态方法调用慢 @提供 方法,并且任何 @提供 必须采取的方法 全部的 参数 任何 实现使用。

    // 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();
    

    这是可行的,因为您可以提供单个模块实例并将其视为 abstract factory pattern ,但通过调用 new 不必要的是,你们并没有充分发挥匕首的潜力。此外,需要维护所有可能依赖项的完整列表可能会使这一点变得更加麻烦,尤其是考虑到您希望所有依赖项都在同一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();
    

    同样,这并没有充分发挥匕首的潜力;您可以通过手动委托给所需的正确提供者来实现这一点。

    // 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提供您所需的方法,并使用静态 @提供 以获得最佳性能。此外,您不需要为API使用Dagger@模块实例,这会隐藏实现细节,并且在以后需要更改时更容易离开Dagger。对于您的情况,我推荐这种解决方案;这将需要一些重组,但我认为最终会有一个更清晰的结构。

    关于GUI模块#配置(活页夹)的旁注

    打电话是不习惯的 feature.configure(binder()) ; 请使用 install(feature); 相反这允许Guice更好地描述代码中发生错误的位置,发现 @提供 方法,并在模块安装多次时消除模块实例的重复。

        2
  •  4
  •   Vasiliy    7 年前

    有没有办法只使用从 就像我们在Guice做的那样?如果不是,最接近的是什么 这样可以将重写注释和 crazyBusinessLogic()方法?

    我不确定这是你想要的答案,但万一你有其他选择,对于其他社区成员,我将描述完全不同的方法。

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

    让我解释一下。

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

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

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

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

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

    有两种方法比IMHO好得多。

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

    @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结构进行任何更改。这个新的需求将通过提供策略的另一个实现并向工厂添加实例化逻辑来处理。

    所有这些,同时通过单元测试保持安全。