codebase-design

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Codebase Design

代码库设计

Complexity is the enemy, and the single lever that fights it at the module level is depth: a module's value is the ratio of functionality it provides to the size of the interface a caller has to learn. A deep module hides a lot behind a little; a shallow one exposes almost as much as it does. Given a choice, make the interface simpler even if that means the implementation underneath grows; the interface is what every caller pays for, forever; the implementation is paid for once, by whoever maintains it.
复杂度是敌人,而在模块层面,对抗复杂度的唯一手段是深度(depth):一个模块的价值,取决于它所提供的功能与调用方需要学习的接口规模之比。深模块用少量接口隐藏大量功能;浅模块暴露的内容几乎与它提供的功能一样多。如果必须选择,应让接口保持简单,即使这意味着底层实现会变得更庞大;接口是每个调用方都要永久付出的代价,而实现只需由维护者支付一次。

Phase 1: Depth is the metric

阶段 1:深度是衡量标准

  • A deep module: powerful functionality, a small number of simple things a caller needs to know to use it. A well-designed file system API, or a well-designed HTTP client with a handful of methods that hide connection pooling, retries, and serialization.
  • A shallow module: an interface about as complicated as what it does. A class with one method that thinly wraps a single library call adds a name to learn without hiding any real complexity; the caller might as well have called the library directly.
  • When deciding whether to add a class, function, or layer, ask what it hides. If the honest answer is "not much," it's shallow, and shallow modules accumulate into a system where understanding any one part requires understanding many parts at once; the opposite of what modules are for.
  • 深模块:功能强大,调用方只需了解少量简单内容即可使用。例如,设计良好的文件系统 API,或只提供少量方法,却隐藏了连接池、重试和序列化等细节的 HTTP 客户端。
  • 浅模块:接口的复杂度与其所做的事情大致相当。一个仅含单个方法、只是薄薄封装一次库调用的类,只是增加了一个需要学习的名称,却没有隐藏任何真正的复杂度;调用方还不如直接调用该库。
  • 决定是否添加类、函数或层时,先问它隐藏了什么。如果坦诚的答案是“没有多少”,它就是浅模块。浅模块不断累积,最终会形成这样的系统:理解其中任何一部分,都需要同时理解很多部分,而这恰恰与模块存在的目的相反。

Phase 2: Information hiding, and its failure mode

阶段 2:信息隐藏及其失败模式

  • Information hiding: a design decision (a data format, an algorithm, an internal data structure) lives inside one module and is invisible to its callers: they depend on the module's behavior, not its internals.
  • Information leakage: the same piece of knowledge is needed in two or more places to make a change. If changing one internal detail forces edits in a second, ostensibly unrelated module, that detail leaked across the boundary: the two modules are more coupled than their interfaces admit.
  • A common leak source is temporal decomposition: splitting a module by the order operations happen in rather than by the knowledge each part owns (a
    Parse
    class, a
    Validate
    class, a
    Save
    class, each half-empty and only sensible read together in sequence); this scatters one coherent piece of domain knowledge across several classes that all have to change together.
  • 信息隐藏(information hiding):一项设计决策,例如数据格式、算法或内部数据结构,被封装在一个模块内部,对调用方不可见。调用方依赖模块的行为,而不是它的内部实现。
  • 信息泄漏(information leakage):完成某项变更时,需要在两个或更多位置使用同一份知识。如果修改一个内部细节会迫使开发者同时修改另一个看似无关的模块,就说明该细节已泄漏跨越边界,这两个模块之间的耦合程度也超过了其接口所体现的程度。
  • 常见的泄漏来源是时序分解(temporal decomposition):按操作发生的顺序拆分模块,而不是按每部分所拥有的知识拆分,例如设置一个
    Parse
    类、一个
    Validate
    类和一个
    Save
    类。它们各自都只有一半内容,只有按顺序组合起来才有意义。这会把一份连贯的领域知识分散到多个类中,导致这些类必须一起修改。

Phase 3: Concrete red flags

阶段 3:具体的危险信号

