随着我们业务发展,参与业务开发的同学也逐渐增多。为了适应新要求,需要对旧的架构做一次升级。组件化是架构升级中的重要一步,将业务模块进行组件化,将各个业务的逻辑和依赖梳理清楚,才能有效降低业务迭代带来的复杂度,为后续更复杂的优化做铺垫。
iOS的App架构早期设计分为三层:业务模块层、基础业务层、基础功能层。
业务模块层承载具体业务模块,基础业务层封装业务基础能力,基础功能层桥接依赖的Pod库。 随着业务的发展,SSCommon基础功能层接入越来越多的功能库,SSFoundation基础业务层沉淀出来BDNovelKit、BDNovelWidget、阅读器SDK和听书SDK,SSApp业务模块层也在不断生成新的功能模块。
经历多个版本的迭代,存在几个较为明显的问题: 业务模块依赖无抽象,以主Tab和分类业务模块为例,既没有主Tab要求分类业务模块实现的TabProtocol,也没有分类业务模块要求宿主实现的Delegate; 层级界限不清晰,当新增某个功能业务时,对应数据结构类型、业务组件等全部放在业务模块层; 业务模块调用不规范,比如说在书城模块需要使用其他业务模块(搜索、有声、金币)时,会直接引入的对应业务模块实例;
组件化的目标是逻辑内聚、依赖抽象。 将复杂的工程,拆分为若干个独立的组件,将复杂的逻辑内聚,仅对外暴露接口,降低整个工程的复杂度。同时也是方便后续工程进行拆分,对于部分业务进行子仓化,部分通用的基础组件甚至可以多App复用。
使用范围不局限于App的基础库,既有公司提供的Heimdallr,也有自己维护的BDReader。
仅服务于自己业务,会被上层业务直接使用,包括SSBook这种业务基础数据结构,也有业务通用的SSBaseView,这些代码会封装到若干个Pod库。
业务接口层主要是描述每个业务组件的接口,调用组件应该通过接口去调用,接口层是组件生成必备。
业务实现层是接口层的具体实现,组件化之后的业务组件主要逻辑放在这里,实现层是组件生成必备。
目前整个App大部分逻辑都是放在这里,随着组件化的推进,逐渐沉淀部分逻辑到业务实现层、业务接口层和业务基础层。
以分类组件为例,这是分类组件的大致构成。
SSCategoryImp可以直接使用业务基础层的SSBook和SSBaseView,也可以使用通用基础层的BDALog和Heimdallr。 主App会通过SSCategoryInterface去调用分类组件,同时也会实现一个SSCategoryDelegate,作为分类部分功能的回调。
组件内部也要分层,便于后续组件管理,以及组件间能力复用。分层建议包括组件接口层、组件实现层、组件基础层、组件数据层。 组件接口层:存放组件对外提供能力的抽象接口; 组件实现层:存放组件对接口实现的具体代码; 组件基础层:存放组件对外提供的业务UI能力;这部分复用组件较多之后,需要下沉到App的业务基础层; 组件数据层:存放组件对外提供的业务Model;这部分复用组件较多之后,需要下沉到App的业务基础层;
在我们的工程里,模块化指的是将业务功能模块拆分成若干个功能模块,模块可以在多个业务中复用。比如说某个书评业务使用评论组件、点赞组件、气泡组件等进行模块化编程。模块化编程可以有效提供代码的复用率,同时也便于沉淀组件。
直接分仓会存在增加开发成本和维护成本的问题,短期组件化会有较多改动。但是分仓又是一种比较好的物理隔离方式,可以减慢代码劣化,同时也比较方便管理依赖。待组件化成熟之后,再进行代码分仓。 业务基础层可以直接沉库,方便建立明确的层级关系。
组件通信其实就是组件A调用组件B的某个功能,这里有几种方式,以分类组件和播放组件为例。 接口调用:分类组件有业务逻辑需要感知当前正在播放的书籍id,那么应该通过播放组件提供的抽象接口(而不是播放器实例),拿到这个书籍id; 依赖注入:分类组件描述需要依赖外部实现的能力并提供注入接口,然后外部组件再主动通过接口注入该能力的实现; 消息通知:播放组件在切换当前播放书籍时,可以通过消息通知所有关注的业务组件,业务组件Register的Message是在业务基础层;
工程梳理,明确基础业务层范围,业务基础层搭建; 接口层实现和组件注册机制; 基础工具支持准备,组件调用能力;
业务落地,以某个业务做组件化,打造show case; 组件化结构合理,包括接口、数据和实现; 搭建配套的检查、review、多仓合码等机制;
降低改造成本,并逐步推广到多个业务;
依赖版本管理问题,组件本身需要和主端App保持一致依赖版本号。 拆分组件之后,如果使用分仓隔离,则不能直接操作组件,需要引入多仓开发。
按照使用范围有: 通用组件,多端App都能用的组件; 业务组件,只有当前业务使用的组件;
按照内容区分: 功能组件,业务的具体业务组件,着重点在于逻辑内聚,代码隔离; 基础组件,业务的基础功能组件,着重点在于功能的重复利用;
代码+资源+依赖+抽象。 代码:承载具体业务逻辑的.h/.m; 资源:业务使用的图片等其他资源; 依赖:组件需要主App实现的能力; 抽象:组件提供给主App使用的能力,也就是接口层,接口层包括方法和数据类型;
Development Pods,组件代码和主工程代码同仓; Pod库,类似第三方库,代码分仓; 子工程依赖,业务组件单独成为一个工程,被主工程依赖,代码同仓; 文件夹分隔,同一个主工程,用不同文件夹分隔,通过添加文件夹Reviewer来避免劣化,代码同仓;
所有的代码分仓,都需要考虑业务组件和主App的依赖版本一致问题,有些成熟业务会有业务组件容器化的解决方案,可以把业务组件和主App的pod版本对齐。 代码分仓带来了另外一个问题是工程配置、宏定义、xcconfig等不同步,需要及时打通和更新。
比较好的实践是综合上述的过程: 1、先梳理依赖,再用文件夹做简单分隔; 2、对应文件夹用Development Pods进行分隔,这一步也可以用子工程来实现;devPod相对简单,但是无法完全隔离;子工程可以物理隔离,但是维护相对麻烦; 3、Pod子仓,明确组件的subspec和podspec依赖;
目前并不打算让主App对系统事件进行封装,App生命周期、Push处理、系统事件处理等由主App处理,各个组件可以直接去监听系统的事件。但有些事件只会回调到主App,那么再由主App分发事件。 待到组件化足够成熟,可以把主App再拆出若干个负责事件转发的底层组件,作为通用的依赖(容器层)。
需要有组件的接口和绑定方法,这里用到了一个公司内部提供的库来实现,基本原理还是通过MachO段来做绑定。 组件外通过GET_PROTOCOL(SSProtocol)和GET_PROTOCOL_CLASS(SSxxProtocol) 两种方式来调用,组件内通过BIND_PROTOCOL_SERVICE(SSProtocol, SSxxImpl)来绑定接口和实现。
网上关于组件化的文章非常多,这里主要介绍我们业务的思考和落地过程。 架构优化是一件持之以恒的事情,技术方案总是可以持续完善。最初的设计不用太复杂,逻辑清晰便于演进架构,能解决当下协作痛点,适应业务迭代的节奏,就是一个好的解决方案。
代码架构,合久必分,分久必合。