<< All versions

Skill v1.0.0

currentAutomated scan100/100
abadcafe/superteam/module-design
──Details
PublishedOctober 29, 2026 at 11:32 PM
Content Hashsha256:3b1afa56e415a207...
Git SHA9b7da4d68d55
──Files
Files (1 file, 11.6 KB)
SKILL.md11.6 KBactive
SKILL.md · 245 lines · 11.6 KB

name: module-design description: Use whenever writing code, writing tests, designing architecture or modules, refactoring, or reviewing code, to enforce module boundaries, public interfaces, dependency direction, cohesion, coupling, information hiding, and black-box tests that use only public interfaces. user-invocation: false metadata: version: "1.0.0"


Module Design

设计各个模块时, 必须遵循以下规范. 这些规范源自经过实证的软件工程原则, 不可违反.

注意, 例如bindgen, grpc等工具自动生成的模块, 保持其原样, 不受规范约束.

1. 模块定义

以 Rust 为例:

  • 一个 xxx.rs 文件是一个模块. 模块下面还可以有子模块, 即 xxx.rs + xxx/ 目录,

xxx/ 目录下是 xxx 的子模块(子模块也是模块)

  • 单独一个函数、一个子程序、一个例程、或者一个类(struct/trait/enum), 都不是模块
  • Crate 不是模块, 它是多个模块的集合
  • Python 的包和模块都是模块,等于 Rust 的模块和子模块

模块是类之上的更大组织粒度——一个模块内含多个相互关联的函数、struct、trait、enum 等项, 是对相关功能的聚合容器.

1.1 模块设计

模块的行为由测试用例定义. 每个模块必须声明模块名、职责、公开操作、主要数据实体和测试用例:

module: auth
responsibilities: user authentication and token management
public operations: login, logout, refresh_token, validate_token
data entities: User, Token, Session
tests: test_login_success, test_login_invalid_password, test_logout, test_token_refresh, test_token_expiry

2. 模块约束(不能怎样)

2.1 大小约束

枚举三个维度, 任一超阈值则必须拆分:

维度含义阈值
职责用一句话能说清的独立职责, 需用"和/与"连接即为多个>3
公开操作模块对外暴露的 pub 项(fn, struct, enum, trait, type, const 等)>14
数据实体模块内定义的 struct/enum/type>7
测试用例模块对应的测试用例总数>35

2.2 内聚约束

  • 禁止巧合内聚: 不得创建 utils、misc、helpers、common 等以"杂项"为名

的模块——这种模块内部操作之间没有任何可辨识的关系, 是最差的内聚形式

  • 禁止逻辑内聚: 不得在模块中通过控制标志(flag/enum 参数)选择执行不同的操作——

不同操作应拆为独立模块

  • 禁止将无关项混入同一模块: 如果模块中半数函数操作半数数据, 另外半数函数操作

另外半数数据, 则实际上有两个模块伪装成一个, 必须拆分

2.3 耦合约束

  • 禁止循环依赖: 模块间的 use 依赖不得形成环路. 环路使所有被圈入的模块变成

一个"超级模块", 任何一个发布都必须构建全部. 打破环路的方法:

  1. 提取共同依赖到新模块
  2. 通过 trait / interface 反转依赖方向(DIP), trait 放在使用方模块或单独模块而非实现方模块
  • 禁止语义耦合: 不得利用另一个模块的内部实现知识:
  • 不得因"知道"对方会自动初始化而跳过初始化调用
  • 不得因"知道"对方只用部分字段而只部分构造数据
  • 不得假设对方内部的全局/静态数据已被自己修改
  • 语义耦合的危险在于: 更改被依赖模块的代码会以编译器完全无法检测的方式破坏调用方
  • 禁止依赖不稳定模块: 模块的依赖方向必须指向更稳定的模块(被更多模块依赖的模块).
  • 不得让被广泛依赖的稳定模块依赖不稳定的模块

2.4 不得暴露实现细节

  • 模块的公开接口不得暴露内部数据结构的表示方式——使用 newtype 模式或封装
  • 不得在公开接口中暴露仅内部使用的函数或类型
  • 不得将私有实现细节放入模块接口声明

