如何量化模块切分的合理性?
除了自嘲为“玄学”之外,有没有量化的可能性?今天我们来尝试一下。
相加的关系
显然我们可以把业务逻辑分解成如下的多个Git仓库

这些一次性的Git仓库会相加到一起,就构成了完整的业务逻辑。指标需要度量的是这些 Git 仓库的维护者之间的沟通量。如果切分不合理,就会导致经常需要把一堆人拉一起开会的结果。下面这个指标是希望度量的就是“沟通量”。
指标:接口改动占比
我们把Git仓库内的文件分为两种类型,负责接口的文件和负责实现的文件。如下图所示

理想的情况下,应该尽可能少的改接口,而是主要去改实现,这样才能减少跨Git仓库的人员沟通。如果一个新需求,需要同时改动N个Git仓库。但是只要不需要改动接口(包括用 Map
这样形式搞的隐式接口),仍然是理想的情况。虽然产品经理需要和多个团队沟通每个部分的需求是什么,但是开发团队之间的沟通仍然可以比较少。要每个新需求都只改动一个Git仓库由一个团队负责,这是不太现实的:
- 新商业玩法往往是破坏性的。我们不要去做提前预测
- 需求大小是任意的,产品经理分工也是有随机性的。总是有办法把一个需求弄大到全公司只做这么一个需求的地步。
接口完全不修改,开发人员之间完全不沟通也是不可能的。我们要关注的是目前的业务逻辑拆分是不是合理,多个 Git 仓库之间的接口如果需要频繁调整,那么说明 Git 仓库是不是分得过多了,或者边界不是最佳的。要根据新的输入,不断去审视过去做过的拆分决策。而 “接口改动” / “实现改动” 比率可以量化目前业务逻辑拆分是否让每个 Git 仓库有多少 Autonomy。这个值越小,说明仅改动实现情况占比越高,自主性做得就越好。
为了数据统计比较稳定:
- 仅做到文件级别的区别。一个文件要么属于接口,要么属于实现。一般通过技术手段都可以做到这样的隔离。
- 一天无论改了多少次,改了多少个文件都记为“1次”改动。这样避免了分多次提交,或者文件数量多寡引起的数据波动。
极端情况下,我们可以不分 Git 仓库,或者只有两个 Git 仓库,从而让 “接口改动” / “实现改动” 比率比较好看。这个也说明了分 Git 仓库的成本。把业务逻辑拆得越碎,必然会导致跨团队的沟通会上升。Git 仓库不是分得越多就越好,而是满足了团队的并发数就可以了。
这个指标的另外一个问题是日常性的文案修改会导致实现改动非常多。所以我们要以“Consistency”维度的指标去平衡。假设我们已经有了一种统一的文案配置机制。那么需要有一个“文案配置机制”接入率的指标。这样就可以避免日常性的例行修改破坏这个指标的真实性。
《A Philosophy of Software Design》 很重要的一个观点就是 “Modules should be deep”,这样的隐喻让人们把注意力放在了静态的结构上。其实作者的本意是接口如果比实现要小很多的话,接口被修改相对于实现被修改的概率也就小了很多。这样我们大部分时候就可以只改实现,而不改接口。“业务逻辑拆分”其成本和收益都要在接下来做新需求的过程中体现,抽离了业务变更的时间轴,静态的代码结构无法度量其好坏。
相乘的关系
然后我们会意识到,有一些重复的模式。 比如说这后台的表单都长得差不多啊,我们把表单的校验和错误回显封装个UI组件吧。 然后所有的后台表单都必须复用这个UI组件。 这样为了保障实现的一致性,就引入了相乘的关系

我们可以完全没有相乘的组合关系,而完全依赖相加来组合。但是相乘的组合关系通过只需要维护一份,使得一致性可以更好。出发点应该是先挖掘需求内在的一致性,然后达成共识之后把实现统一成一份。而不是反过来,在不控制约束需求的情况下,开发者一厢情愿地搞复用。
为了防御常见的两个设计错误:
- 过度抽象:强行把一堆不相关的东西拧巴到一起。
- 过早抽象:没想清楚的情况下,根据局部的一两处重复就抽出一个可复用的东西。
我们需要额外的指标来约束“相乘组合关系”的Git仓库。
指标:必要参数占比
我们把可复用Git仓库对外提供的函数参数分为两类,必要参数和非必要参数。非必要参数的计算口径是只有 10% 的调用方传递了的参数。

指标为必要参数的数量占总参数数量的占比。为了抽取出可复用的模块是不是做得过度了?是不是把一些小众场景的具体业务也以额外参数的方式 pull down 到可复用Git仓库里了。 每一个额外参数都增加了调用者的负担,是额外的学习和维护成本。
指标:阻断率
当我们使用了 Java 这样的编程语言的时候,Java 会阻断你在代码中使用汇编语言直接操纵 CPU。这是比较典型的“阻断”。 阻断是最强有力地保障一致性的措施。

如果使用了 C++ 这样的编程语言,很有可能 Git 仓库之间对什么是一个 string 都没有共识。 这样就必须要在一定范围内(比如某个项目,某个部门),强制要求所有的 Git 仓库都接入同样的 string 库,从而保证互操作的低摩擦。
类似的,一个彼此互相RPC调用的分布式应用,各个进程都用不同的 RPC 协议,用不同的 RPC 实现库来互相通信会导致很多问题。 例如,A调用B又调用C,一旦调用失败,层层加码地去重试,就可能导致最底层模块被反复重试,最终被击垮。 解决办法是需要分布式调用链上的所有进程都遵循同样的重试规则。 如果没有办法“阻断”手撸RPC的实现,看见一个http url,就直接随意找个 http 库去调用,那就很难保证重试规则的一致性。
阻断率指所有可接入的地方,有多少处上了强制检查,确保了违规行为会被阻断。
真的一点提前设计都不需要吗?
是的,需求是不可预测的,不要基于臆想的需求提前设计。
不是的,我们需要在开工之前先取得共识。共识应该包含两部分内容:
- 我们总是要做出一个“如何分工”的决定,才能开始工作。没有完美的决定,Autonomy/Consistency/Feedback 三个方面总是有被取舍掉的方面。但是我们要对“取舍了什么”达成共识。
- 敏捷开发需要我们持续地调整,持续地去响应变化。“如何保持敏捷”同样需要计划,需要达成共识。
在实践中,我们经常盲目地采取某种架构风格,比如说微服务架构,但是并不能明确地说出这个做法是“取舍了Autonomy/Consistency/Feedback中的哪些”。既不知道是为了获得什么收益,也不知道背后的代价是什么。 然后一条路走到黑,没有反思的节点,没有项目的复盘。如果我们不承认做 Big Upfront Design 是可能的,那至少要有日常反思的计划。 什么 症状 应该触发大家去调整业务逻辑的拆分?这些症状能不能被 量化 ?能不能排上议程?
最后,我们不需要提前设计,但是要基于共识进行分工。这就是《业务逻辑拆分模式》主旨所在。