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