3. 模块必须达到的标准

3.1 功能内聚

模块必须达到功能内聚——模块内的所有项围绕一个单一的、明确的职责组织. 判断标准:

  • 能用一句话描述模块做什么(而不是"做X和Y和Z")
  • 模块名就是对其职责的精确描述(如 parser、config、auth)
  • 移除模块中任何一个项都会使模块的职责不完整

实证数据: 高内聚模块无故障率 50%, 低内聚模块仅 18%; 内聚-耦合比率最差的模块错误率高 7 倍, 修复成本高 20 倍.

3.2 无环依赖

模块依赖图必须是有向无环图(DAG). 每次新增或修改 use 依赖时必须验证不引入环路.

3.3 依赖方向正确

依赖必须指向更稳定的方向. 模块的"稳定性"由传入依赖数(Ca)和传出依赖数(Ce)决定:

  • I = Ce / (Ca + Ce), 取值 [0, 1], I 越小越稳定
  • 依赖 I 值比自己低的模块

3.4 稳定模块必须抽象

被大量模块依赖的稳定模块必须以抽象(trait)为主, 而非具体实现. 这样即使模块难以改变 (因大量传入依赖), 仍可通过扩展适应新需求. 具体原则:

  • 稳定模块(I 接近 0)应暴露 trait 和类型定义, 具体实现放在依赖它的不稳定模块中
  • 不稳定模块(I 接近 1)可以包含具体实现, 因为它们易于改变
  • 禁止出现"痛苦区"模块: 高度具体(A 接近 0)且大量传入依赖(I 接近 0)——这种模块

改变极其痛苦且无法通过扩展应对

3.5 共同闭合

因同一需求变化而必须同时修改的项, 必须放在同一个模块中(CCP). 这确保变化集中, 减少每个发布周期中受影响的模块数量. 一个模块不应有多个变化原因.

3.6 共同复用

总是一起被复用的项必须放在同一个模块中(CRP). 不一起复用的项不得放在同一模块. 对模块的依赖就是对模块内所有内容的依赖——模块中存在不被一起使用的项, 会导致无关项的 变化强制依赖方升级和重新验证.

4. 模块接口设计

4.1 最小化公开接口

模块的公开接口必须是最小必要集. 判断标准:

  • 不确定是否应该 pub 时, 就不 pub
  • 每增加一个 pub 项都要问: "这个项与模块的抽象一致吗? 去掉它模块职责是否完整?"
  • 通过组合少量精确定义的接口提供功能, 而非暴露大量细粒度操作

4.2 接口一致性

模块的公开接口必须呈现一致的抽象层次:

  • 所有 pub 项必须围绕同一个职责, 不得混入不同层次的关注点
  • 不得在模块接口中添加与现有抽象不一致的公共成员——每次添加时验证一致性
  • 成对提供服务: 有创建就有销毁, 有打开就有关闭, 有添加就有移除

4.3 接口程序化而非语义化

模块接口必须尽可能通过类型系统和编译器强制约束, 而非依赖调用方的语义理解:

  • 用类型(如 newtype、enum)代替裸基本类型, 让编译器捕获误用
  • 用必须调用的方法代替可选的初始化步骤(消除"忘记调用 init"的可能)
  • 将语义接口元素(调用方必须知道的隐式规则)转化为程序化接口元素(编译器可强制的约束)

4.4 信息隐藏

设计模块接口就是问"这个模块需要隐藏什么?". 两类秘密:

  1. 隐藏复杂性: 复杂数据结构、文件格式、算法细节——使调用方的大脑不需要处理
  2. 隐藏变化源: 当变化发生时影响被局部化——业务规则、硬件依赖、数据格式等

可能变化的区域必须被隐藏在接口之后:

  • 业务规则
  • 硬件或外部软件依赖
  • 输入输出格式
  • 状态变量(用访问函数而非直接暴露变量)
  • 数据大小约束(用命名常量而非硬编码)

4.5 抽象接口放在使用方

模块间的 trait (rust) / interface (go, java) 应放在使用它的模块中, 而 非实现它的模块中.

