4. 模块职责与接口契约:模块内聚、接口定义、依赖方向
4.1 核心概念映射
在系统分解中,模块职责决定了“做什么”,接口契约决定了“怎么协作”。二者共同决定了系统的可维护性和演进成本。以下表格归纳了本节将展开的核心关系:
| 设计维度 | 目标 | 评判标准 | 常见反例 |
|---|---|---|---|
| 模块内聚 | 模块内部职责单一、关联紧密 | 仅因一个原因修改模块内部代码 | 工具类模块承担业务编排职责 |
| 接口契约 | 对外提供稳定、可验证的交互协议 | 接口参数/返回值语义明确,版本兼容 | 接口返回Map |
| 依赖方向 | 依赖指向稳定层,避免循环依赖 | 依赖倒置、分层隔离、构建图无环 | 上层模块直接依赖第三方SDK内部类 |
4.2 模块内聚:从“类”到“上下文”的聚合规则
职责内聚是模块划分的第一原则。实践中,我们通常使用单一职责原则(SRP)判断模块粒度:一个模块应当只有一个引起变化的原因。
- 功能内聚:同一模块内的所有元素共同完成一个单一功能(如:模块“SHA256计算器”仅输出给定输入的哈希值)。
- 通信内聚:模块内元素操作同一数据集合(如:模块“用户令牌管理”读取、签发、吊销同一用户令牌数据)。
- 时序内聚:模块内元素按固定顺序执行(如:初始化模块需依次执行配置加载→资源池预热→健康检查)。
原型落地匹配要点: 构建原型时,先用“文件/目录”作为内聚物理边界。例如,将订单生成、订单验证、订单持久化放在同一目录下,而非按“工具类”跨目录聚合。原型阶段过度抽离会导致后续拆分成本升高。
4.3 接口契约:语义、格式、版本与错误处理
接口契约是模块之间唯一的信任节点。契约应包含以下要素:
- 语义清晰的动作名称(动词+名词,如
submitOrder而非process)。 - 请求/响应的数据结构定义(强类型对象,拒绝Map泛型)。
- 幂等性与超时规则(重试策略应在契约中注明)。
- 错误码与异常范围(接收方必须处理的错误类型)。
方案选型权衡: 原型阶段接口定义可使用 Protobuf / Thrift IDL 或 OpenAPI 3.0。若团队尚未定技术栈,推荐先用 YAML 手写契约文件,避免过早绑定 RPC 框架。
# 契约定义示例 (order-service / proto / v1 / order_api.proto)
service OrderService {
// 提交订单(幂等:使用 client_order_id 去重)
rpc SubmitOrder (SubmitOrderRequest) returns (SubmitOrderResponse);
// 查询订单状态
rpc GetOrderStatus (GetOrderStatusRequest) returns (GetOrderStatusResponse);
}
message SubmitOrderRequest {
string client_order_id = 1; // 客户端幂等ID
string user_id = 2;
repeated Item items = 3;
}
message SubmitOrderResponse {
string order_id = 1; // 生成的订单ID
OrderStatus status = 2; // PENDING / CONFIRMED / FAILED
string fail_reason = 3; // 失败时的详细原因
}
4.4 依赖方向:分层、反转与规避循环
模块之间的依赖方向决定了系统的弹性。依赖规则:高层的策略模块不应依赖低层的实现细节,而是依赖抽象接口。层级依赖方向应始终指向稳定的基础模块。
- 正向依赖:包A依赖包B(B更稳定)。
- 反向控制(依赖倒置):接口定义在调用方包内,实现方依赖该接口。
- 禁止:包A依赖包B,包B又依赖包A(循环依赖需通过引入抽象层或事件总线破除)。
以下SVG图展示了模块职责划分与接口契约在典型分层架构中的依赖关系和信任链路:
4.5 关键风险点与落地步骤
关键风险点:
- 接口膨胀:原型期未收敛接口,出现大量类似方法(如 createUser_v1 / createUser_v2),应在一开始就明确契约版本策略。
- 循环依赖:跨模块引用时未做接口抽离,导致构建失败或运行时死锁。使用构建工具(如 Java 的 jdeps)提前扫描。
- 内聚不足:模块内同时包含业务逻辑与数据库连接、日志打印等横切关注点,导致模块对外暴露过多细节。
落地步骤(原型阶段可执行):
- 绘制模块依赖图(手绘或工具),标记每个模块的输入输出接口。
- 在每个模块目录下创建
api/子目录,存放接口定义文件(Proto / OpenAPI / 接口抽象类)。 - 使用 依赖方向检查脚本 在 CI 中阻止反向依赖。
# 依赖检查示例(基于 Go 的 import 路径检查)
#!/bin/bash
# 检查 domain 层不应 import infrastructure 层
for pkg in $(find ./domain -type d); do
if grep -q "infrastructure" "$pkg/*.go" 2>/dev/null; then
echo "ERROR: Domain package imports infrastructure: $pkg"
exit 1
fi
done
echo "依赖方向检查通过"
4.6 原型实现匹配要点
- 在原型代码中保持显式接口定义,禁止模块间通过全局变量或单例直接耦合。
- 使用依赖注入方式组装模块,便于后续替换实现(如从内存存储切换为 MySQL)。
- 每个接口的单元测试覆盖率 > 90%,测试即契约文档。
4.7 验收标准
| 编号 | 验收项 | 检查方式 |
|---|---|---|
| 4-1 | 所有模块职责可在1句话内描述清晰 | 代码评审 |
| 4-2 | 模块间接口均为显式定义(非Map/JSON) | 静态分析 + 审阅 |
| 4-3 | 模块依赖图无环(可在构建时验证) | CI 依赖检测脚本 |
| 4-4 | 任意模块可被Mock替换进行单元测试 | UT 覆盖率报告 |
| 4-5 | 接口契约文档(OpenAPI / Proto 文件)与实现始终保持一致 | diff 检查 + 契约测试 |