模块化实践 Part1
模块化拆分是近些年来客户端开发面临的常见而又头疼的问题,项目的代码量在随着需求不断的增加,提出重构也会伴随着新的业务需求并行。
模块化思路在社区内已经有很多非常优秀的文章指出了思路与例子,大方向上相信大家都已了解。但是由于各自产品业务与项目代码现状不同,在谈及如何具体操作拆分工作时,一般都是一笔带过了。对于开发者而言,实施的重点就是在具体的拆分细节上,所以接下来聊聊如何站在优秀理论上开展具体的工作。(错误的地方欢迎指正交流👏)
热身一下
如果之前没有看过相关文章的话,强烈推荐看完👇的文章继续食用
看完之后,我们继续来填坑…
上面的文章中文章中一些关键点: Pin 工程
, API 化
, 跨模块通信
。 面对自己家的 project,怎么实施呢?这里从最头疼的代码组织划分开始说起。
Pin工程
提及 project 的代码划分,我们第一时间想到的就是以 module 来划分功能模块,之前有很多文章也是这样的思路,然后配合着动态变更 module 为 application 模式来实现功能模块作为拆分 app,方便模块的开发与调试。
但是这种思路个人认为有些理想化,对于结构划分不是很明确的项目而言,现状可能是代码大量堆积在主工程,功能虽然以包名划分,却存在严重的耦合调用,群魔乱舞。对于这种情况,拆 module 可不是一件容易的事情:module 的拆分改变了代码隔离的方式,很可能出现拆分中发现大量的代码都需要改动,从而不了了之。
那么我们该怎么继续呢?先来看看痛点有哪些:
- 拆 module 怎么开始
- 现有的包名行不成代码划分,代码组织分散,调用点众多,不太直观评估业务体量与之间的耦合
- 耦合的不光有代码,还有布局与资源,如果项目所有的图片资源放在 common 或者 base module 中,那简直是相当难受了(
lint unusedRes
在这种场景下没用) - 对于一个代码混杂程度很高的项目,拆分整理基础库的可行性,那么多的代码,谁该放谁不该放?
- module 的拆分工作会在迭代中与版本并行,开发同学和测试同学都会面临不小的压力
微信团队提出的 pin 粒度工程划分告别了以分包划分代码的假边界,解决了从物理层面制定代码边界的问题,同时我们上面的痛点也能得到改善。pin 工程对于模块划分不明确,耦合严重的项目给予一次最大的重生机会:先苟住,再抽取。具体的做法是:
首先在已有的主工程或者子 module 中划分 pin 工程,因为耦合性很重的原因,可以在配置中先不断开源码的相互依赖,对于被改造的主工程或者 module 而言,代码全部都被 include 的,所以就算是因为开发周期的原因,一个 pin 工程没有提取完成,对编译打包来说是完全不影响的。
- 先在主工程或者 module 内部划分 pin 工程, 没法划分或者动不了的代码就留在 main/ 下面等着二次改造
- pin 工程划分包含了代码与布局资源,所以拆分后将 pin 工程组合升级为 module 会非常便捷
- pin 工程划分先移动代码,耦合调用的可以先不用管,先把业务主体代码抽取出来。后续对 pin 工程粒度找出耦合、不符合规范的调用会容易很多
- 代码划分中可以使用 stuido 的 refator/move 解决涉及代码的包名变更问题(有使用 DataBinding 的项目建议要使用这两个来进行,布局文件中的包名也会随之修改)
- 主体业务完成迁移,剩余的代码划分也会水到渠成
代码初步划分迁移完成之后,接下来会有一个头疼问题是涉及到的布局资源处理?一个一个文件的去找布局文件做迁移移动未免有些效率低下。我们可以用代码来检测代码中包含的布局文件,原理也很简单,就是匹配出 layout 文件名,然后做对应移动就好。这里提供了一个 python 脚本 refactor_pins_layout_file.py
来实现。 使用也很简单,在对应的 module 中运行脚本附带 pin_name
参数即可。
处理完布局文件,还有 drawable 的资源。但是 drawable 的资源比较特殊,可能会有其它的模块会共用,所以我们可能不能直接像 layout 文件用脚本处理。但是脚本依然可以用来检测 pin 工程中所使用到的 drawable,生成一个清单帮助我们迁移。
API 化
Pin 工程拆分可以更细粒度的管理模块业务,但是模块间的关联性是我们无法避免的,那么模块间的依赖组织该怎样处理呢?
微信团队提出的 api 化是非常优秀的思路与设计,解决了模块间调用依赖如何组织的问题。在寻常的拆分中,我们一般会将 Module A 与 Module B 中涉相互引用的代码下沉到一个 A 与 B 都能引用的地方,暂且称之为 BaseAB,当有新的业务 C 中代码需要被 A,B 使用时,BaseAB 会变成 BaseABC,如此一来,随着业务发展,BaseABC… 会慢慢成为多人维护,充斥各种被下沉的代码。除非团队内部有非常严格的 review,业务体量上来了,随意的下沉依赖代码,BaseABC… 变成三不管区域基本上是跑不掉的。
api 化的思路就是将这个依赖的代码中心给去中心化,各自 module 提供出的可以被别的 module 依赖使用的代码,将转化为自己来维护,提供最小化的接口出去,制定模块间统一的边界。跟随着微信团队的思路,这里写了一个 demo 来小试牛刀 APIModuleDemo。
怎么动态的生成一个 api module 呢?微信在文章中提到说是将要暴露的代码重命名为 .api , 然后将 .api 为后缀的文件 copy 出去到一个全新的 module ok 了,那这个时机应该是什么时候呢? 在 setting.gradle 中编写 copy api module 相关逻辑即可。
我在具体实现中遇到一个问题是,虽然可以更改 Android Studio 的 FileType 来让 IDE 识别 java 或者 kt 文件,引用时也是正常。但是在 build 项目时,实际上是编译器处理不了这种特殊后缀的文件,然后 module 会找不到需要暴露出去的 api 代码,从而编译报错。所以对于业务 moudle 来说,还是需要自己 include_api(‘:${self}’)
来保证正常编译。(后续会继续寻求更好的方法)
跨模块通信
跨模块通信具体实现也就是经常提及的 Router 组件,提及 Router 其实是项目中常见的通用组件了,最早的场景是做页面间跳转分发,后期慢慢衍生到现在的跨模块调用,如果项目原来就有 Router 组件,那么就继续我们的拆分工作。没有的话可以按项目业务需求评估一下引入:
- ARouter
- WMRouter
- Andromeda
- …
在这里就不横向对比各大 Router 了,各个项目都有良好的 README 与 Wiki,可以自己写下 demo 对比性能和用法,选取适合项目场景的方案或者参考后自研都行。
这里有个小建议是,如果是考虑直接引入使用,请务必阅读熟悉源码,其一是为了更好的评估对比,选用适合项目的;其二是开源项目的维护可能不会那么的及时,熟悉后万一项目中出现奇怪的问题不会那么慌,解决起来也快(毕竟是基础组件,稳字当头=。=)
回到我们的拆分,上面 pin 工程拆分的结果能把臃肿的主工程或者业务 module 代码初步的在自己内部拆分开来,但是业务间直接引用或者调用的耦合依然没有解决。 所以 api 化与 router 化基本上可以算是同一时间段的工作:
- 评估 pin 或者 module 依赖(比起主工程或者 module ,更细的粒度后应该会更直观)
- 抽取 api,同时将依赖调用转化为接口注册,router 调用
- 提取页面间跳转耦合,转化为 router 调用
一些工具
- 代码移动
- Android Stuido Refactor/Remove
- Nofu: 文件移动 plugin
- 布局, 图片资源迁移工具
- API 化小插件: 请戳 APIModuleDemo
- 依赖切换小插件: 这是美团在文章分享中所提及的工具,当拆分成型了之后,免不了常常切换 aar 与 源码依赖的开发场景。这里我写了个小插件实现了这个功能 DepSwitchPlugin
小结
模块拆分工作是非常大的重构,重构免不了冲突(不仅仅指代码冲突),冲突免不了团队间的紧密协作。所以对于模块化拆分,可能实际上不是技术的瓶颈限制,可能更多是组内沟通,协作的限制。从出发点来讲,模块化拆分并不是为了追赶新技术,而是为了解决项目大了之后开发人员间协作效率的问题,这是软件开发自然迭代的结果,顺带的可能还会提高项目质量。所以在拆分工作开始前,内部沟通培训达成统一,评估项目代码划分模块、指定边界,制定拆分计划(测试),分支管理等等细节,准备做的越周全,在具体实践中可能会少掉很多的坑,提升开发体验。
Copyright © 2020 qiugng.github.io - 保留所有版权,未经允许禁止转载