English
Writing maintainable code means making behavior and its reasons inspectable. A reader should be able to identify the inputs, the permitted states, the effects, and the conditions under which an operation succeeds or fails. Formatting helps, but it cannot supply a missing explanation of what the program is supposed to preserve.
The need is older than modern style guides. Early subroutine libraries already required agreements about inputs, outputs, and control returning to the caller. Structured programming and reasoning about invariants developed ways to understand larger computations in smaller parts. Today’s work applies these ideas to names, interfaces, state, and changes that another person can review.
Prerequisites and connections: Basic programming (§2.1) and code reading (§8.3). Testing (§7.2) and refactoring (§7.5) provide ways to check and improve an implementation.
The Shape of the Skill: Communicating Through Code
Readable code reduces the number of facts a reader must keep in mind at once. A function with an explicit input and return value may be easier to reason about than one that silently reads and changes several global variables. This is about dependencies, not a fixed maximum number of lines.
An invariant is a useful explanation of state. Consider summing a list of integer amounts in cents. After processing the first k elements, the running total equals their sum. It starts at zero for an empty prefix; adding the next amount preserves the claim; after all elements it gives the required total. For [120, 80], the totals are zero, 120, and 200. This reasoning also explains the empty-input result. A finite loop over the list supplies termination; the invariant alone does not.
Aho and Ullman’s treatment of program correctness makes this connection explicit: the reason a loop works can also be a short explanation for its reader. In production code, use a comment or assertion when it makes a non-obvious invariant visible. State the numeric assumptions too: an argument over mathematical integers does not by itself rule out overflow in a fixed-width type.
Naming, Decomposition, Simplicity, and the AI Shift
Naming and the Communication of Intent
A name should express the distinction a caller needs. timeout_ms distinguishes milliseconds from seconds; end_exclusive clarifies which boundary is included; a name such as load_or_create warns that an apparent lookup may write state. Names and types can share this work: a duration type may carry units more reliably than repeated naming conventions.
Use the vocabulary of the domain consistently. If “customer” and “account” refer to different entities, interchanging the names creates a false equivalence. Avoid expanding names with information already obvious from a small local context, but make externally visible interfaces explicit enough for callers who do not see the implementation.
Comments are useful for a constraint, an invariant, a surprising decision, or a reference to an external requirement. A comment that only repeats the next statement adds little. When behavior changes, update the name, interface documentation, and relevant examples together; a clear but stale explanation can be especially misleading.
Decomposition and Abstraction
A useful abstraction lets callers rely on a contract while leaving some implementation choices inside the module. For a reservation operation, callers may need to know whether it can fail, whether it changes availability, and whether retrying is safe. They should not need to know the layout of an internal table unless that detail affects the contract.
Separate calculations from external effects where this improves reasoning. A function that computes an invoice total can be tested without a database. Recording the invoice and sending a notification raise different failure questions. Splitting these operations into named functions does not make them atomic: a crash between them still needs a design for recovery or coordination (§7.1).
Extract a function when it names a coherent operation, hides a changing decision, or allows independent checking. Excessive fragmentation can force readers through many tiny functions to understand one condition. Similar-looking code also need not share one abstraction if the two uses are expected to change for different reasons. Compare the assumptions and responsibilities before removing duplication.
Simplicity and the Resistance to Cleverness
Simplicity concerns how much reasoning is required to establish behavior. A shorter expression can hide evaluation order, mutation, or a special case. Prefer the version whose conditions and effects a maintainer can check, using the team’s familiar idioms.
Data representation can remove mistakes before control flow is written. Suppose two booleans, is_paid and is_refunded, encode an order. They permit four combinations, including refunded but never paid. If the domain has exactly three relevant states, pending, paid, and refunded, one state value can exclude that invalid combination. It still does not prevent an invalid transition such as refunding a pending order; transitions need their own rules.
Avoid speculative generality. Implement the variants the task actually needs, while leaving a reasonable place for a known extension. When performance requires a less obvious implementation, measure the relevant workload and preserve a clear explanation or simple reference implementation. Cleverness is not the problem by itself; an unexplained cost or correctness argument is.
The AI Shift: Specifying and Reviewing Generated Code
Generating code changes how an implementation may be produced, but leaves the need to specify and check its behavior. Give an assistant the input domain, required outputs, failure behavior, and repository interfaces. Ask for a bounded change whose consequences you can inspect.
Review generated code against those requirements, including dependency use and state changes. Derive at least some test expectations independently of the proposed implementation; a generated test can repeat the same misunderstanding as generated code. For the three-state order example, check both permitted states and forbidden transitions.
Keep or revise a proposal because you can explain why it satisfies the task. If a large generated change is difficult to understand, reduce its scope or divide it into separately checkable steps. The useful skill is still reasoning about a program and its maintenance, whether the first draft was typed, generated, or adapted from existing code.
What Changes With Mastery
You can justify a name, representation, or module boundary by the confusion or invalid behavior it prevents. You can also recognize when a style rule costs more than it helps.
Revisit a working function and ask a reader to explain its contract and failure cases. Revise the parts they cannot infer, then check that the behavior remains correct. This gives feedback about readability without treating line count, abstraction count, or agreement with one author’s style as a substitute for understanding.
Resources
Books and Texts
Kernighan and Pike connect style, interfaces, testing, and debugging. McConnell provides a broad construction reference. Ousterhout emphasizes interfaces and complexity; Martin offers detailed style prescriptions. Compare their advice on one program instead of treating either as a universal rulebook. Hunt and Thomas discuss broader working habits, and Knuth’s Literate Programming examines how explanation and code can be organized together.
| Book | Role | Type |
|---|---|---|
| Kernighan & Pike, The Practice of Programming | Style, interfaces, testing, and debugging | Practice |
| Ousterhout, A Philosophy of Software Design (2nd ed.) | Complexity and deep modules | Depth |
| McConnell, Code Complete (2nd ed.) | Encyclopedic construction reference | Reference |
| Martin, Clean Code | Widely-read craft handbook; read critically | Entry |
| Hunt & Thomas, The Pragmatic Programmer (20th Anniversary) | Craft and mindset of the working programmer | Entry |
| Knuth, Literate Programming | The foundational vision of code as communication | Depth |
Talks and Essays
Hickey’s “Simple Made Easy” distinguishes familiarity from structural simplicity. Hoare’s “The Emperor’s Old Clothes,” published in 1981 following his 1980 Turing Award, reflects on language design and implementation failures. Use both as arguments to examine against concrete designs, rather than as a source of slogans that settle every tradeoff.
| Resource | Platform | Type |
|---|---|---|
| Hickey, “Simple Made Easy” (free) | InfoQ | Depth |
| Hoare, “The Emperor’s Old Clothes” (published 1981) | ACM | Depth |
Practice, Tools, and Projects
Revise a working function, explain its invariant or contract, and ask another reader to inspect it. Compare the original and revised versions for both behavior and ease of understanding. Apply the same exercise to generated code: identify the assumptions, derive boundary cases, and make only changes whose consequences you can explain.
| Resource | Platform | Type |
|---|---|---|
| Revising code for clarity after it works | Local practice | Practice |
| Reading excellent code critically (§8.3) | Open source / stdlib | Practice |
| Critically reviewing AI-generated code | Local practice | Practice |
Traps
| Trap | Why it misleads | Better response |
|---|---|---|
| Equating readability with formatting | Layout does not explain state, effects, or contracts. | Make the important assumptions and invariants visible. |
| Applying a universal function-size limit | Small fragments can obscure a coherent operation. | Judge dependencies and the explanation a reader needs. |
| Abstracting every similar fragment | Similar text may encode independently changing rules. | Compare responsibilities before sharing an abstraction. |
| Using fewer lines as the definition of simple | Dense expressions can hide state and edge cases. | Prefer behavior that can be checked locally. |
| Assuming valid states imply valid transitions | A representation can exclude bad combinations but still allow bad operations. | Define and check transition rules. |
| Approving generated code because its tests pass | Code and tests can share the same mistaken assumption. | Derive expectations from the requirement independently. |
中文
编写易于维护的代码,就是让行为及其理由能够被检查。读者应该能找到输入、允许的状态、产生的影响,以及操作成功或失败的条件。格式有帮助,但如果程序应该维持什么性质尚不清楚,格式无法补上这份解释。
这种需求早于现代风格指南。早期子程序库已经需要约定输入、输出,以及控制怎样返回调用者。结构化程序设计和不变量推理,发展了把较大计算分成较小部分来理解的方法。今天的工作把这些思想用于命名、接口、状态,以及他人能够审查的修改。
前置知识与联系:基础编程(§2.1)与代码阅读(§8.3)。测试(§7.2)和重构(§7.5)提供检查与改进实现的方法。
这项能力的形状:通过代码进行沟通
可读代码减少了读者必须同时记住的事实。输入和返回值明确的函数,往往比暗中读取并修改多个全局变量的函数更容易推理。这涉及依赖关系,而不是统一的函数行数上限。
不变量可以解释状态。考虑对一组以分为单位的整数金额求和。处理前 k 个元素后,累计值等于这些元素的和。空前缀的初始值为零;加入下一个金额后,这个结论仍然成立;处理完全部元素时,就得到所需总和。对于 [120, 80],累计值依次是零、120 和 200。这也解释了空输入的结果。遍历有限列表的循环会终止;仅有不变量还不能证明终止。
Aho 和 Ullman 对程序正确性的讨论明确联系了这两件事:循环为何正确,也可以成为对读者的简短解释。在实际代码中,如果注释或断言能揭示不明显的不变量,就使用它们。还要说明数值假设:基于数学整数的论证,并不能单独排除固定位宽类型的溢出。
命名、分解、简单性与 AI 转变
命名与意图沟通
名称应该表达调用者需要区分的含义。timeout_ms 区分毫秒与秒;end_exclusive 说明边界是否包含在内;load_or_create 这样的名称提示,看似查询的操作也可能写入状态。名称与类型可以分担这项工作:时长类型可能比反复依赖命名约定更可靠地表达单位。
一致地使用领域词汇。如果“客户”与“账户”指不同实体,交替使用会制造错误的等同关系。局部上下文已经清楚的信息,不必全部塞进名称;但对外接口应足够明确,让不阅读实现的调用者也能使用。
注释可以说明约束、不变量、不寻常决定的原因,或外部需求的出处。只重复下一条语句的注释,价值很小。行为变化时,应一起更新名称、接口文档与相关例子;清楚却过时的解释,尤其容易误导。
分解与抽象
有用的抽象让调用者依赖约定,同时把部分实现选择留在模块内部。对于预约操作,调用者可能需要知道它会不会失败、是否改变可用数量,以及重试是否安全。内部表格怎样排列,如果不影响接口约定,就不必让调用者了解。
在有助于推理时,把计算与外部影响分开。计算发票总额的函数可以不依赖数据库来测试。记录发票与发送通知,则涉及不同的失败问题。把这些操作拆成有名称的函数,并不能让它们成为原子操作:在两步之间崩溃,仍然需要恢复或协调设计(§7.1)。
当一个函数能命名完整操作、隐藏可能变化的决定,或支持独立检查时,就值得提取。过度拆分可能迫使读者穿过许多小函数,才能理解一个条件。外观相似的代码,如果预计会因不同原因而变化,也未必应该共用一个抽象。消除重复前,先比较假设与职责。
简单性与抵抗炫技
简单性涉及确定行为需要多少推理。更短的表达式可能隐藏求值顺序、状态修改或特殊情况。应结合团队熟悉的写法,选择维护者能够检查条件与影响的版本。
数据表示可以在编写控制流程之前排除一些错误。假设用两个布尔值 is_paid 和 is_refunded 表示订单,它们允许四种组合,包括从未付款却已退款。如果领域中恰好只有三个相关状态——pending、paid、refunded——用单个状态值就能排除这种无效组合。但它仍不能阻止“待付款订单直接退款”这样的无效转换;状态转换还需要单独的规则。
避免为了猜测中的需求进行通用化。实现任务确实需要的变体,同时为已知扩展留下合理位置。如果性能要求采用较难理解的实现,应测量相关工作负载,并保留清楚的解释或简单的参考实现。问题不在于技巧本身,而在于成本或正确性缺少能够检查的理由。
AI 带来的变化:规定并审查生成代码
生成代码改变了实现可能怎样产生,但仍然需要规定和检查行为。向辅助工具提供输入范围、所需输出、失败行为和仓库接口,并要求生成范围明确、影响能够检查的修改。
根据这些要求审查生成代码,包括依赖的使用与状态变化。至少独立推导一部分测试预期,不要全部从建议实现反推;生成的测试可能重复生成代码的同一个误解。对于三状态订单的例子,既要检查允许的状态,也要检查禁止的转换。
保留或修改建议,应基于自己能够解释它为何满足任务。如果大批生成改动难以理解,就缩小范围,或拆成能够分别检查的步骤。无论初稿是手写、生成,还是改编已有代码,核心能力仍是对程序及其维护进行推理。
熟练之后会发生什么变化
你能够说明,一个名称、表示方式或模块边界避免了什么混淆或无效行为;也能够识别某条风格规则何时带来的成本超过帮助。
重新检查一个已经能工作的函数,请另一位读者解释它的约定和失败情况。修改对方无法推断的部分,再检查行为仍然正确。这样可以得到可读性的反馈,不必用行数、抽象数量,或是否符合某位作者的风格代替真正的理解。
资源
书籍与文本
Kernighan 与 Pike 联系了风格、接口、测试与调试;McConnell 提供范围较广的程序构造参考。Ousterhout 强调接口与复杂性,Martin 给出详细风格建议。应在具体程序上比较建议,而不是把任何一本当作普遍规则。Hunt 与 Thomas 讨论更广泛的工作习惯;Knuth 的《Literate Programming》研究怎样共同组织解释与代码。
| 书籍 | 作用 | 类型 |
|---|---|---|
| Kernighan & Pike,The Practice of Programming | 风格、接口、测试与调试 | 实践 |
| Ousterhout,A Philosophy of Software Design(第 2 版) | 复杂性与深模块 | 深入 |
| McConnell,Code Complete(第 2 版) | 百科式构造参考 | 参考 |
| Martin,Clean Code | 广为阅读的代码技艺手册;应批判性阅读 | 入门 |
| Hunt & Thomas,The Pragmatic Programmer(20 周年版) | 工作程序员的技艺与心态 | 入门 |
| Knuth,Literate Programming | 把代码视为沟通的奠基性愿景 | 深入 |
演讲与文章
Hickey 的 Simple Made Easy 区分熟悉程度与结构简单性。Hoare 在获得 1980 年图灵奖后的讲演 The Emperor’s Old Clothes 于 1981 年发表,回顾语言设计与实现中的失败。可以结合具体设计检查两者的论点,不应把其中的句子当作能够解决所有取舍的口号。
| 资源 | 平台 | 类型 |
|---|---|---|
| Hickey,“Simple Made Easy”(免费) | InfoQ | 深入 |
| Hoare,The Emperor’s Old Clothes(1981 年发表) | ACM | 深入 |
实践、工具与项目
修订一个能够工作的函数,解释其不变量或约定,并请另一位读者检查。从行为与理解难度两方面比较前后版本。对生成代码也做同样练习:找到假设,推导边界案例,只保留能够解释其后果的修改。
| 资源 | 平台 | 类型 |
|---|---|---|
| 代码能工作之后,为清晰性修改 | 本地实践 | 实践 |
| 批判性阅读优秀代码(§8.3) | 开源 / 标准库 | 实践 |
| 批判性审查 AI 生成代码 | 本地实践 | 实践 |
陷阱
| 误区 | 为什么会误导 | 更好的做法 |
|---|---|---|
| 把可读性等同于格式 | 排版不能解释状态、影响和约定。 | 明确重要假设与不变量。 |
| 使用统一的函数长度上限 | 碎片化可能掩盖一个完整操作。 | 根据依赖和读者需要的解释判断。 |
| 把每处相似代码都抽象出来 | 相似文本可能表达独立变化的规则。 | 共用抽象前先比较职责。 |
| 把行数少当作简单的定义 | 密集表达式可能隐藏状态和边界。 | 优先选择能在局部检查的行为。 |
| 认为状态合法就意味着转换合法 | 表示方式可以排除错误组合,却仍允许错误操作。 | 定义并检查转换规则。 |
| 因生成代码通过测试就批准它 | 代码与测试可能共享错误假设。 | 独立从需求推导预期。 |