这使得依赖方向从"使用方→实现方"反转为"使用方→抽象接口, 实现方→抽象接口", 打破直接依赖环路, 并让抽象接口精确反映使用方的需求.

5. 模块层次结构设计

深模块: 简洁接口隐藏强大功能(如 Unix I/O: open/read/write/close 封装文件系统与io设备等多个复杂层次) 浅模块: 接口复杂度 ≈ 实现复杂度, 没有抽象价值

深模块优于浅模块

5.1 设计原则

  • 模块层级与目录层级必须对应:同层级模块放在同目录下,子模块放在子目录下
  • 例如:xxx.rs 模块的子模块放在 xxx/yyy.rs
  • 每个模块层级必须改变抽象, 而非仅转发调用
  • 必须按信息隐藏原则而不是按操作的执行顺序拆分模块
  • 如果信息隐藏原则造成模块过大,必须拆分成子模块,而不是拆成同层级的浅模块

5.2 判断标准

若去掉某模块层级后调用方几乎不需要多了解实现细节, 则该层级必须去掉. 例如 pass-through 反例:

// handler.rs — 该模块仅转发, 未改变抽象, 参数和返回值与下层完全一致
pub fn handle_user_query(id: u64) -> User { service::find_user(id) }
// service.rs — 该模块仅转发, 未改变抽象, 参数和返回值与下层完全一致
pub fn find_user(id: u64) -> User { repo::get_user(id) }
// repo.rs — 真正有意义的逻辑
pub fn get_user(id: u64) -> User {
let row = db.query("SELECT ...", &[id]);
User::from_row(row) // 这里改变了抽象: SQL Row → domain User
}
// handler 和 service 模块没有改变抽象, 应去掉, 直接用 repo 模块
// **注意**: 这是模块层级的判断标准, 不适用于函数调用等其他场景的层级判断

6. 模块测试规范

6.1 测试定义行为

测试模块是模块行为的唯一定义来源, 测试用例必须完整覆盖模块所有行为. 模块能做什么、 不能做什么、边界在哪里, 都由测试用例规定. 没有测试覆盖的行为可认为不存在.

  • 测试必须先于实现编写(TDD)——测试定义了模块应该做什么, 实现只是让测试通过
  • 测试用例即行为契约:
  • 正常路径定义"能做什么": 所有公共函数的输入输出、所有公共类型的构造与转换、

所有公共 trait 的行为

  • 错误路径定义"不能做什么": 非法输入、前置条件不满足、资源不可用
  • 边界条件定义"做到什么程度": 极值、空值、溢出、并发
  • 不变量(invariant)定义"始终成立的约束": 无论什么路径都不得违反
  • 测试用例必须从模块的外部可观察行为出发设计, 而非基于内部实现细节
  • 必须先同步更新测试以定义新的行为, 再变更模块的公开接口——不得出现测试与接口不一致
  • 禁止后向兼容测试: 删除即消失, 禁止测试被删除的旧行为, 即使只是验证它会失败

6.2 测试模块结构

  • 每个模块 xxx.rs 对应同目录下的 xxx_tests.rs, 测试用例数 >35 个则拆为 xxx_<子职责>_tests.rs
  • 测试模块禁止调用或依赖任何被测模块的内部实现,最多只允许调用被测模块的模块级公开接口
  • 被测模块内部不得包含任何测试代码——所有测试代码必须在 _tests.rs 文件中
  • 测试模块与被测模块必须是两个独立的模块, 通过被测模块的模块级公开接口交互, 实现模块级黑盒测试
  • 模块级测试必须放在被测模块的同目录下, 禁止放在被测模块的子目录下
  • 进程级测试属于集成测试, 必须端到端覆盖所有用户场景, 只能调用进程外部接口, 而不是代码级接口
  • 集成测试必须放在tests目录而不是src目录下, 与模块级测试区分开

6.3 测试用例命名

  • 格式: test_<操作>_<场景>, 如 test_login_invalid_password
  • 操作名对应被测的公开操作, 场景名描述具体行为或条件
  • 禁止模糊命名: test_function_1、test_edge_case、test_ok 均不合格
All versions