****
****
组件化
一. 项目现状
当前iOS端APP项目大概有35万行代码,早期为了iPad和iPhone双端开发的效率,将所有业务模块的网络请求和数据模型统一抽离到DDEngine工程,自定义了私有开发库DDDevLib工程,和第三方SDKs管理库ThirdSDKs工程,和功能相对独立的DDMIX_UI工程。在项目较小时,这个架构是完全可以满足开发需要的,也是容易推进的。但是开发过程中缺少组件化的考量,随着产品线的扩展和人员流动,对项目不熟悉和开发规范不够严谨,造成业务模块依赖关系非常复杂,主要有三类耦合:
工程耦合:某些模块在运行时需要依赖主工程的代码才能运行或实现完整的功能;
界面耦合:App 执行过程中,硬编码的界面跳转行为;
依赖耦合:两个业务模块之间的代码依赖。
现在一处改动有可能造成多处受影响,需要测试同事做大量回归测试,存在重复造轮子、项目编译时间长、开发效率降低等问题。目前还只是基于OC编码,Swift编码也是项目演进的一个重要方向,Swift和OC的相互调用还不是很方便,暗坑无数,需要先对项目进行组件化演进,以便项目向Swift和OC混编过渡。
二. 方案调研
参考业内的流行方案
1、基于 URL Router、ModuleMediator
代表:蘑菇街 Limboy
URL组件化架构图
优缺点:
1、URL Router方案,在使用时需要注册模块类,需要维护一个URL路由表,而且路由表一般保存服务端,拓展性和可维护性降低。
2、非常规对象无法直接参与本地组件间调度,模块之间用URL去完成调度,徒增复杂化。
3、项目中已有URL路由配置,URL Router 开发起来相对比较容易推进。
2、基于 Target-Action、Runtime、Category
Target-Action组件化架构图
优缺点:
1、将远程调用和本地调用做了拆分,而且由本地应用调用位远程应用调用提供服务,安全保证,对url中进行native前缀验证。
2、模块仅通过Action暴露可调用接口,必需去Model设计:只有调用方依赖Mediator,响应方依赖是没有必要的。
3、模块调用存在一定的硬编码问题,可以通过在模块中维护针对DDMediator的Category,每个category对应一个target,categroy中的方法对应Action场景。
4、业务发现的模式,存在业务调用的未知性,代码的可读性有所降低。
主要是基于Mediator模式和Target-Action模式,中间采用了runtime来完成调用。这套组件化方案将远程应用调用和本地应用调用做了拆分,而且是由本地应用调用为远程应用调用提供服务。
3、去model化
在组件和模块拆分之前,一定要明确一个概念,组件间调用是需要针对参数做去model化的。如果组件间调用不对参数做去model化的设计,就会导致业务形式上被组件化了,实质上依然没有被完全独立。其中的model并不是指架构中的model层,此处model化指的是模块间使用数据对象传值,去model化指的是模块间不使用数据对象传值。
三. 目标设定
组件化开发很重要的目的就是解耦合,提高开发效率。****解耦的主要方式:
1. 把依赖的代码先做成一个Pod库,然后转而依赖Pod库,“依赖下沉”。
2. 使用category的方式把依赖改成组合的方式。
3. 使用一个block、delegate(协议)、通知等把这部分职责抛出去。
4. 直接copy代码。避免业务干扰,也可以直接copy代码来用,谨慎使用。
1、架构设计:****层级架构+组件化架构
层级架构:只能上层对下层依赖,下层对上层不能有依赖,下层中不要包含上层业务逻辑,同层间的功能库或组件之间都应无依赖关系。
组件化层级及拆分
组件化架构:处于层级架构的最上层,也就是业务层。各业务组件之间通过中心路由转发调起其他组件的服务,并能独立编译、运行。
组件化架构图
采取这种结构混合的方式进行整体架构,与项目当前的架构比较接近,方便组件化的推进,符合公司业务需求。
2、目标效果
1. 第三方库管理、基础库、功能组件独立
保证所有第三方库管理、基础库、功能组件从主工程抽出,可独立编译,便于各业务模块的调用。提高代码复用率,提高业务开发效率。
2. 业务模块划分与拆解,独立编译
将业务按对应用功能或流程进行划分和拆解,能够单独进行编译、运行。保障各业务线开发相互独立,减少业务耦合,方便 QA 有针对性地测试。
3. CocoaPods 发布私有库,减少编译时间
将稳定模块打包成.a文件,做成私有库,并对每个组件用 Pods 进行管理和后续更新工作。加快编译速度,提高业务开发效率。
4. 通过模块库,提高代码重构或搭建新产品线的效率
通过对模块的组合配置或替换,能快速安全的替换旧的模块库,或快速搭建新的产品线,或将模块库扩展到其他产品线。
四. 制定计划
首先要明白,不是重写而是重构,是不改变软件功能的前提下,调整软件内部结构,优化项目管理,规范开发流程,提高开发效率,让代码更易读,更好维护。
原则:****稳定、精简、能够重用、持续进行,最大程度的让框架和业务分离,功能和业务分离。
1、在原项目的基础上调整?
1.目前的结构经过改进还能满足开发需要,项目扩展,没必要“推到重来”。
2.项目一直在迭代开发,没有足够的人力和时间,去重写项目,重写项目的周期太长,并不能随时中断。
3.没有单元测试,人员变动较大,熟悉业务的同事不多,重写的风险极大。
4.项目历史悠久,隐含配置较多,对旧系统的兼容配置,另外多是接管的代码,代码也没有注释,甚至注释不匹配的情况。
5.逻辑相互耦合,拔出萝卜带出泥,迁移模块时,关联太多。
6.有可能过度设计,导致重写计划迟迟无法完成,也不能优化工作用到项目中。
总之,在原项目的基础上逐步改进。
2、风险评估控制
开始前预估工期,预估重构风险。
修改时,拉去新分支,要做到代码隔离。
并详细做好重构的细节记录,如文件移动,文件改写等重要操作文档化,便于review,问题回归,测试回归。
每一个拆分出的模块及时添加文档,每一个模块或组件的建立者把模块内容、拆分目的、设计思路等基本信息记录一下,有什么坑或者注意点也可以文档化,是以后的长期项目维护成为可能。
3、组件化应分步推进
组件化是一个项目的整体调整,需要分阶段,分批次的逐步重构。
每个阶段大概的步骤:
1.明确该阶段工作的重点,预估工期,预估重构风险。
2.拉取新分支,隔离重构代码。
3.阶段重构调整之后,需要半数组员做代码review。
4.提交测试回归相关功能。
5.将重构代码整合到最近的发版中。
五. 实施计划
1、一期:梳理项目工程结构,抽离基础层和中间层
1. 项目工程定义
初步调整工程目录结构如下:
基础层和中间层
Pods: 仅Pods管理的第三方库。
DDManualThirdSDKs: Pods不能管理的第三方库,对第三方进行必要的封装,集中做成私有库的形式,再交给Pods管理。
DDDevelopLib: 自行封装的私有库,如系统类分类、数据处理类、通用UI组件、宏定义和常量定义等。做成私有库,交给Pods管理。
业务组件层
DDEngine: 后续会把模型规整到具体模块中,部分文件需要抽离到基础层或中间层,其他暂时保持不变,后续抽离封装网络库。
DDMIX_UI: 暂时保留,属于三期。
ddDemo: 将其中的第三方库、UILib、分类等,下沉到基础层或中间层外,暂时保持不变。
2. 整理三方库统一Pods管理
把项目中散落的第三方库都规整到Pods和DDManualThirdSDKs工程中,优先Pods管理。
散落的第三方库有:iflyMSC、libSunFlower、DDReader、YYText、MiaoZhen、NTalkerUIKitSDK、TalkingData、VoiceConvert、HPGrowingTextView(7年未维护了,局部使用,可以内化为自行代码)等。
3. 梳理DDDevelopLib私有库
自行封装的私有库,作为整个工程的基础库,不与具体的业务耦合,如系统类分类、数据处理类、通用UI组件、宏定义和常量定义等。做成私有库,交给Pods管理。如ddDemo中的VerifyUpdate、UILib、DDAppkit、Common、Categorys、Function、DDMonkey等。
4. Resources文件整理
初步整理Resources资源文件,后期会根据资源情况再统一整理图片资源,注意资源文件的加载路径的变化后,保证资源文件能正常加载。
5. SupportingFiles文件整理
创建真实目录SupportingFiles,存放系统支持文件如下:main.m、Info.plist、Prefix.pch、Debug.entitlements。
2、二期:****梳理中心路由,搭建组件开发的模板框架
1.梳理跳转中心
将跳转中心的路由按业务组件进行分组。
2.创建一个组件的模板框架
每个组件,都有一个组件服务管理类,来管理对外能提供的服务列表和使用说明。
组件内的架构设计,使用MVC、MVP还是MVVM,根据各自情况合理选择就好了。
3、三期:****将组件化,扩展到整个项目,将稳定组件用pod管理
将稳定的组件打包成.a文件,进行pod管理。
将组件化,扩展到整个项目。
六. 常见问题及对应思路
1、组件化工时不好评估,工作进度难以控制
每个阶段开始前都要做好技术调研和探讨,少走些弯路,降低返工率。制定较为详细的计划表,做好工作分配,处理好业务开发与技术改造工作之间的关系,保障组件化能连续的进行。
2、分层结构不明确和横向依赖
业务模块之间也存在一些不合理的横向依赖,没有进行一个合理的业务边界划分。这些原因导致我们在进行拆分工作时经常需要回过头来对已经拆出来的模块和组件重新进行整理和处理,重复劳动量很大。采用先易后难的步骤,尽量下沉逻辑,横向依赖,可以做一个下层抽取,变横向依赖为上下层依赖。
3、拆分粒度大小不好把控
如单品页组件中有单品主页,评论页,写评论等较大的模块,拆分力度不够细,单品页的信息会交织在一起。前期可以将业务关联性强的模块集合在一起,后期在逐个组件做细分拆解。
4、注意对代码调整的控制
拆分过程中,会有重复代码,甚至无用代码,在做调整时要谨慎,尤其是公共类,保障逻辑不变的情况下酌情调整,设置不做修改,并做好记录,方便review,请测试回归。杜绝随意改动,也不回归测试。