我正在参加「启航方案」

在模块化代码库中运用插件形式

模块化已经成为大规模移动开发的一个重要部分, 但是它并不简略. 有用模块化的目标之一是保持模块独立模块图谱扁平. 运用跨模块的插件接口是完成这一目标并取得所需的优点的最有用技术之一. 让咱们看看怎么运用它.

在模块化代码库中使用插件模式

问题

在模块化过程中, 咱们经常面对一个常见的场景–一个模块需求依靠许多其他的功用, 并将它们组合在一起. 幻想一下使用程序发动时初始化使用程序的不同部分–在这种情况下, 发动代码必须要拜访许多功用.

典型的做法是让咱们的功用依靠于一切相关的模块, 以便能够引证代码. 如果咱们依靠很少的模块, 这或许是能够的, 但是一旦依靠性增加, 就会引起一些问题.

  • 耦合性: 一个功用依靠于许多其他功用.
  • 杂乱度: 在一个模块中聚集的依靠性.
  • 模块图: 一旦咱们有了更多的这些聚合功用, 模块图就开端变得愈加杂乱, 咱们终究会有一个难以办理的依靠联系网(为什么重要).

在模块化代码库中使用插件模式

问题是: 图的高度增加了, 并且模块是紧密耦合的.

插件的运用情况

在很多情况下, 你需求许多模块为一起的逻辑做出奉献, 这时插件就供给了一个解决方案. 一些比如:

  • 使用程序发动: 许多模块需求某种形式的初始化, 并需求钩住使用程序.onCreate办法或生命周期中的不同点.
  • 登录或刊出: 使用程序的不同部分需求在用户登录或刊出时进行设置或清理.
  • 处理推送: 通常一个模块是接纳推送信息的进口点, 但是多个模块或许希望接纳不同类型的信息.
  • 处理Deep Link: 与推送相似, 一个模块作为一个进口点, 而许多模块需求处理不同的深度链接.
  • 功用开关: 一个模块或许想声明不同的切换, 但是使用程序或许有一个单一的切换端点供一切模块运用.
  • HTTP头: 不同的模块想为使用程序设置一起的头文件.

解决方案

一般的解决方案触及一个跨模块组成的插件调集, 手动收集或经过依靠注入, 如Dagger. 咱们将运用一个运用Dagger的登录插件作为比如, 但是相同的概念也能够经过其他传递依靠联系的办法完成.

在模块化代码库中使用插件模式

插件形式–登录实例.

  1. 一个功用模块定义了一个公共的插件接口, 并希望收到一个实例的调集, 完成该接口.
    让咱们幻想一个:login-api模块供给一个接口, 这个接口将在许多模块中运用.
interface LoginPlugin {
  fun onLogin(user: User)
  fun onLogout()
}
  1. :login模块将插件作为一个调集进行消费.
class LoginLogic @Inject constructor(
  val loginPlugins: @JvmSuppressWildcards Set<LoginPlugin>)
) {
  ... 
  fun onLogin(user: User) {
    loginPlugins.forEach { plugin -> plugin.onLogin(user) }
  }
  fun onLogout() {
    loginPlugins.forEach { plugin -> plugin.onLogout() }
  }
  ...
  1. 其他模块依靠于:login-api, 并完成他们自己的插件. 例如, :user-theme模块依据用户设置改动主题.
class UserThemePlugin @Inject constructor() : LoginPlugin {
    override fun onLogin(user: User) {
      setPreferredTheme(user.prefferedTheme)
    }
    override fun onLogout() {
      setDefaultTheme()
    }
  } 
  1. 最终一块是把工作连接起来, 这能够用Dagger多绑定来完成. :user-theme模块运用@IntoSet注解来奉献给Set<LoginPlugin>. 一切有绑定的模块然后在:app模块中组成.
@Binds
@IntoSet
fun bindUserThemePlugin(plugin: UserThemePlugin) : LoginPlugin

Set<LoginPlugin>现在将包含UserThemePlugin实例, LoginLogic将开端调用相关办法 – UserThemePlugin“插入”了登录.

为什么这很强壮?

  1. :login模块与使用程序的其他部分完全解耦, 不知道任何关于主题的信息.
  2. 咱们能够依据咱们包含的模块来增加或删去逻辑. 当你有多个使用程序, 想具有只包括部分代码库的使用程序以完成快速开发或即时使用程序时, 这变得很有用.
  3. 咱们能够为咱们的测验供给不同的插件, 使咱们能够验证逻辑.
  4. 咱们能够完成完全隔离的功用, 不需求修改出模块 – 只要插件.

比如

这个概念肯定不是新的, 咱们能够找到很多现有的比如.

  • OkHttp中的Interceptor就像具有责任链的插件. 许多其他的库经过供给他们自己的拦截器来扩展OkHttp.
  • ActivityLifecycleCallbacksFragmentLifecycleCallbacks是一种强壮的方式, 能够在不修改任何现有功用的情况下向你的使用程序增加新的逻辑, 例如被Firebase使用内音讯或云音讯或上下文无关的导航运用.
  • PushActionCommand能够展现怎么委托推送并运用@IntoMap注解来挑选不同的完成.
  • OnAppCreate展现了使用程序的发动逻辑, 在许多情况下与 ActivityLifecycleCallbacks相结合, 供给可插拔的功用, 如性能监控或日志记载.
  • LinkLauncher展现了与Deep Link导航挂钩的不同模块.

应战

没有什么是完美的, 插件接口也有自己的缺陷, 咱们需求记住这些缺陷, 以避免受到它们的影响.

  • 短少必要的插件: 很容易忘记增加使用所依靠的插件, 或许过错地绑定了它. 该使用程序将编译和运行正常, 但咱们会失去一些功用, 导致过错的行为.
    • 解决方案是运用集成测验, 验证功用的行为是否契合预期.
  • 过错处理: 简略地将插件的履行包裹在try-catch中或许是很诱人的, 但咱们无法知道插件抽象中的哪些逻辑会产生过错. 插件的不完全履行会导致不一致的状况.
    • 这个解决方案取决于用例, 但一般的建议是由具体的插件来处理抛出的过错并自行恢复. 如果插件自己不能处理过错, 那么咱们应该只是传播反常, 而不是企图在插件调集的层面上处理它.
  • 插件之间的依靠联系: 一组插件或许需求按照一定的顺序运行, 并出现隐含的依靠联系.
    • 解决方案是设置一个优先级, 然后在运用@IntoMap Dagger注解进行注入后, 咱们能够经过优先级对插件进行排序, 或许运用已知完成的枚举作为优先级的要害. 这将把一些完成常识传播到插件声明中, 作为一种权衡, 但当需求对插件进行准确排序时, 这或许是有用的.
  • 低性能的插件: 如果有很多插件, 或许有一个插件在做贵重的工作, 那么插件的履行或许会变得很慢. 由于插件接口的一切者不能控制和看到奉献插件的完成.
    • 解决方案是在使用程序发动等要害部分监测每个插件的履行目标, 以确认潜在的瓶颈.

享受插件的世界

模块化和扁平化模块图谱很难, 因此需求引入某些模块化形式. 基于插件的办法就是这样一种形式, 使用它能够协助带来模块化和高度模块化代码库的预期优点.

你在你的项目中运用插件或其他模块化办法吗?

祝你模块化愉快!