2、模块化设计理念:高内聚低耦合原则、模块接口定义、模块粒度划分

模块化设计,说白了就是“分而治之”。把一个复杂系统拆成若干个独立模块。每个模块负责自己的事,彼此之间少打交道。

听起来简单,对吧?但我在项目中见过太多次拆得乱七八糟的案例。有的模块内聚性极差,一个函数干三件事。有的模块之间耦合像麻花一样拧在一起,改一个地方要联动七八个文件。

这一章,我聊聊我的经验。高内聚低耦合、接口定义、粒度划分。这三个东西是模块化的基石。

2.1 高内聚低耦合原则

先记住一句话:模块内部尽量紧密,模块之间尽量松散。

  • 高内聚:一个模块封装一组高度相关的职责。比如“订单模块”只负责订单创建、查询、状态变更;别把“支付”逻辑塞进去。
  • 低耦合:模块间只通过明确接口通信。不要偷偷摸摸共享全局变量、不要直接访问另一个模块的内部数据。

我个人习惯用一个简单表格判断模块的内聚程度:

内聚类型 描述 是否推荐
功能内聚 模块内所有元素共同完成一个单一功能 ✅ 最佳
顺序内聚 一个元素输出是另一个输入 ✅ 可接受
通信内聚 共享相同数据 ⚠️ 谨慎
逻辑内聚 把相似但无关的功能放在一起 ❌ 避免
偶然内聚 完全无关的代码凑一锅 ❌ 强烈避免

耦合也一样,我通常把它看作七层阶梯:

耦合类型 特征 后果
数据耦合 只通过参数传递简单数据 最理想,改动影响小
标记耦合 传递整个数据结构但只用一小部分 容易被误导,过度依赖
控制耦合 传递标志位控制对方逻辑 模块不再单纯,职责模糊
外部耦合 共享外部资源(如全局变量、文件) 不好调试,并发容易出问题
公共耦合 多个模块读写同一全局数据区 改一处可能炸一片
内容耦合 一个模块直接使用另一个模块的内部数据 极难维护,基本等于没模块化

我在项目中遇到过一种经典的反模式:把日志和告警写在同一个函数里。日志是日志,告警是告警,两个完全不同的事,硬塞到一起。后来业务要求告警分级,日志又要换格式,改一个函数影响两条链路。拆开之后,清爽多了。

核心原则:一个模块只改变一个原因。这就是单一职责在模块维度的体现。

我的检查清单:

  • 模块内部功能是否围绕同一个概念?
  • 改动一个功能,会不会影响本模块其他功能?
  • 另一个模块是否必须依赖本模块的内部实现?

但凡有一个回答是“是”,就该重构了。

2.2 模块接口定义

接口是模块间的契约。定好接口,你改你里面,我改我里面,互不干扰。

我建议接口定义要回答三个问题:

  1. 入参是什么? 类型、范围、是否可为空。
  2. 出参是什么? 成功返回什么,失败返回什么。
  3. 边界条件怎么处理? 超时、重试、并发安全?

举个例子,一个“任务调度”模块的接口定义:

// 调度模块接口声明(伪代码)
// 入参:任务ID, 调度时间戳(UTC, 毫秒)
// 出参:调度结果枚举 { SUCCESS, TASK_NOT_FOUND, SLOT_EXHAUSTED, INTERNAL_ERROR }
Status schedule_task(
    TaskId id,
    uint64_t scheduled_at_ms
);

你看,只有这三个参数,没有泄露任何内部细节。调度模块内部用数据库还是用队列,外部完全不关心。

我曾经在一个项目中接手过一套接口:入参传了一个JSON对象,里面包含了十几个字段。调用方根本不知道该传哪些。出参返回一个字符串“ok”或“fail”,失败原因无从得知。嗯,那段时间改bug改到怀疑人生。

接口设计三大坑:

  • 坑1:参数太多、类型模糊。改用明确的结构体或强类型参数。
  • 坑2:返回值过于简单,无法区分错误。至少返回错误码+错误信息。
  • 坑3:接口不稳定,频繁变更。用版本号管理,比如 /v1/tasks、/v2/tasks。

接口定义还有一个容易被忽略的点:接口的稳定性。接口一旦发布给外部模块使用,就不应该随意删改。我习惯把接口分成“公开接口”和“内部接口”,公开接口必须经过评审,改动需要版本升级。

如果你用接口描述语言(IDL)比如 Protobuf、Thrift、OpenAPI,记得在 proto 文件或 yaml 文件里写清楚每个字段的含义。我曾经见过一个 proto 文件,字段名叫 extra,备注是空的。整整查了三天才搞明白它原来是做容灾备份用的。

2.3 模块粒度划分

最后一个问题:一个模块应该有多大?

没有标准答案。但我可以分享我的经验法则:

  • 太细:每个模块只有几个函数,接口数量爆炸,模块间调用链比头发还多。管理成本高,调用开销大。
  • 太粗:模块内部塞了几百个功能,没人说得清它到底做了什么。改一个地方,影响面难以评估。

我常用的划分方法叫“职责驱动划分”。你想想看,一个系统里有哪些自然边界?

划分依据 例子
业务领域 用户模块、订单模块、支付模块、库存模块
技术层次 数据访问层、业务逻辑层、展示层
非功能需求 日志模块、监控模块、配置模块
变化频率 核心稳定模块 + 易变业务模块

我个人建议优先按业务领域来切。因为技术层次的划分容易让一个模块同时服务多个业务,最后耦合越来越深。

举个例子,你要做一个“内容管理系统”。按领域切:

// 模块清单(按业务领域)
- 文章模块
- 用户模块  
- 标签模块
- 评论模块
- 统计模块

每个模块内部再按技术层拆成子模块。这样拆下去,粒度就很自然了。

粒度检查标准:一个模块应该小到你可以用一句话说清它做什么,大到它不需要依赖太多其他模块才能完成自己的工作。

我曾经参与过一个网络库的设计。初期把“TCP连接管理”、“协议解析”、“消息路由”放到了同一个模块。结果想换个协议(比如从自定义协议切到HTTP/2),几乎要重写整个模块。后来我把协议解析拆成独立模块,只暴露一个接口parse(raw_bytes) -> Message。再换协议,只需要换这一个模块。

这就是粒度划分带来的好处。拆得巧,改得少。

要注意循环依赖。如果模块A依赖模块B,模块B又依赖模块A,说明它们该合并,或者你缺了一个公共抽象层。

好了,关于模块化的三个核心理念就先聊到这儿。高内聚低耦合是目标,接口定义是执行契约,粒度划分是尺寸拿捏。三件事都做好了,你的系统架构会非常干净,改一个模块不需要惊动所有人。

模块化设计理念知识体系 模块化设计理念三层结构 高内聚 + 低耦合(核心理念) 内聚类型判断 接口定义契约 粒度划分原则 功能内聚 逻辑内聚 入参出参 边界条件 领域划分 层次划分 关键判断点: 一个模块是否只负责一个职责?接口是否稳定简洁?粒度能不能一句话说清楚?