组件间通信方式
一种组件间通信方式
QHOServiceManager 是我们推出的业务间通信的解决方案,方便客户端不同的业务仓库之间进行交互。解决不同业务库代码需要互相调用,但是不能直接依赖代码的问题。
没有看懂?没关系,简单来说,当你想要用其他业务库的代码逻辑,却不想,或者,不能把其他业务 pod 添加到你的 podspec 时,你可能就是 QHOServiceManager 的潜在用户。
问题的起因和我们的解决思路:
一、问题与现状
现实中,如果有两个组件中的代码需要互相调用,或者复用代码,比如,我们希望在订单详情页收藏结束后,展示收藏清单。
然而订单详情页面和收藏清单页面分散在两个仓库中,此时,为了实现这个需求,我们有哪些选择呢?
1. 直接依赖
这是最简单直接的一种做法,但是有以下缺点:
1. 订单引入了清单的直接或间接依赖,比如清单使用了 Logan埋点 后,订单也不得不引入,并且在自己的工程中做初始化配置
2. 当清单需要使用订单的代码时,会因为循环依赖导致需要另寻他法
3. 造成应用内整体依赖层级过多,如果大家都这么做,那么可能会出现很多业务库之间相互依赖的情况,增加依赖复杂度。
2. 下沉公共代码
相对间接的一种做法,但是有以下缺点:
1. 需要建立一个新的第三方仓库,成本高
2. 新的第三方仓库可能会成为无人管理的状态
3. 复制代码
简单粗暴,但是:
1. 复制代码需要繁琐的改名,容易忘记改名
2. 造成代码成倍膨胀
4. 运行时依赖
也就是通过类似 NSClassFromString 等方式拿到代码逻辑,但是:
1. 代码存在很多硬编码
2. 当 Class 名称修改时,没有有效的机制去校验
总结上述4种方式:
方法 | 优点 | 缺点 |
直接依赖 | 不需要额外代码改动 |
调用方引入过多无用依赖 依赖层级加深 |
下沉公共代码 | 拆分干净 |
开发成本高 依赖层级加深,严重时影响可维护性 |
复制代码 | 开发成本低 | 代码量增加,影响包体积 |
运行依赖 | 开发成本低 | 隐形依赖,严重时影响可维护性 |
解决方案
思路与目标
这一类问题的常规解决方案是遵循依赖倒置的设计原则(DIP),通过面向接口编程的方式来解决。之前说到的四种解决方案,其实3和4都能看到一点面向接口编程的影子。只是他们对应的协议不是成本太高,就是不可靠
构建一个框架,方便业务方通过结构化,低成本的方式实现面向接口编程
模型设计
本质来说,我们的需求是实现两个同级组件之间优雅的互相调用,系统中可以抽象为如下三个角色:服务提供者,服务调用者和框架。按照目标所述,我们对于组件间调用,定义以下两条选型原则:
1. 遵循 DIP 原则,解耦依赖流和控制流
2. 遵照一般的设计原则,将编译时依赖决议推迟到运行时
根据以上设计原则,抽象模型设计如下:
技术选型
在 iOS 上实现面向接口编程的方案有几种思路,如下:
1. 基于协议, 通过运行时获取实例
2. 基于虚基类,通过 category 覆盖实现扩展
3. performSelector
最后我们选择基于协议的方案,这也是在其他语言框架中常见的 ServiceLoader 设计方案
架构设计
通过对模型的细化,框架设计如下:
组件分为服务的提供方和调用方,双方共同依赖公共协议来交互
ServiceRegistry 是一个公共协议池(层),沉淀了公共协议和元信息
ServiceManager 负责在运行时处理绑定协议的声明和实现
为了弥补 ServiceLoader 的缺点,加强开发体验,框架需要大量使用工具辅助生成代码。
工作流举例
服务提供(P)和使用(C)双方定义公共协议(或者单方面定义)
双方同时依赖公共协议
P 实现并注册自己对协议的实现
C 以协议为参数和 Manager 通信,获取服务实例
C 根据协议,调用服务实例
业务举例
在以上方案中,方案四的短板是我们可以想办法弥补的,所以我们可以建立一个协议仓库(METServiceRegistry)
订单和清单共同依赖这个仓库,因此都可以拿到这个协议
在清单仓库中,声明一个 Service 类实现该协议。
然后在订单仓库中,通过向 QHOServiceManager 传递该协议即可拿到 Service 类的示例,从而调用显示清单的代码。
整体组成
提供三个仓库实现方案功能:
METServiceRegistry: 整个 imeituan 使用的协议池,所有的协议都放到这里
QHOServiceManager: 在运行时,各个组件通过 QHOServiceManager 来通信
cocoapods-service-manager: 由于 METServicePool 是一个所有人都要编辑的仓库,通过这个插件来加快一些本来很麻烦的操作
更多业务场景举例
1. 从订单的收藏页面进入业务方的详情页,详情页取消收藏,通知订单页,订单页不需要重新刷新页面即可同步数据
2. 搜索需要在搜索结果中展示酒旅的内容,这部分 UI 以及数据获取在酒旅的仓库中进行开发,搜索通过 Service Manager 在运行时获取已经渲染好的 UI
3. 美食搜索需要和首页搜索共享搜索记录
4. 首页的导航栏的代码在 PFBEntrance 中,内容在 PFB 中,导航栏的颜色需要根据内容变色。
5. UGC 的业务方需要对 UGC 进行自定义配置