随着业务的不断发展壮大,App 端所承担的功能也越来越重,特别是代码几易其主之后开始变得杂乱无章,牵一发而动全局的事情时常发生。为了应对团队壮大之后的开发模式,我们必须要对业务进行隔离,同时沉淀出通用组件,完善移动开发的基础设施。
1. 痛点
模块化之前,我们主要面临以下痛点:
业务边界不清晰
通用代码与业务代码耦合
代码、资源文件大量重复
常量满天飞
其中业务边界不清晰是最大的痛点,最直接的表现就是处处有雷,经常会引入新的 Bug,而且很多 Bug 往往不能从根本上解决,代码维护成本居高不下。
2. 重构原则
模块化并不能一蹴而就,我们在重构的同时也在做新需求,每次看到那一坨旧代码心中就会有无数只”*”奔腾而过,干脆重写的无奈之情难以抑制,结果在红牛的日夜陪伴下写出来的新代码虽然看上去“漂亮”,但是实际上问题更多,得不偿失。吃过几次苦头之后,我们总结出了重构的三项基本原则:
2.1 渐进式重构
如果一段代码已经比较稳定,可以从中抽取一部分功能重写,不要一上来就全部推翻重写,可以慢慢淘汰掉老代码。
2.2 iOS / Android 互相参考
业务代码总是惊人的相似,两端互相参考的过程中,不但可以 Review 代码,还能加深对业务的理解,可谓一举两得。
实践证明,如果人手紧张,项目早期可以只让一端的开发人员跟需求,另一端直接“翻译代码”,甚至一个人写两端代码。
2.3 理清业务再动手
App 作为业务链的末端,由于角色所限,开发人员对业务的理解比后端要浅,所谓欲速则不达,重构不能急,理清楚业务逻辑之后再动手。(可以找熟悉业务的同学聊一下 — PD、后端、测试)
3 模块化过程
所谓模块化,是一个分而治之的过程,概念类似于 SOA,首先进行垂直拆分,过程中必然会催生出业务共享的 Common 模块,而 Common 又可以继续水平拆分,逐渐变薄,直到 Common 消失。
刚开始不需要完美的目标,简单粗暴一点,后续再逐渐改善。
3.1 抽取 Common
Common 层服务于所有的上层业务,是通用层,不允许引用业务层代码。
首先把 Common 层用到的 Business 层代码下放到各个业务
然后把多个 Business 之间共用的代码提取到 Common 层
资源文件的处理方式与代码一致
Common 层作为权宜之计,它的命运是向死而生,最终会诞生出许多功能独立的基础模块。而这个过程是漫长的,我们只能在业务隔离的同时,不断丰富 Common 模块,然后在某个节点将其再拆分成一个一个独立模块。
代码也逃不出分久必合、合久必分的的宿命。
3.2 业务隔离
业务模块之间不能互相依赖,只能单向依赖 common。
业务之间存在两种耦合关系:
页面耦合
功能耦合
要做到彻底隔离就必须打破这两种耦合关系:
页面解耦 - 跳转协议
功能解耦 - 模块间 RPC
3.2.1 统一跳转协议
页面解耦可以借鉴 Web 的设计原理,给业务模块中对外的页面定义一个 URI,然后页面之间通过 URI 跳转。
举个栗子,A、B 两个页面分属于不同的业务模块,在页面未解耦之前,A 如果要跳转到 B,必须要依赖 B 的模块,那么跳转代码会写成如下形式:
Android
1.
Intent intent =
new
Intent(getContext(), BbbActivity.
class
);
2.
intent.putParcelable(BbbActivity.EXTRA_MESSAGE, message);
3.
startActivity(intent);
iOS
1.
BbbViewController *bbbVC = [[BbbViewController alloc] init];
2.
bbbVC.messageModel = messageModel;
3.
[self.navigationController pushViewController:bbbVC animated:YES];
如果 A、B 之间还需要传递数据,就要共享常量、Model,耦合继续加重。
如果我们为 B 页面定义一个 URI - wsc://home/bbb
,然后把共享的 messageModel
拍平序列化成 Json 串,那么 A 只需要拼装一个符合 B 页面 scheme 的跳转协议就可以了。
1.
wsc:
//home/bbb?message={ "name":"John", "age":31, "city":"New York" }
URL Router
有很多种实现方式,网上资料也是多如牛毛,这里只提供一种思路。
Android 实现方式
1. 在 AndroidManifest.xml 文件中定义 URI
01.
<activity
02.
android:name=
".ui.BbbActivity"
03.
<intent-filter>
04.
<category android:name=
"android.intent.category.DEFAULT"
/>
05.
<action android:name=
"android.intent.action.VIEW"
/>
06.
<data
07.
android:host=
"bbb"
08.
android:path=
"/home"
09.
android:scheme=
"wsc"
/>
10.
</intent-filter>
11.
</activity>
2. 封装跳转 Intent
1.
final
Uri uri =
new
Uri.Builder().authority(
"wsc"
).path(
"home/bbb"
)
2.
.appendQueryParameter(
"message"
,
new
Gson().toJson(messageModel)).build();
3.
final
Intent intent =
new
Intent(Intent.ACTION_VIEW);
4.
intent.setData(uri);
5.
startActivity(intent);
3. 步骤 2 代码进一步封装
1.
ZanURLRouter.from(getContext())
2.
.withAction(Intent.ACTION_VIEW)
3.
.withUri(
"wsc://home/bbb"
)
4.
.withParcelableExtra(
"message"
, messageModel)
5.
.navigate();
iOS实现方式
1. 通过 plist 文件保存 URI 到 Controller class 的映射
2. 封装一个根据 URI 跳转到 Controller 的 SDK
3. 页面跳转
1.
[ZanURLRouter routeURL:@
"wsc://home/bbb"
];
注意事项
两端协议要保持一致
需要通过工程手段保证页面 URI 唯一
3.2.2 模块间 RPC
「业务 A 」与「Remote: 服务端」之间通过 HTTP 或者其他协议进行远程调用,「Remote: 服务端」是服务提供者,「业务 A 」是服务消费者。
对于「业务 A 」来说,「Local: 业务 B」也是服务提供者,但是两者不存在依赖关系,所以只能通过协议来通信。
iOS 通过
protocol
提供服务,利用 BeeHive 做“服务治理”。Android 通过
interface
提供服务,然后我们模仿 Retrofit 做了一个“服务治理”框架 - ServiceRouter,它的优势在于可以只在业务提供方的 module 中定义interface
,解耦更彻底。
4 代码管理
如果被隔离的业务模块仍然在一个 Project 中,就无法从“物理”上彻底隔绝代码间的相互引用,我们需要从工程上保证业务之间互相独立。
4.1 代码结构
Android (Module) | iOS (Project) |
---|---|
4.2 独立发版
每一个 subproject 可以独立发版,然后通过坐标依赖组装成 App,以 Android 为例:
4.3 独立 Repo
现在还没有找到一个很好的代码组织形式,所以我们的观点是:
在团队规模不大的时候,一个人要 Cover 多个子工程,所以没有必要独立 Repo,当一个 Repo 需要多个人 Cover 时可以考虑独立 Repo。
规模 | 是否独立 Repo |
---|---|
Developer 1 : N projects | 否 |
Project 1 : N developers | 是 |
当解耦方案确定之后,模块化其实就是一个体力活,返工重做便成了家常便饭,所以我们觉得比较好的方式应该是专人负责、一气呵成。
5 诗和远方
通过移动配置中心动态下发跳转协议
抽取移动端业务通用 UI 组件库
主工程可选择性依赖业务模块