模块化实践 Part1

模块化拆分是近些年来客户端开发面临的常见而又头疼的问题,项目的代码量在随着需求不断的增加,提出重构也会伴随着新的业务需求并行。

模块化思路在社区内已经有很多非常优秀的文章指出了思路与例子,大方向上相信大家都已了解。但是由于各自产品业务与项目代码现状不同,在谈及如何具体操作拆分工作时,一般都是一笔带过了。对于开发者而言,实施的重点就是在具体的拆分细节上,所以接下来聊聊如何站在优秀理论上开展具体的工作。(错误的地方欢迎指正交流👏)

热身一下

如果之前没有看过相关文章的话,强烈推荐看完👇的文章继续食用

看完之后,我们继续来填坑…

上面的文章中文章中一些关键点: Pin 工程, API 化, 跨模块通信。 面对自己家的 project,怎么实施呢?这里从最头疼的代码组织划分开始说起。

Pin工程

提及 project 的代码划分,我们第一时间想到的就是以 module 来划分功能模块,之前有很多文章也是这样的思路,然后配合着动态变更 module 为 application 模式来实现功能模块作为拆分 app,方便模块的开发与调试。

但是这种思路个人认为有些理想化,对于结构划分不是很明确的项目而言,现状可能是代码大量堆积在主工程,功能虽然以包名划分,却存在严重的耦合调用,群魔乱舞。对于这种情况,拆 module 可不是一件容易的事情:module 的拆分改变了代码隔离的方式,很可能出现拆分中发现大量的代码都需要改动,从而不了了之。

那么我们该怎么继续呢?先来看看痛点有哪些:

微信团队提出的 pin 粒度工程划分告别了以分包划分代码的假边界,解决了从物理层面制定代码边界的问题,同时我们上面的痛点也能得到改善。pin 工程对于模块划分不明确,耦合严重的项目给予一次最大的重生机会:先苟住,再抽取。具体的做法是:

首先在已有的主工程或者子 module 中划分 pin 工程,因为耦合性很重的原因,可以在配置中先不断开源码的相互依赖,对于被改造的主工程或者 module 而言,代码全部都被 include 的,所以就算是因为开发周期的原因,一个 pin 工程没有提取完成,对编译打包来说是完全不影响的。

代码初步划分迁移完成之后,接下来会有一个头疼问题是涉及到的布局资源处理?一个一个文件的去找布局文件做迁移移动未免有些效率低下。我们可以用代码来检测代码中包含的布局文件,原理也很简单,就是匹配出 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 组件,那么就继续我们的拆分工作。没有的话可以按项目业务需求评估一下引入:

在这里就不横向对比各大 Router 了,各个项目都有良好的 README 与 Wiki,可以自己写下 demo 对比性能和用法,选取适合项目场景的方案或者参考后自研都行。

这里有个小建议是,如果是考虑直接引入使用,请务必阅读熟悉源码,其一是为了更好的评估对比,选用适合项目的;其二是开源项目的维护可能不会那么的及时,熟悉后万一项目中出现奇怪的问题不会那么慌,解决起来也快(毕竟是基础组件,稳字当头=。=)

回到我们的拆分,上面 pin 工程拆分的结果能把臃肿的主工程或者业务 module 代码初步的在自己内部拆分开来,但是业务间直接引用或者调用的耦合依然没有解决。 所以 api 化与 router 化基本上可以算是同一时间段的工作:

一些工具

小结

模块拆分工作是非常大的重构,重构免不了冲突(不仅仅指代码冲突),冲突免不了团队间的紧密协作。所以对于模块化拆分,可能实际上不是技术的瓶颈限制,可能更多是组内沟通,协作的限制。从出发点来讲,模块化拆分并不是为了追赶新技术,而是为了解决项目大了之后开发人员间协作效率的问题,这是软件开发自然迭代的结果,顺带的可能还会提高项目质量。所以在拆分工作开始前,内部沟通培训达成统一,评估项目代码划分模块、指定边界,制定拆分计划(测试),分支管理等等细节,准备做的越周全,在具体实践中可能会少掉很多的坑,提升开发体验。