软件设计

【模块化 #3】Common 模块为何会变成垃圾桶

大多数进行模块化的团队,都会在某个阶段创建名为 Common、Core 或 Utils 的模块。

4 分钟阅读
【模块化 #3】Common 模块为何会变成垃圾桶 封面图

大多数进行模块化的团队,都会在某个阶段创建名为 Common、Core 或 Utils 的模块。

当“很多地方都要用,先放这里”的判断不断积累后,这个模块不知不觉成了项目中最大、变化最频繁、所有人都依赖的模块。

先说结论:把模块命名为 Common,等于承认它没有内聚性。“公共”不能成为组织代码的标准。

本文将总结 Common 模块为何会让模块化倒退,以及如何通过分层结构(垂直拆分)和功能结构(水平拆分)来预防。


Common 模块为什么危险?

问题会分三个阶段发展。

第一阶段:变成垃圾桶。“不知道该放在哪里的代码”全部进入 Common。日期格式化工具旁边放着网络客户端,旁边又放着自定义按钮。它们彼此毫无关系。

第二阶段:全员依赖。所有功能模块都 import Common。此时 Common 位于依赖图最下游,也就是本应最稳定的位置。

第三阶段:整体重新构建。但 Common 是垃圾桶,所以它变化最频繁。上一篇文章的稳定依赖原则被彻底颠倒了。只改一个按钮颜色,所有模块都要重新构建。

当被依赖最多的模块变成变化最频繁的模块时,模块边界就形同虚设。


分层结构:垂直拆分

预防措施的第一条轴线,是按职责进行垂直拆分。常见的 3~4 层结构如下。

层级 内容 示例
Feature 页面/功能模块 首页、搜索、订单、设置
Domain 业务规则、模型、用例 订单策略、会员模型
Core 特定技术的薄封装 网络、存储、日志
Shared 真正通用的工具 日期格式化工具、字符串扩展

规则只有一条:依赖只能从上向下流动。

Feature 可以使用 Domain 和 Core,但 Core 不能知道 Feature。Feature 之间也不直接相互引用,而由上层应用 target 负责组装。

依赖只能向下流动;分层规则全部都在这张图中。
依赖只能向下流动;分层规则全部都在这张图中。

这里要注意它与 Common 的区别。Core 不是一个巨大的模块,而是按技术拆分出的多个模块。

  • 例如 CoreNetwork、CoreStorage 和 CoreLogging,分别都是独立模块。
  • 如果搜索功能只使用日志,就只 import CoreLogging。

这样即使修复网络代码,只使用日志的模块也不会重新构建。


功能结构:水平拆分

第二条轴线是按领域进行水平拆分。即使在同一层中,首页、搜索和订单也应该是不同的模块。

判断标准是第一篇文章中提到的问题:“这些代码是否因相同原因发生变化?”

  • 如果搜索策略变化时不需要修改订单代码,它们就是不同的模块。
  • 如果订单页面和订单用例总是同时变化,继续进行垂直拆分可能就过度了。

将垂直(分层)和水平(功能)叠加后,就形成了网格。实际模块会占据其中一个格子,例如“订单功能的 Domain”或“搜索功能的 Feature”。


如果 Common 已经变得过于庞大?

不必一次性摧毁现有的 Common。实践验证过的顺序如下。

  1. 禁止新增:先建立从今天起不再向 Common 添加新代码的规则。
  2. 调查使用位置:从变化最频繁的代码开始,确认它实际在哪里使用。
  3. 找到归属:只被一个功能使用,就移到该功能模块;如果是特定技术的封装,就移到 Core 系列模块。
  4. 重命名剩余内容:只将最后留下的真正通用代码,用 Shared 之类更精确的名称隔离。

这项工作需要几个月,但可以清楚地看到重新构建范围在每个阶段逐步缩小。

整理的起点,是不再加入新内容。
整理的起点,是不再加入新内容。

什么时候需要这种结构,什么时候又会过度?

  • 开发者达到 4~5 人以上、功能领域达到 3 个以上时:非常值得建立分层规则。
  • 如果是 1~2 人的项目:从 Feature/Shared 两层开始即可。先画完整网格会过度设计。
  • 无论规模大小:建议从一开始就遵守“不创建名为 Common 或 Utils 的模块”这一规则。

面试时可以这样问

Q. 公共模块(Common)变大后会产生什么问题?应该如何设计?

所有人都依赖的模块变成变化最频繁的模块,因此即使是小修改,也需要整体重新构建并进行广泛的影响分析。这是稳定依赖原则被颠倒的状态。应按职责细分为网络、存储、日志等模块,只将真正通用的代码隔离在最小的 Shared 模块中。

Q. 请说明将模块结构划分为分层时的规则。

设置 Feature、Domain、Core 等按职责划分的层级,并只允许依赖从上向下单向流动。同层模块之间不能直接相互引用,而应在上层组装层连接。遵守这条规则后,变更影响范围就始终被限制在上方。


以上就是模块化原理三部曲。掌握边界、依赖方向和分层结构,就具备了所需的概念工具。

从下一篇开始,我们将把这些原则直接应用到 iOS 项目中,先介绍如何使用 Swift Package 实际拆分模块。