English
Writing code that works is the minimum; writing code that other people — including your future self — can read, understand, modify, and trust is the craft. The distinction is the whole subject. A program that produces correct output but is structured so that no one can safely change it is a liability, not an asset, because software is changed far more than it is written once and abandoned. The code that endures is read dozens of times for every time it is written, modified by people who did not write it, and depended upon by systems whose authors never saw it. Craftsmanship is the set of habits and judgments that make code serve these uses well: clear naming, appropriate decomposition, honest abstraction, and the relentless pursuit of simplicity over cleverness.
The defining principle is that code is written for human readers, not for the machine. The machine does not care whether a variable is named x or customerAccountBalance, whether a function is three lines or three hundred, whether the logic is clear or tangled — the machine executes either equally. The reader cares about all of these, and the reader is who the code is actually for, because the reader is who will maintain it. This reframing — from “make the machine do the thing” to “communicate to the reader what the machine does and why” — is the conceptual shift that separates writing code that works from writing code that is good. Donald Knuth captured it in the idea of literate programming: a program should be regarded as a piece of literature addressed to human beings, with the instructions to the computer a secondary product.
This section addresses the daily craft of writing code: the local decisions, made constantly, that accumulate into a codebase that is either a pleasure or a misery to work in. It is distinct from the larger-scale concerns of architecture (§7.1), which is about the structure of systems, and from the disciplines of testing (§7.2) and refactoring (§7.5), which are their own subjects. And it must now address a development that has changed the activity of writing code more in a few years than anything in the preceding decades: AI assistance, which has shifted much of code writing from typing every line to directing and reviewing generated code — a shift that makes the craft of judgment more important even as it makes the craft of typing less so.
Prerequisites: Programming (§2.1) — fluency in writing code. Reading code (§8.3) — you write better when you have read widely and felt the cost of poor readability. Testing (§7.2) and refactoring (§7.5) are companion disciplines.
The Shape of the Skill: Communicating Through Code
The craft of writing code is, at its core, the craft of communication under a hard constraint: the communication must also be a correct, executable program. This dual nature — code is simultaneously an instruction to a machine and a message to a reader — is what makes the craft distinctive and what most of its principles serve.
The novice writes code that works and considers the job done. The experienced practitioner knows that working is necessary but not sufficient, and that the larger part of the code’s life is what happens after it works: the reading, the modifying, the debugging, the extending, done by people (often including the author, months later, having forgotten the context) who must understand the code to work with it. The practitioner therefore writes for those people. Every naming choice, every decision about how to decompose a computation into functions, every choice about what to make explicit and what to leave implicit, is a communication decision: will this help the reader understand, or will it obstruct them?
The principles of the craft are mostly elaborations of “communicate clearly to the reader.” Names should say what a thing is or does, precisely and without requiring the reader to look elsewhere: a well-named variable or function lets the reader understand its role without tracing its definition. Functions should be small enough and focused enough that the reader can hold what they do in mind at once, and should do one thing so that their name can honestly describe them. Abstractions should hide what the reader does not need to know and expose what they do, at a boundary that matches how the reader thinks about the problem. Comments should explain what the code cannot — the why, the context, the non-obvious constraint — rather than restating what the code already says. Each principle is a different facet of the same underlying aim: reduce the effort the reader must spend to understand the code.
Simplicity is the master virtue, and it is harder than it looks because it competes with cleverness, which is seductive. Clever code — code that solves the problem in a surprising, compact, intricate way — is satisfying to write and is usually a mistake, because it is hard to read, hard to modify, and hard to debug. The clever solution that the author understood perfectly while writing it becomes opaque to the next reader, including the author six months later. The pursuit of simplicity — solving the problem in the most straightforward way that works, resisting the temptation to be clever, removing everything that is not needed — is the discipline that produces code that endures. “Simple” does not mean “easy to write”; the simple solution is often harder to find than a clever one, because it requires understanding the problem well enough to solve it without intricacy. Tony Hoare’s observation captures the stakes: there are two ways to write code, one so simple there are obviously no deficiencies, and one so complex there are no obvious deficiencies.
There is little history specific to the craft of writing code, but the trajectory of the field’s understanding is worth noting because it bears on what is current. The early decades treated programming as primarily about making the machine do the thing, with readability a secondary concern; the structured programming movement (Dijkstra, 1968) was partly an argument that how code is written affects whether it can be reasoned about. The software craftsmanship literature of the 1990s and 2000s — Kent Beck, Martin Fowler, Robert Martin, the agile and XP movements — elevated readability, simplicity, and maintainability to first-class concerns and produced the vocabulary (code smells, refactoring, clean code) that the field now uses. The most recent and most disruptive development is AI code generation, which has changed not the principles of good code but the activity of producing it, shifting the human’s role from writing every line to specifying intent and reviewing generated output — a shift whose implications for the craft are still being worked out and are addressed throughout this section.
Naming, Decomposition, Simplicity, and the AI Shift
Naming and the Communication of Intent
Naming is the most frequent craft decision and one of the most consequential. Every variable, function, class, and module is named, and the names are the primary vocabulary through which the code communicates. A codebase with good names reads almost like prose; a codebase with poor names requires constant cross-referencing to understand what anything is.
A good name says what a thing is or does, at the right level of precision, without misleading. getUserById says what the function does; process does not. remainingRetries says what the number is; count does not. isEligible reads as the boolean it is; flag does not. The cost of a vague name is paid every time the reader encounters it and must reconstruct, from context or from the definition, what it actually means; the cost of a good name is paid once, by the writer, in the moment of choosing it. Since names are read far more than they are written, the trade strongly favors investing in the name.
Names should also avoid misleading. A name that suggests something the thing does not do is worse than a vague name, because it actively misdirects the reader. A variable named userList that is actually a set, a function named validate that also modifies its input, a temporary value that persists — each misleads, and the reader who trusts the name is led to a wrong understanding. The discipline is to make names honest: the name should match what the thing actually is and does, and when the thing changes, the name should change with it. Renaming as understanding improves — using the editor’s rename refactoring, which updates every reference — is part of the craft, not a sign of having named wrongly the first time.
The hardest naming is at the level of concepts: finding the right name for an abstraction, a module, a domain concept, is finding the right way to think about it. Eric Evans’s ubiquitous language (§7.1) is the disciplined version of this: developing a shared, precise vocabulary for the domain, used consistently in the code and in conversation, so that the names in the code match the concepts the team reasons with. Good names at this level are not just communication aids but evidence that the underlying concept has been understood clearly; the struggle to name something well is often the struggle to understand it well.
Decomposition and Abstraction
How a computation is decomposed into functions, and how functions are grouped into modules, determines how readable and modifiable the code is. The principles — small functions, single responsibility, appropriate abstraction boundaries — all serve the reader’s ability to understand a piece in isolation and to understand how the pieces fit together.
A function should do one thing and do it at one level of abstraction. A function that does one thing can be named honestly for that thing, can be understood without tracing into its details, and can be modified or replaced without affecting unrelated behavior. A function that does several things resists honest naming (its name either lies by omission or becomes a conjunction), forces the reader to understand all of it to understand any of it, and entangles concerns that should be separable. The discipline of extracting a well-named function for each distinct thing — which the refactoring catalog (§7.5) calls Extract Function — is the most common craft operation and one of the most valuable.
The level-of-abstraction principle is subtler and equally important: within a function, the operations should be at a consistent level of abstraction, so that the function reads as a coherent description at one level rather than mixing high-level intent with low-level detail. A function that interleaves a high-level algorithm with the low-level mechanics of each step is hard to read because the reader must constantly shift levels. Extracting the low-level mechanics into named helper functions lets the high-level function read as a clear statement of the algorithm, with the details available but not intrusive. This is what makes well-decomposed code readable top-down: each level can be understood as a composition of the level below, named clearly enough that the reader can defer the details.
Abstraction is the tool for managing complexity, and its discipline is to hide what the reader does not need while exposing what they do, at a boundary that matches the problem. A good abstraction lets the reader use it without understanding its implementation — the interface is enough — and this is what allows a large system to be understood in parts. A bad abstraction leaks: it requires the reader to understand its implementation to use it correctly, which defeats the purpose. The craft of abstraction is choosing boundaries that genuinely encapsulate — that hide a decision that can change without affecting users, in Parnas’s sense (§7.1) — rather than boundaries that merely divide the code without hiding anything. Over-abstraction is its own failure: abstractions introduced before they are needed, layers that add indirection without hiding meaningful complexity, generality that serves hypothetical future needs at the cost of present clarity. The right amount of abstraction is the amount that manages the complexity actually present, no more.
Simplicity and the Resistance to Cleverness
Simplicity is the virtue that the other principles serve and the one that is hardest to maintain, because the pressures push toward complexity: the temptation of clever solutions, the accretion of special cases, the addition of features and configurations, the reluctance to remove anything. Maintaining simplicity is an active discipline, not a default state.
The pursuit of simplicity has several specific practices. Solve the problem you actually have, not the more general problem you imagine you might have — YAGNI (“you aren’t gonna need it”) is the discipline of resisting speculative generality, building for the present requirement and adding generality only when a real need for it appears. Remove what is not needed — dead code, unused parameters, abstractions that no longer earn their place, configurations no one uses — because every element that exists must be understood by readers and maintained by modifiers, and the elements that serve no purpose are pure cost. Prefer the straightforward solution over the clever one, even when the clever one is more satisfying to write, because the straightforward solution is the one the next reader can understand and modify safely.
The deepest version of simplicity is the distinction Rich Hickey drew between “simple” and “easy”: simple means not intertwined, having one role, being composed of separable concerns; easy means familiar, near at hand, requiring little effort. The two are often confused, and the confusion produces systems that are easy to start (familiar tools, quick to write) but complex (intertwined concerns, tangled dependencies) and therefore hard to maintain. The pursuit of genuine simplicity — disentangling concerns so that each part has one role and can be understood and changed independently — sometimes requires more initial effort than the easy path, and pays off over the system’s life in the ability to reason about and change its parts independently.
The AI Shift: From Writing to Directing and Reviewing
AI code generation has changed the activity of writing code more fundamentally in a few years than anything in the preceding decades. A developer working with a capable AI assistant no longer types every line; they specify intent — in a comment, a function signature, a natural-language description — and the AI generates code, which the developer then reviews, accepts, modifies, or rejects. For larger tasks, AI agents generate multi-file changes from a description, shifting the developer’s role further toward specification and review. This is a genuine change in what writing code consists of, and it changes which parts of the craft matter most.
The principles of good code are unchanged by AI generation. Code still needs clear names, good decomposition, honest abstraction, and simplicity, whether a human or an AI produced the characters. What changes is where the human’s craft is applied: less in the typing, more in the judgment. The developer’s essential contributions become specifying clearly what is wanted (which requires understanding the problem well enough to describe it precisely), and reviewing critically what is generated (which requires the judgment to recognize good code from bad, correct from subtly incorrect, simple from needlessly complex). Both of these are craft skills, and both are harder to develop than typing — which means the AI shift, paradoxically, raises the premium on the deepest parts of the craft (judgment, taste, the ability to recognize quality) even as it lowers the premium on the mechanical parts (typing, recalling syntax, writing boilerplate).
The specific risks of AI-generated code are the ones the craft must now guard against. AI-generated code can be subtly incorrect in ways that pass casual inspection — using an API slightly wrong, handling an edge case incorrectly, introducing a security vulnerability — because the generated code is fluent and plausible whether or not it is correct. It tends toward a certain genericness, producing the common solution rather than the one fitted to the specific context, which can be the wrong solution for a specific system’s constraints. It can introduce subtle inconsistencies with the surrounding codebase’s conventions and patterns, because it does not fully share the codebase’s context. And it can produce code that works but that the developer does not understand, which is dangerous because code one does not understand cannot be safely maintained or debugged. The developer remains responsible for the code regardless of who or what produced it, and the craft now includes the judgment to use AI generation where it helps while maintaining the understanding and verification that ensure the result is good.
The risk of skill atrophy is real and specific. A developer who delegates all routine code to AI without engaging with it may lose the fluency that lets them recognize when the AI’s output is wrong — and that recognition is precisely what makes AI assistance safe. The craft skills of naming, decomposition, and simplicity are maintained by exercising them; a developer who stops exercising them because the AI handles the routine cases may find their judgment dulled when a case arises that the AI handles badly. The productive relationship uses AI to handle the genuinely routine while keeping the developer engaged enough — reading the generated code critically, understanding it, improving it — that the craft skills stay sharp. The goal is AI as a force multiplier on a developer who retains the judgment to direct and evaluate it, not AI as a replacement for judgment the developer no longer has.
What Changes With Mastery
Craftsmanship in writing code changes the value of the code a developer produces and the experience of working with it.
The first change is that the developer’s code becomes an asset rather than a liability over time. Code written with craft — clear, simple, well-decomposed, honestly named — can be read, understood, modified, and extended by others and by the author later; it accommodates the changes that software inevitably needs. Code written without craft — tangled, clever, poorly named — resists change, accumulates bugs when modified, and eventually becomes the legacy code that everyone is afraid to touch. The craft determines whether the code the developer writes is something the organization can build on or something it must eventually work around.
The second change is the developer’s effect on the people around them. A developer who writes clear code, and who reviews others’ code with attention to clarity, raises the quality of the codebase they work in and teaches the craft to those they work with. Craft is partly transmitted through example and through the standards applied in review; the craftsman developer improves not just their own code but the codebase and the team. This multiplier effect is part of why craft matters beyond the individual: it shapes the environment.
The third change is the judgment that the AI era makes central. As AI handles more of the mechanical production of code, the developer’s value concentrates in the judgment that AI lacks: knowing what to build, recognizing whether generated code is correct and well-fitted, maintaining the simplicity and coherence that AI does not reliably produce, and understanding the system deeply enough to direct AI effectively and to step in when AI fails. The developer who has cultivated this judgment is more valuable in the AI era, not less, because the judgment is exactly what does not automate.
The fourth change is a kind of aesthetic discomfort with bad code that drives improvement. The developer who has internalized the craft feels the friction of poor names, tangled logic, and needless complexity — they experience badly written code as actively unpleasant, the way a writer experiences bad prose. This discomfort is productive: it motivates the continuous small improvements, the renaming and refactoring and simplification, that keep a codebase healthy. The developer who feels this discomfort leaves code better than they found it, not from duty but from an acquired sense of how code should be.
Resources
Books and Texts
Robert Martin’s Clean Code: A Handbook of Agile Software Craftsmanship (Prentice Hall, 2008) is the most widely read book on the daily craft of writing code, covering naming, functions, comments, formatting, error handling, and the qualities that make code clean. It is also genuinely controversial: some of its specific prescriptions (very small functions, particular formatting rules) are contested, and parts have aged unevenly. It is best read critically — engaging with its principles while forming your own judgment about its specific rules — rather than as doctrine. Read this way, it is valuable; read as commandments, it can produce its own kind of dogmatism.
Kernighan and Pike’s The Practice of Programming (Addison-Wesley, 1999) is the more durable treatment, by two masters of the craft, covering style, design, debugging, testing, and performance with a concision and judgment that Clean Code lacks. Its chapter on style is the best short treatment of writing readable code. It is shorter, less dogmatic, and ages better; for many readers it is the better single book.
McConnell’s Code Complete (2nd ed., Microsoft Press, 2004) is the encyclopedic treatment of construction — the broadest and most thorough coverage of the decisions involved in writing code, from variable naming through control structures through defensive programming. It is long and can be used as a reference rather than read straight through, but its coverage is unmatched and grounded in empirical software engineering research where evidence exists.
Ousterhout’s A Philosophy of Software Design (2nd ed., Yaknyam Press, 2021) is the best contemporary treatment of the central craft question of managing complexity through good decomposition and deep abstractions. Its concept of “deep modules” (modules that provide powerful functionality through simple interfaces) and its treatment of complexity as the fundamental enemy are clarifying. It is short, opinionated, and in productive tension with some of Clean Code’s prescriptions — reading both and weighing their disagreements is itself instructive.
Hunt and Thomas’s The Pragmatic Programmer (20th Anniversary Edition, Addison-Wesley, 2019) covers the broader craft and mindset of the working programmer — including writing code but extending to the habits, attitudes, and practices of effective developers. Its principles (DRY, orthogonality, tracer bullets, and many others) have entered the field’s common vocabulary.
| Book | Role | Type |
|---|---|---|
| Kernighan & Pike, The Practice of Programming | Durable, concise craft treatment | 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
Rich Hickey’s talk “Simple Made Easy” (2011, free on InfoQ) is the clearest articulation of the distinction between simple (not intertwined) and easy (familiar, near at hand) and why conflating them produces complex systems. It is among the most influential talks in software and is worth watching more than once; its vocabulary has entered the field’s discourse.
Tony Hoare’s 1980 Turing Award lecture “The Emperor’s Old Clothes” (free) contains the observation about the two ways of writing code — one so simple there are obviously no deficiencies, one so complex there are no obvious deficiencies — and reflects on the value of simplicity by one of the field’s foundational figures.
| Resource | Platform | Type |
|---|---|---|
| Hickey, “Simple Made Easy” (free) | InfoQ | Depth |
| Hoare, “The Emperor’s Old Clothes” (1980, free) | ACM | Depth |
Practice, Tools, and Projects
The craft is developed by writing code with attention to its quality, by reading excellent code (§8.3), and by having code reviewed by people with higher standards (§8.5). The single most effective practice is the discipline of revising code for clarity after it works: once the code produces correct output, read it as the next maintainer would, and improve the names, the decomposition, and the simplicity. This separation of “make it work” from “make it good” — first solve the problem, then improve the solution — is how craft is applied in practice, and the second step is where the craft is exercised.
Reading excellent code teaches craft directly: the standard library of a well-designed language, widely-respected open-source projects, and the code of programmers known for quality demonstrate the craft in practice. Reading code critically — asking why it is structured as it is, what makes it clear or unclear, how it could be improved — develops the judgment that the craft requires.
In the AI era, a specific high-value practice is reviewing AI-generated code critically: treating the AI’s output as a draft to be evaluated and improved, reading it as carefully as you would review a colleague’s code, and developing the judgment to recognize where it is good and where it falls short. This practice maintains the craft skills that AI generation could otherwise allow to atrophy, and it is the skill that the AI era makes central.
| 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 |
|---|---|---|
| Cleverness over clarity | Clever code — compact, surprising, intricate — is satisfying to write and is usually a mistake. The cleverness that the author understood while writing it becomes opaque to the next reader, including the author later, making the code hard to read, modify, and debug. Cleverness optimizes for the moment of writing; clarity optimizes for the much longer period of reading and maintaining. | Prefer the straightforward solution over the clever one, even when the clever one is more satisfying. When tempted by a clever solution, ask whether the next reader will understand it, and whether the cleverness buys anything worth the cost in clarity. Reserve cleverness for the rare cases where it is genuinely necessary (a real performance constraint, for instance) and document it when used. |
| Premature abstraction | Introducing abstractions before they are needed — building general mechanisms for hypothetical future requirements, adding layers of indirection that do not hide meaningful complexity, generalizing from a single case — adds complexity that serves no present need and often turns out to be the wrong generalization when the future need actually arrives. Speculative abstraction is a common and costly form of over-engineering. | Apply YAGNI: build for the requirement you actually have, and add abstraction when a real, present need for it appears — typically when you have two or three concrete cases that reveal what the right abstraction is. Abstractions derived from real cases are usually right; abstractions imagined in advance are usually wrong. The rule of three (abstract when you have three instances) is a useful heuristic. |
| Comments that restate the code | Comments that describe what the code does — // increment i above i++ — add noise without information, go stale when the code changes, and train readers to ignore comments. They reflect a misunderstanding of what comments are for. |
Comment the why, not the what. The code says what it does; comments should explain what the code cannot — the reason for a non-obvious decision, the constraint being satisfied, the edge case being handled, the context that makes the code make sense. If a comment restates the code, either delete it or, if the code needed the comment to be understood, improve the code (better names, clearer structure) so the comment is unnecessary. |
| Confusing easy with simple | Reaching for the familiar, quick-to-write solution (easy) without considering whether it is simple (not intertwined, separable concerns) produces systems that start fast and become hard to maintain as the intertwined concerns resist independent change. The confusion is invisible at the start, when easy feels productive, and becomes painful later, when the complexity that was accepted for ease must be paid down. | Distinguish easy from simple, in Hickey’s sense. When choosing an approach, ask not only “which is quickest to write?” but “which keeps concerns separable, so the parts can be understood and changed independently?” The simple approach sometimes costs more up front and pays off over the system’s life. Watch especially for tools and patterns that are easy (familiar) but produce complexity (intertwining). |
| Accepting AI-generated code without understanding it | AI-generated code is fluent and plausible whether or not it is correct, well-fitted to the context, or consistent with the codebase’s conventions. A developer who accepts it without understanding it ships code they cannot maintain or debug, may ship subtle bugs or security vulnerabilities, and gradually loses the judgment that would let them recognize the problems. | Treat AI-generated code as a draft requiring review, not as finished work. Read it as critically as you would review a colleague’s code: is it correct, including edge cases? does it fit this system’s constraints and conventions? is it as simple as it should be? do you understand it well enough to maintain it? Only accept code you understand and have judged to be good. The developer remains responsible for the code regardless of what produced it. |
| Letting craft skills atrophy through over-delegation | A developer who delegates all routine code to AI without engaging with it loses the fluency that lets them recognize when AI output is wrong — which is exactly the judgment that makes AI assistance safe. The atrophy is invisible until a case arises that AI handles badly and the developer no longer has the sharpness to catch it. | Stay engaged with the code AI produces: read it critically, understand it, improve it. Continue to write code directly often enough that the craft skills — naming, decomposition, simplicity, the recognition of quality — stay exercised. Use AI as a force multiplier on judgment you retain, not as a substitute for judgment you let lapse. The craft skills are maintained by use; ensure they continue to be used. |
中文
写出能运行的代码,只是最低要求;写出别人——包括未来的自己——能够阅读、理解、修改并信任的代码,才是技艺。这个区别就是本节的全部主题。一个程序如果能够产生正确输出,却被组织得让任何人都无法安全修改,那么它不是资产,而是负债,因为软件被修改的次数远远多于它被写完后就被永久放置的次数。真正能留存的代码,会在每一次被编写之后被阅读几十次,会被并非原作者的人修改,也会被从未见过它的系统作者所依赖。代码技艺,就是一组让代码能够良好服务于这些用途的习惯和判断:清晰命名、合适分解、诚实抽象,以及持续追求简单而非炫技。
定义性原则是:代码是写给人类读者看的,不是写给机器看的。机器并不在乎一个变量叫 x 还是 customerAccountBalance,也不在乎一个函数是三行还是三百行,更不在乎逻辑是清楚还是纠缠——机器都会照样执行。读者在乎这一切;而代码真正面向的正是读者,因为读者才是要维护它的人。这种重构——从“让机器完成事情”,转向“向读者传达机器做了什么以及为什么这样做”——正是把能运行的代码与好代码区分开的概念转变。Donald Knuth 用 literate programming 的思想捕捉了这一点:程序应当被视为一篇面向人类的文学作品,而给计算机的指令只是次要产物。
本节讨论日常写代码的技艺:那些不断发生的局部决策,会逐渐积累成一个让人愉快或痛苦的代码库。它不同于更大尺度的架构问题(§7.1),后者关心系统结构;也不同于测试(§7.2)和重构(§7.5),它们各自是独立学科。本节还必须处理一个在短短几年内比过去几十年任何变化都更深刻改变了写代码活动的发展:AI 辅助。它已经把许多代码写作从逐行输入,转变为指导和审查生成代码——这种转变让判断力的技艺变得更重要,哪怕打字的技艺变得没那么重要。
前置知识:编程(§2.1)——能够熟练写代码。阅读代码(§8.3)——广泛阅读并感受过可读性差的代价之后,才会写得更好。测试(§7.2)和重构(§7.5)是相伴的学科。
这项能力的形状:通过代码进行沟通
写代码的技艺,核心上是一种在强约束下沟通的技艺:这种沟通同时还必须是一个正确、可执行的程序。这种双重性质——代码既是给机器的指令,也是给读者的信息——正是这项技艺的独特之处,也正是多数原则所服务的对象。
新手写出能工作的代码,就认为任务完成了。有经验的实践者知道,能工作是必要条件,但远远不够;代码生命中更大的部分发生在它能工作之后:阅读、修改、调试、扩展,而这些工作由人来完成。这个人常常包括几个月后的作者本人,那时他已经忘记了原来的语境。实践者因此是在为这些人写代码。每一个命名选择,每一次把计算分解成函数的决定,每一个关于什么显式写出、什么保持隐含的选择,都是沟通决策:这会帮助读者理解,还是会阻碍他们?
这门技艺的原则,大多都是“清楚地向读者沟通”这一原则的展开。名称应该精确地说明一个东西是什么或做什么,不应要求读者去别处查找:一个命名良好的变量或函数,可以让读者不追踪定义就理解它的角色。函数应该足够小、足够聚焦,使读者能够一次性在脑中把握它做什么;并且它应该只做一件事,这样它的名字才能诚实地描述它。抽象应该隐藏读者不需要知道的东西,暴露读者需要知道的东西,并且边界要符合读者对问题的思考方式。注释应该解释代码无法说明的内容——为什么、语境、非显而易见的约束——而不是重复代码已经说出的东西。每一条原则,都是同一个底层目标的不同侧面:减少读者理解代码时必须付出的努力。
简单性是最高德性,而且它比看起来更难,因为它要与炫技竞争,而炫技很诱人。炫技代码——以令人意外、紧凑、复杂的方式解决问题的代码——写起来很满足,但通常是错误选择,因为它难以阅读、难以修改、难以调试。作者写下时完全理解的巧妙解法,会在下一个读者面前变得晦涩,包括六个月后的作者本人。追求简单——用最直接且可行的方式解决问题,抵抗炫技冲动,移除一切不需要的东西——正是让代码能够留存的纪律。“简单”并不等于“容易写”;简单解法往往比巧妙解法更难找到,因为它要求你足够理解问题,才能不依赖复杂性来解决它。Tony Hoare 的观察抓住了其中利害:写代码有两种方式,一种简单到显然没有缺陷,另一种复杂到没有明显缺陷。
关于写代码这项技艺本身,并没有太多独立历史,但这个领域对它的理解轨迹值得指出,因为它关系到当下。早期几十年,编程主要被看作让机器完成任务,可读性只是次要问题;结构化编程运动(Dijkstra,1968)部分是在论证:代码如何被书写,会影响它是否能被推理。1990 年代和 2000 年代的软件工艺文献——Kent Beck、Martin Fowler、Robert Martin,以及敏捷和 XP 运动——把可读性、简单性和可维护性提升为一等关切,并创造出今天这个领域使用的词汇,例如 code smells、refactoring、clean code。最新也最具颠覆性的发展是 AI 代码生成,它改变的不是好代码的原则,而是生产代码的活动:人的角色从写下每一行,转向说明意图并审查生成结果。这种转变对技艺意味着什么,仍在被展开;本节会贯穿讨论。
命名、分解、简单性与 AI 转变
命名与意图沟通
命名是最频繁的技艺决策,也是最有后果的决策之一。每个变量、函数、类和模块都要被命名,而名称是代码沟通的主要词汇。名称良好的代码库,读起来几乎像散文;名称糟糕的代码库,则要求读者不断交叉查找,才能理解任何东西是什么。
一个好名称会以合适的精确度说明一个东西是什么或做什么,而且不会误导。getUserById 说明函数做什么;process 不说明。remainingRetries 说明这个数字是什么;count 不说明。isEligible 读起来就像它实际所是的布尔值;flag 不说明。模糊名称的成本会在读者每次遇到它时被支付:读者必须从语境或定义中重建它到底是什么意思。好名称的成本只由作者支付一次,也就是选择名称的那一刻。由于名称被阅读的次数远远多于被书写的次数,这个取舍强烈支持在命名上投入精力。
名称也应该避免误导。一个暗示某物会做某件事、但实际上并不会做的名称,比模糊名称更糟,因为它会主动把读者引向错误。一个叫 userList 但实际上是集合的变量,一个叫 validate 但同时会修改输入的函数,一个叫 temporary 但会持久存在的值,都会误导。信任名称的读者会被带入错误理解。纪律是让名称诚实:名称应该匹配这个东西实际是什么、做什么;当东西变化时,名称也应该随之变化。随着理解加深而重命名——使用编辑器的 rename refactoring 自动更新所有引用——是技艺的一部分,而不是第一次命名错误的耻辱。
最困难的命名发生在概念层面:为一个抽象、一个模块、一个领域概念找到合适名称,就是找到一种合适的思考方式。Eric Evans 的 ubiquitous language(§7.1)是这种实践的纪律化版本:为领域发展一套共享而精确的词汇,并在代码和对话中一致使用,使代码中的名称与团队推理时使用的概念相匹配。这个层面的好名称不仅是沟通辅助,也证明底层概念已经被清楚理解;努力给某物命名,往往就是努力理解它。
分解与抽象
如何把一段计算分解成函数,如何把函数组织成模块,决定了代码是否可读、是否可修改。相关原则——小函数、单一职责、合适的抽象边界——都服务于读者的能力:能够孤立理解某一部分,也能够理解各部分如何组合在一起。
一个函数应该只做一件事,并且在一个抽象层级上做这件事。只做一件事的函数,可以用诚实的名称描述,可以在不追踪细节的情况下被理解,也可以在不影响无关行为的情况下被修改或替换。一个做很多事情的函数会抗拒诚实命名:它的名字要么因为省略而撒谎,要么变成一个冗长的并列短语;它会迫使读者为了理解其中任何一部分而理解全部;也会把本应分开的关切纠缠在一起。为每个独立事项抽取一个命名良好的函数——重构目录(§7.5)称之为 Extract Function——是最常见也最有价值的技艺操作之一。
抽象层级原则更微妙,也同样重要:在一个函数内部,操作应该处于一致的抽象层级,使函数读起来像是在一个层级上进行连贯描述,而不是把高层意图和低层细节混在一起。一个把高层算法与每一步低层机制交错写在一起的函数很难阅读,因为读者必须不断切换层级。把低层机制抽取到命名良好的辅助函数中,可以让高层函数读起来像对算法的清楚陈述;细节仍然可用,但不会侵入。这正是良好分解的代码能够自顶向下阅读的原因:每一层都可以被理解为下一层的组合,而名称足够清楚,使读者可以暂时推迟细节。
抽象是管理复杂性的工具,其纪律是:在符合问题的边界上,隐藏读者不需要知道的东西,暴露他们需要知道的东西。好抽象让读者不需要理解实现也能使用它——接口已经足够——而这正是大型系统能够被分部分理解的原因。坏抽象会泄漏:它要求读者理解其实现,才能正确使用它,从而违背了抽象的目的。抽象的技艺在于选择真正能够封装的边界——用 Parnas 的说法(§7.1),隐藏一个可以改变而不影响使用者的决策——而不是只是把代码切开,却什么都没有隐藏。过度抽象本身也是失败:在需要之前引入抽象;增加只带来间接性、却不隐藏有意义复杂性的层;为了假想的未来需求而追求通用性,却牺牲当前清晰性。合适的抽象量,就是足以管理已经存在的复杂性的量,不多不少。
简单性与抵抗炫技
简单性是其他原则所服务的德性,也是最难维持的德性,因为压力总是推向复杂:巧妙解法的诱惑、特殊情况的累积、功能和配置的增加、对删除任何东西的犹豫。维持简单性是一种主动纪律,不是默认状态。
追求简单性有几种具体实践。解决你实际拥有的问题,而不是你想象中将来也许会拥有的更一般问题——YAGNI(you aren’t gonna need it)就是抵抗推测性通用性的纪律:为当前需求构建,只有当真实需求出现时才添加通用性。移除不需要的东西——死代码、未使用参数、已经不再配得上存在的抽象、没人使用的配置——因为每一个存在的元素,都必须被读者理解,也必须被修改者维护;没有目的的元素就是纯成本。优先选择直接解法,而不是巧妙解法,即使巧妙解法写起来更令人满足,因为直接解法才是下一个读者能够理解并安全修改的解法。
简单性的最深版本,是 Rich Hickey 所区分的 “simple” 与 “easy”。simple 意味着不纠缠、只有一个角色、由可分离的关切组成;easy 意味着熟悉、触手可及、不费力。二者常被混淆,而这种混淆会产生一些起步容易、但内部复杂的系统:熟悉工具、快速写成,却包含纠缠的关切和混乱依赖,因此难以维护。追求真正的简单性——拆开关切,使每一部分拥有单一角色,并能被独立理解和修改——有时比走容易路径需要更多初始努力,但会在系统整个生命中回报为对各部分进行独立推理和修改的能力。
AI 转变:从编写到指导和审查
AI 代码生成在短短几年中,对写代码这项活动的改变,比此前几十年任何东西都更根本。使用强大 AI 助手的开发者,不再输入每一行代码;他们说明意图——通过注释、函数签名、自然语言描述——然后 AI 生成代码,开发者再审查、接受、修改或拒绝。对于更大的任务,AI 代理可以根据描述生成跨文件修改,从而进一步把开发者的角色推向规约和审查。这确实改变了写代码意味着什么,也改变了这门技艺中最重要的部分。
好代码的原则并不会因为 AI 生成而改变。无论字符由人类还是 AI 产生,代码仍然需要清晰名称、良好分解、诚实抽象和简单性。改变的是人类技艺施加的位置:更少体现在打字上,更多体现在判断上。开发者的核心贡献变成两点:清楚说明自己想要什么,这要求足够理解问题,才能精确描述它;批判性审查生成结果,这要求能够判断好代码与坏代码、正确与细微错误、简单与不必要复杂之间的差别。这两者都是技艺能力,而且都比打字更难培养——因此,AI 转变反而提高了最深层技艺的溢价,例如判断力、品味、识别质量的能力;同时降低了机械部分的溢价,例如打字、记忆语法、编写样板代码。
AI 生成代码的具体风险,正是这门技艺如今必须防范的东西。AI 生成代码可能以能通过随意检查的方式细微错误——API 使用略有不当、边界情况处理错误、引入安全漏洞——因为生成代码无论正确与否都可以流畅而貌似合理。它往往带有某种通用性,生成常见解法,而不是适合具体语境的解法;对于某个具体系统的约束来说,常见解法可能就是错误解法。它可能引入与周围代码库约定和模式之间的细微不一致,因为它并不完全共享代码库语境。它也可能生成能工作、但开发者并不理解的代码,而这是危险的,因为自己不理解的代码无法被安全维护或调试。无论代码由谁或什么生成,开发者仍然要对代码负责;而如今的代码技艺包括一种判断:在 AI 生成有帮助的地方使用它,同时维持理解和验证,确保结果真正是好的。
技能萎缩的风险是真实而具体的。一个把所有常规代码都委托给 AI、却不参与其中的开发者,可能会失去识别 AI 输出何时错误的流利度——而这种识别能力正是安全使用 AI 辅助的前提。命名、分解和简单性的技艺,是通过练习维持的;如果开发者因为 AI 处理了常规情况而停止练习这些能力,那么当出现 AI 处理不佳的情况时,他可能会发现自己的判断已经迟钝。有效关系是:让 AI 处理真正常规的部分,同时让开发者保持足够参与——批判性阅读生成代码,理解它,改进它——使技艺能力保持锋利。目标是让 AI 成为保留判断力的开发者的能力倍增器,而不是替代开发者已经不再拥有的判断力。
熟练之后会发生什么变化
写代码的工艺,会改变开发者产出代码的价值,也会改变与这些代码共事的体验。
第一种变化,是开发者的代码会随着时间成为资产,而不是负债。带着技艺写出的代码——清晰、简单、分解良好、命名诚实——可以被他人以及后来的作者本人阅读、理解、修改和扩展;它能容纳软件不可避免要经历的变化。不带技艺写出的代码——纠缠、炫技、命名糟糕——会抵抗变化,在被修改时积累 bug,并最终变成所有人都害怕触碰的遗留代码。技艺决定了开发者写出的代码是组织可以继续建设的东西,还是最终不得不绕开的东西。
第二种变化,是开发者对周围人的影响。一个写清晰代码,并在审查他人代码时关注清晰性的开发者,会提高自己所在代码库的质量,也会把这门技艺教给同事。技艺部分是通过示范和代码审查中施加的标准来传递的;有工艺意识的开发者改善的不只是自己的代码,也包括代码库和团队。这种乘数效应,是技艺超出个人层面的重要原因:它会塑造环境。
第三种变化,是 AI 时代使之变得核心的判断力。随着 AI 处理越来越多机械性的代码生产,开发者的价值会集中在 AI 缺少的判断上:知道应该构建什么,识别生成代码是否正确并适合当前语境,维持 AI 不可靠地产生的简单性和一致性,足够深入理解系统以便有效指导 AI,并在 AI 失败时介入。培养了这种判断的开发者,在 AI 时代更有价值,而不是更不有价值,因为判断正是无法自动化的东西。
第四种变化,是对糟糕代码产生一种审美性不适,而这种不适会推动改进。内化了代码技艺的开发者,会感受到糟糕命名、纠缠逻辑和不必要复杂性带来的摩擦——他们会像作家面对烂散文一样,把糟糕代码体验为主动的不愉快。这种不适是有生产力的:它会推动持续的小改进,推动重命名、重构和简化,从而让代码库保持健康。有这种不适感的开发者,会让代码比他们发现它时更好;不是出于义务,而是出于一种已经习得的代码应当如何的感觉。
资源
书籍与文本
Robert Martin 的 Clean Code: A Handbook of Agile Software Craftsmanship(Prentice Hall,2008)是关于日常写代码技艺最广为阅读的书,覆盖命名、函数、注释、格式、错误处理,以及让代码干净的品质。它也确实有争议:其中一些具体规定,例如非常小的函数、特定格式规则,存在争议;部分内容也有不同程度的过时。最好的读法是批判性阅读——吸收其原则,同时对具体规则形成自己的判断——而不是把它当成教条。这样读,它有价值;把它当成戒律来读,则可能产生另一种教条主义。
Kernighan 和 Pike 的 The Practice of Programming(Addison-Wesley,1999)是更耐久的处理,由两位工艺大师写成,以简洁和判断力讨论风格、设计、调试、测试和性能,而这正是 Clean Code 相对欠缺的地方。它关于风格的一章,是关于写可读代码最好的短篇处理。它更短,更少教条,也更经得起时间考验;对许多读者来说,它是更好的单本书。
McConnell 的 Code Complete(第 2 版,Microsoft Press,2004)是关于构造的百科式处理——从变量命名,到控制结构,再到防御式编程,它对写代码涉及的决策提供了最广、最彻底的覆盖。它很长,可以作为参考书使用,而不必从头读到尾;但它的覆盖范围无可匹敌,并且在有证据的地方扎根于经验软件工程研究。
Ousterhout 的 A Philosophy of Software Design(第 2 版,Yaknyam Press,2021)是当代关于通过良好分解和深模块管理复杂性的最佳处理。它提出的 “deep modules” 概念——通过简单接口提供强大功能的模块——以及把复杂性视为根本敌人的处理,非常澄清问题。这本书很短,有鲜明立场,并且与 Clean Code 的一些规定形成有益张力;把二者一起读,并权衡它们的分歧,本身就很有教育意义。
Hunt 和 Thomas 的 The Pragmatic Programmer(20 周年纪念版,Addison-Wesley,2019)覆盖工作程序员更广泛的技艺和心态——包括写代码,但也延伸到有效开发者的习惯、态度和实践。它的原则,例如 DRY、orthogonality、tracer bullets 等,已经进入这个领域的共同词汇。
| 书籍 | 作用 | 类型 |
|---|---|---|
| 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 | 把代码视为沟通的奠基性愿景 | 深入 |
演讲与文章
Rich Hickey 的演讲 “Simple Made Easy”(2011,可在 InfoQ 免费观看)是对 simple(不纠缠)与 easy(熟悉、触手可及)区别最清楚的阐述,也说明了为什么混淆二者会产生复杂系统。这是软件领域最有影响力的演讲之一,值得反复观看;其中词汇已经进入该领域的话语中。
Tony Hoare 的 1980 年图灵奖演讲 “The Emperor’s Old Clothes”(免费)包含那条关于两种写代码方式的观察:一种简单到显然没有缺陷,一种复杂到没有明显缺陷。它由这个领域的基础人物之一反思简单性的价值。
| 资源 | 平台 | 类型 |
|---|---|---|
| Hickey,“Simple Made Easy”(免费) | InfoQ | 深入 |
| Hoare,“The Emperor’s Old Clothes”(1980,免费) | ACM | 深入 |
实践、工具与项目
这门技艺通过带着质量意识写代码、阅读优秀代码(§8.3),以及让标准更高的人审查代码(§8.5)来发展。最有效的单一实践,是在代码能工作之后,为清晰性修改它:一旦代码产生正确输出,就像下一位维护者那样阅读它,并改进名称、分解和简单性。这种把“让它工作”和“让它变好”分开的做法——先解决问题,再改进解法——正是代码技艺在实践中的应用方式,而第二步才是技艺真正被锻炼的地方。
阅读优秀代码会直接教授技艺:设计良好语言的标准库、广受尊敬的开源项目,以及以代码质量闻名的程序员所写的代码,都会在实践中展示这门技艺。批判性阅读代码——追问它为什么这样组织,什么让它清楚或不清楚,它还能如何改进——会发展这门技艺所需的判断力。
在 AI 时代,一项高价值的具体实践是批判性审查 AI 生成代码:把 AI 输出当成需要评估和改进的草稿,像审查同事代码一样仔细阅读它,并发展判断力,识别它哪里好、哪里不足。这种实践会维持 AI 生成原本可能导致萎缩的代码技艺能力,而这种能力正是 AI 时代最核心的能力。
| 资源 | 平台 | 类型 |
|---|---|---|
| 代码能工作之后,为清晰性修改 | 本地实践 | 实践 |
| 批判性阅读优秀代码(§8.3) | 开源 / 标准库 | 实践 |
| 批判性审查 AI 生成代码 | 本地实践 | 实践 |
陷阱
| 陷阱 | 为什么会误导 | 更好的回应 |
|---|---|---|
| 炫技压过清晰 | 炫技代码——紧凑、出人意料、复杂精巧——写起来很满足,但通常是错误选择。作者写下时理解的巧妙性,会在下一个读者面前变得晦涩,包括后来的作者本人,使代码难以阅读、修改和调试。炫技优化的是写下代码的那一刻;清晰优化的是更漫长的阅读和维护期。 | 优先选择直接解法,而不是巧妙解法,即使巧妙解法写起来更令人满足。当你被巧妙解法吸引时,问自己:下一个读者能否理解它?这种巧妙是否带来了值得牺牲清晰性的收益?只在极少数真正必要的地方使用巧妙性,例如真实性能约束,并在使用时说明原因。 |
| 过早抽象 | 在需要之前引入抽象——为假想未来需求构建通用机制,添加并不隐藏有意义复杂性的间接层,从单个案例中泛化——会增加没有当前用途的复杂性;而当未来需求真正到来时,这种泛化往往会被证明是错误的。推测性抽象是一种常见且代价高昂的过度工程。 | 应用 YAGNI:为实际拥有的需求构建;只有当真实、当前的需求出现时才添加抽象——通常是在你已经有两三个具体案例,能够显示正确抽象是什么的时候。来自真实案例的抽象通常是正确的;事先想象出来的抽象通常是错误的。“三次法则”(有三个实例时再抽象)是一个有用启发。 |
| 重复代码含义的注释 | 描述代码做什么的注释——例如在 i++ 上方写 // increment i——只会增加噪音而不增加信息;当代码变化时,它还会过时,并训练读者忽视注释。这反映了对注释用途的误解。 |
注释为什么,而不是注释做什么。代码说明它做什么;注释应该解释代码无法表达的东西——某个非显而易见决策的原因、正在满足的约束、正在处理的边界情况、让代码成立的语境。如果一条注释只是重复代码,要么删除它;要么如果代码确实需要这条注释才能被理解,就改进代码本身,例如更好的命名、更清楚的结构,让注释变得不必要。 |
| 混淆 easy 与 simple | 选择熟悉、快速能写出的解法(easy),却不考虑它是否简单(simple:不纠缠、关切可分离),会产生一开始很快、后来很难维护的系统,因为纠缠的关切会阻碍独立变化。这种混淆在一开始是不可见的,因为 easy 会让人感觉有生产力;但后来会变得痛苦,因为当初为了容易而接受的复杂性必须被偿还。 | 按 Hickey 的意义区分 easy 和 simple。选择方法时,不只问“哪个最快写出来?”,也问“哪个能保持关切分离,使各部分可以被独立理解和修改?”简单方法有时前期成本更高,但会在系统生命周期中回报。尤其要警惕那些 easy(熟悉)但会制造 complexity(纠缠)的工具和模式。 |
| 不理解就接受 AI 生成代码 | AI 生成代码无论是否正确、是否适合语境、是否与代码库约定一致,都可能流畅而貌似合理。开发者如果不理解就接受它,就会交付自己无法维护或调试的代码,可能交付细微 bug 或安全漏洞,也会逐渐失去识别问题所需的判断力。 | 把 AI 生成代码当成需要审查的草稿,而不是成品。像审查同事代码一样批判性阅读它:它是否正确,包括边界情况?是否适合这个系统的约束和约定?是否足够简单?你是否足够理解它,能够维护它?只接受你理解并判断为好的代码。无论代码由什么产生,开发者仍然负责。 |
| 因过度委托而让技艺萎缩 | 如果开发者把所有常规代码都委托给 AI,而不参与其中,就会失去识别 AI 输出何时错误的流利度——而这正是安全使用 AI 辅助所需的判断。萎缩一开始是不可见的,直到 AI 处理不好某个情况,而开发者已经不再足够敏锐,无法发现。 | 继续参与 AI 产生的代码:批判性阅读、理解、改进。也要保持足够频率直接写代码,使技艺能力——命名、分解、简单性、识别质量——持续被锻炼。把 AI 用作你仍然保有的判断力的倍增器,而不是用它替代你放任衰退的判断力。技艺能力靠使用维持;确保它们仍然被使用。 |