mcpField
C
codebase-design

codebase-design

github.com/lwlee2608开发1142 次安装1 天前更新
代码仓库

关于 codebase-design

这项软件设计技能指导开发者构建接口精简、内部行为丰富的深模块。它提供统一术语和设计原则,以改善调用方的能力、维护局部性和可测试性。

主要功能

  1. 01深模块设计指导
  2. 02统一接口与接缝术语
  3. 03选择适配器和端口的原则
  4. 04通过依赖注入提升可测试性
  5. 05Go 测试中的 testify 断言指南

适用场景

  1. 01围绕清晰模块重构代码
  2. 02评估接口复杂度
  3. 03判断接缝是否需要端口
  4. 04围绕真实依赖设计测试

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/lwlee2608/agent-skills --skill codebase-design

安装后在 Claude Code 中输入/codebase-design确认已加载。

附带文件DEEPENING.mdDESIGN-IT-TWICE.md

详细介绍

代码库设计

设计深模块:在简洁的接口后封装大量行为,放在清晰的接缝处,并且可通过该接口进行测试。设计或重构代码时,始终使用这套语言和这些原则。目标是为调用方提供杠杆效应,为维护者提升局部性,并让所有人都能进行测试。

术语表

请准确使用以下术语:不要替换成“组件”“服务”“API”或“边界”。统一用语正是关键所在。

模块:任何具有接口和实现的东西。刻意不限定规模:可以是函数、类、包或跨层切片。避免使用:单元、组件、服务。

接口:调用方必须了解的所有信息,才能正确使用模块:包括类型签名,也包括不变量、顺序约束、错误模式、必需配置和性能特征。避免使用:API、签名(范围太窄,它们仅指类型层面的表面)。

实现:模块内部的内容,即它的代码主体。它不同于适配器:一个东西可以是实现很大的小型适配器(一个 Postgres 仓库),也可以是实现很小的大型适配器(一个内存中的伪实现)。当讨论接缝时使用“适配器”;其他情况使用“实现”。

深度:接口所带来的杠杆效应。调用方(或测试)每了解一单位接口,就能运用多少行为。大量行为隐藏在简洁接口后面的模块是深模块;接口几乎和实现一样复杂的模块是浅模块。

接缝 (Michael Feathers):一个可以在不修改该位置的情况下改变行为的地方;也就是模块接口所在的位置。接缝放在哪里本身就是一项设计决策,与接缝后放什么内容是两回事。避免使用:边界(这个词被 DDD 的限界上下文占用了)。

适配器:在接缝处满足接口的具体事物。描述的是角色(它填补什么位置),而非实质(它内部是什么)。

端口:专门用于在接缝处替换适配器的接口(Ports & Adapters)。端口是一个接缝,再加上至少有两个适配器的预期,通常分别用于生产环境和测试。端口可以是语言层面的 interface,但并非每个接缝都需要端口。

杠杆效应:调用方从深度中获得的收益。每了解一单位接口,就能获得更多能力。一个实现能在 N 个调用点和 M 个测试中发挥作用。

局部性:维护者从深度中获得的收益。变更、缺陷、知识和验证集中在一处,而不是散布到各个调用方。修复一次,处处修复。

深模块与浅模块

深模块 = 简洁的接口 + 大量实现:

┌──────────────────────┐
│   Small Interface    │  ← Few methods, simple params
├──────────────────────┤
│                      │
│ Deep Implementation  │  ← Complex logic hidden
│                      │
└──────────────────────┘

浅模块 = 复杂的接口 + 少量实现(应避免):

┌─────────────────────────────────┐
│        Large Interface          │  ← Many methods, complex params
├─────────────────────────────────┤
│  Thin Implementation            │  ← Just passes through
└─────────────────────────────────┘

设计接口时,请自问:

  • 能减少方法数量吗?
  • 能简化参数吗?
  • 能把更多复杂性隐藏在内部吗?

原则

  • 深度是接口的属性,而非实现的属性。 深模块内部可以由小型、可模拟、可替换的部分组成;这些部分只是不属于接口。模块可以有内部接缝(仅对其实现私有,由自身测试使用),也可以在接口处有外部接缝。
  • 删除测试。 想象删除该模块。如果复杂性随之消失,它只是一个传递层。如果复杂性又出现在 N 个调用方中,它就发挥了应有的价值。
  • 接口就是测试面。 调用方和测试都经过同一个接缝。如果你想越过接口进行测试,这个模块的形状可能就不合适。
  • 一个适配器意味着接缝仅存在于设想中;两个适配器才意味着接缝真实存在。 除非接缝两侧确实存在变化,否则不要引入接缝。只有当测试中无法运行真实依赖时,测试替身才算第二个适配器。

可测试性

可测试性是结构带来的结果,而非独立目标。两条规则可以实现这一点:

  1. 接收依赖,而不是自行创建。 自行构造依赖的模块会将依赖固定下来,所有调用方都无法更改。将依赖作为参数传入。
  2. 返回结果,而不是修改状态。 报告计算结果的模块会把是否采取行动的决定留给调用方。

不要仅仅为了模拟而声明接口。“我需要模拟这个”并不是定义端口的理由:

Can the real dependency run inside the test?
  yes -> use it. No interface.
        (pure logic, in-memory state, temp files,
         Postgres/Redis/Kafka in a container)
  no  -> port + two adapters, production and test.
        (third-party APIs, another team's service
         across a network seam)

注入依赖才能让模块可测试。在依赖之上声明接口是另一项独立决定,只有选择“否”的情况才值得这么做。模拟本可以实际运行的内容,只能证明代码会调用你让它调用的东西:它无法发现错误的查询、错误的端点或已经偏离的架构,而这些才是真正会发生的问题。

断言(Go)

使用 testify,不要手写 if got != want { t.Fatalf(...) }。当测试无法继续时(设置、错误),使用 require.*;可以继续时,使用 assert.*,这样一次运行就能报告所有失败。先写期望值:assert.Equal(t, want, got)。

关系

  • 一个 模块 恰好有一个 接口(它向调用方和测试呈现的表面)。
  • 深度是 模块 的属性,以其 接口 为参照衡量。
  • 接缝是 模块 的 接口 所在之处。
  • 适配器位于 接缝,并满足 接口。
  • 深度为调用方带来 杠杆效应,为维护者带来 局部性。

不采用的表述

  • 将深度视为实现代码行数与接口代码行数之比(Ousterhout):这会鼓励人为扩充实现。我们采用将深度视为杠杆效应的定义。
  • 将“接口”视为语言关键字(Go 或 TypeScript 的 interface)或类的公共方法:范围太窄;这里的接口包括调用方必须了解的所有信息。具体的 Go 结构体也已有接口。只有为 端口 才声明 Go 的 interface;只有一个适配器时,它只会增加表面,而不会增加深度。
  • “边界”:这个词与 DDD 中的限界上下文含义重叠。请说 接缝 或 接口。
  • 将可模拟性作为声明接口的理由:这会让每个依赖看起来都需要一个端口。先问问真实依赖能否在测试中运行;类别请参见 DEEPENING.md。

深入探讨

  • 在给定依赖项的情况下深化一个集群,请参见 DEEPENING.md:依赖类别、接缝规范,以及替换而非叠加。
  • 探索不同的接口方案,请参见 DESIGN-IT-TWICE.md:启动多个并行子 Agent,以截然不同的方式设计接口,然后从深度、局部性和接缝位置方面进行比较。

致谢

改编自 mattpocock/skills。

评论

支持 Markdown