关于 solid
solid 是帮助开发者运用 SOLID 原则设计和审查模块边界的技能,适用于需要改善依赖方向与可测试性的工作。它通过职责划分、依赖箭头和接口检查来引导设计,避免仅为测试而引入抽象。
主要功能
- 01划分模块职责与边界
- 02检查依赖方向
- 03精简调用方接口
- 04将 IO 放在适配器边缘
- 05避免仅为测试创建抽象
适用场景
- 01规划新增模块与依赖关系
- 02审查类和 API 的边界设计
- 03重构以减少单元测试中的模拟对象
- 04处理模块耦合导致的测试困难
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ gusen1453/ creed --skill solid安装后在 Claude Code 中输入/solid确认已加载。
详细介绍
SOLID
输出语言: 使用用户自己的语言回答并编写产物(匹配用户的消息,而不是仓库的历史)。
概述
应用 SOLID,让行为易于修改和测试,而不必堆砌一大堆模拟对象。
配套技能: 使用 grill → write-spec 塑造产品;使用 tdd + test-design 验证行为。本技能只关注结构和依赖方向。
核心原则:难以测试通常意味着难以使用——修正边界,不要用模拟对象掩盖问题。
触发模式(重要)
| 模式 | 触发时机 | 要做什么 |
|---|---|---|
| Gate(主要) | write-spec(或免除设计阶段)之后、write-plan 之前,且该切片新增模块/端口/IO 边界时 | 执行以下完整工作流——宣布 Using solid to … |
| Rescue(救援) | 测试需要 ≥3 个模拟对象;评审/PR 出现耦合;在 tdd 过程中卡在结构问题上 | 执行完整工作流;然后返回调用方 |
| Smell only(仅提示异味,位于 grill 内) | 比较会导致不同边界划分的方案时 | 不要将本技能作为主技能加载。由 Grill 主导会话;方案标签可以指出一种边界异味(“这会让领域层依赖 SQL,之后就会堆满模拟对象”)。正式的 SOLID 检查等到 Gate 阶段再做 |
默认: 如果产品的“是什么/为什么”仍未确定 → 使用 grill,而不是 solid。
何时使用
- 规范已获批准,下一步将要设计包、端口或依赖箭头
- 设计或评审模块边界、类/API 结构、依赖方向
- 单元测试需要大量模拟对象才能“隔离”
- 为提高可测试性而重构;PR 引入耦合/上帝类
何时不要使用:
- 产品决策仍未确定(使用 grill)
- 纯粹的胶水代码/顺手重命名/一次性试验
- 小改动且没有新边界(现有类型、单一行为)——跳过 Gate;转到 write-plan 或 tdd
- 用来替代对产品权衡的讨论
铁律(不可妥协)
1. New non-trivial behavior lives behind a boundary you can name (SRP).
2. Domain/policy does not depend on IO/framework details (DIP).
3. Fake abstractions invented only to satisfy a test are forbidden (no theater).
违反字面要求就是违反其精神。“把整个世界都模拟出来”和“暂时用一个上帝类也没关系”都不算符合要求。
五项工程检查
S — 单一职责
- 一个单元 → 一个变更理由
- 如果名称需要用“和”连接/表示“什么都管的管理器” → 拆分
- 分离:编排、策略、IO 和格式化
O — 开闭原则
- 优先通过新增类型/策略/组合来扩展,而不是永远修改核心分支
- YAGNI:不要预先构建插件系统;但要避免不断把新情况塞进一团代码
L — 里氏替换原则
- 子类型必须遵守调用方的契约(前置条件/后置条件、错误)
- 不要为了复用而继承,导致调用方遇到意外;优先使用组合
I — 接口隔离原则
- 端口应当小巧并贴合调用方,而不是一个庞大的通用接口
- 调用方不应依赖自己从不使用的方法
D — 依赖倒置原则
- 高层策略依赖抽象(端口),而非数据库/HTTP/文件系统/SDK 的具体实现
- 适配器放在边缘;核心应保持纯粹且易于替换
- 信号: 单元测试需要 ≥3 个模拟对象 → 很可能存在 DIP/SRP 问题(参见 test-design)
工作流(调用时——Gate 或 Rescue)
宣布“Using solid to …”,然后:
- 列出单元——每个单元负责什么、如何使用、依赖什么
- 画出依赖箭头——领域层 → 端口 ← 适配器(绝不要让领域层直接依赖 SQL/HTTP 客户端)
- 检查 SRP——拆分包含“和”的职责
- 检查 ISP——精简臃肿的接口
- 检查 DIP——将 IO 移到外部;注入端口
- 检查形式主义——每个新增接口都必须有产品层面的理由,而不能只有测试层面的理由
- 交接——转到 write-plan(Gate)或恢复 tdd(Rescue)。如果产品问题重新出现 → 转到 grill
危险信号
- 上帝类/包“了解整个功能”
- 领域层导入 ORM、HTTP 客户端或 UI 套件
- 子类通过重写来弱化不变量
- 一个调用方只使用 2 个方法,接口却有 15 个方法
- 新接口只用于测试(没有生产适配器)
- 单元测试为了“隔离”而模拟系统的一半
合理化借口对照表
| 借口 | 现实 |
|---|---|
| “等遇到问题再拆分” | 需要 4 个模拟对象时,问题已经出现了。 |
| “具体实现比端口更简单” | 直到出现第二条 IO 路径。端口加一个适配器,比重写更省事。 |
| “继承可以复用代码” | 继承会耦合层级;应改用组合。 |
| “一个服务接口更整洁” | 臃肿的接口会迫使你实现伪方法,并带来 ISP 债务。 |
| “模拟对象能证明 DIP” | 模拟对象会掩盖 DIP 的失败。真正端口就绪后,模拟对象更少才是证明。 |
| “我们还在梳理需求——现在就全面运行 solid” | 异味只出现在选项标签中。确定 what/why 后,再全面运行 solid。 |
检查清单
- 产品的 what/why 已确定(或正在 Rescue 中途实施)
- 每个单元都有一个明确用途,名称中不含“和”
- 依赖箭头指向策略,而非指向 IO
- 端口精简;适配器负责框架
- 没有仅用于测试的抽象
- 已准备好进入 write-plan 或 tdd/test-design,无需堆砌模拟对象
交接
转换关卡——在将边界交给 write-plan(关卡)或退回代码(Rescue)之前,询问用户(使用一次 AskUserQuestion;绝不自动推进):
A) Recommended: Proceed → write-plan (Gate) / resume tdd (Rescue)
B) Review first — run **review** on these boundaries (`references/solid.md`; upstream = spec's promised boundaries), then return to this gate
C) Adjust — rework the structure, then return to this gate
D) Something else (I will type it)
- 产品决策仍未确定 → grill → write-spec
- 边界已确定 → write-plan
- 正在实施 → tdd + test-design
- 正在发布 → review / commit-and-push
评论
支持 Markdown