Name the smell rather than a vague "this feels off" (same discipline as
code-review
's Fowler-smell naming):
  • Pass-through method: a method that does nothing but call another method with the same arguments. It adds interface surface without adding depth; usually a sign the layering itself is unnecessary or misplaced.
  • Conjoined methods/classes: two pieces that can't be understood independently, because a change to one routinely requires a matching change to the other. Either merge them (they're really one module pretending to be two) or find the actual missing abstraction that would let them vary independently.
  • Generic-sounding names hiding a special-purpose module: a class called
    Manager
    ,
    Helper
    , or
    Processor
    with no further specificity is a sign the module's actual responsibility was never named, which usually means it was never really decided either.
  • Repeated pattern with no shared home: the same few lines copy-pasted at every call site is knowledge that should be hidden inside one deep module instead of known by every caller.
与其模糊地说“感觉哪里不对”,不如明确指出问题所在,这与
code-review
中采用 Fowler 气味命名方式的纪律相同:
  • 透传方法(pass-through method):一个方法所做的只是使用相同参数调用另一个方法。它增加了接口面积,却没有增加深度,通常意味着分层本身不必要或位置不当。
  • 相互绑定的方法或类(conjoined methods/classes):两部分内容无法独立理解,因为修改其中一部分通常需要同步修改另一部分。此时应将它们合并,因为它们实际上是一个模块,只是假装成了两个;或者应找到真正缺失的抽象,使两部分能够独立变化。
  • 使用泛化名称掩盖专用模块:一个仅命名为
    Manager
    、
    Helper
    或
    Processor
    、没有进一步明确用途的类,说明模块的实际职责从未被命名,而这通常也意味着职责从未被真正确定。
  • 没有统一归属的重复模式:在每个调用位置复制粘贴同样的几行代码,意味着这些知识应被隐藏在一个深模块内部,而不是要求每个调用方都掌握它。

Phase 4: Design it twice

阶段 4:设计两遍

For a genuinely new interface or module boundary, sketch at least two substantially different designs before committing to one, not two trivial variations of the same idea. Comparing two real alternatives surfaces trade-offs neither would reveal alone, and the second design is often clearly better once the first exists as a point of contrast. Skipping straight to the first idea that comes to mind is how a project ends up living with the first idea's weaknesses indefinitely.
对于真正全新的接口或模块边界,在确定一种方案前,应至少勾勒出两种实质不同的设计,而不是同一想法的两个琐碎变体。比较两个真正不同的备选方案,可以暴露任何单一方案都无法单独揭示的权衡;而有了第一个方案作为参照,第二个方案往往明显更优。直接采用脑海中冒出的第一个想法,会让项目永远带着这个想法的弱点生存下去。

Phase 5: Where the seam goes

阶段 5:接缝应放在哪里

The module boundary chosen here is the seam
tdd
tests against; a seam placed at a genuinely deep interface stays stable as implementation changes, which is exactly what makes tests at that seam survive refactors. A seam placed at a shallow, leaky boundary forces tests (and every caller) to know internal details, and both the tests and the callers break together whenever the internals shift. If a
tdd
cycle keeps needing to update tests for reasons that aren't behavior changes, that's this phase's red flag showing up from the testing side; revisit where the seam actually sits.
此处选定的模块边界就是
tdd
测试所针对的接缝(seam)。位于真正深层接口上的接缝,会在实现发生变化时保持稳定,这正是接缝处的测试能够在重构后继续存在的原因。如果接缝位于浅薄且存在泄漏的边界上,测试和每个调用方都被迫了解内部细节,一旦内部实现发生变化,测试与调用方就会同时失效。如果一个
tdd
循环总是因为行为变化以外的原因而需要更新测试,这表明本阶段的危险信号从测试侧暴露出来;此时应重新审视接缝的实际位置。

Done when

完成标准

  • Every new module's interface is simpler, in absolute terms, than what it hides, not proportionally simpler, actually simple enough to hold in your head.
  • No single piece of domain knowledge requires touching two or more modules to change.
  • None of Phase 3's red flags describe the design as shipped.
  • A genuinely new module boundary was compared against at least one real alternative before being committed to.
  • Generated prose uses commas, colons, periods, or reworded sentences instead of em dashes.
  • 每个新模块的接口从绝对意义上看,都比它隐藏的内容更简单;不是按比例更简单,而是简单到足以完整记在脑中。
  • 没有任何一份领域知识需要通过同时修改两个或更多模块才能变更。
  • 阶段 3 中的任何危险信号都不适用于最终交付的设计。
  • 真正全新的模块边界在确定方案前,已与至少一个真实可行的备选方案进行比较。
  • 生成的文字使用逗号、冒号、句号或改写后的句子,而不使用 em dash。