这是一份非常详尽的《代码整洁之道》(Clean Code)第17章的翻译与整理。这一章是全书的精华汇编,列出了作者在职业生涯中总结的“代码异味”(Smells)和“启发式规则”(Heuristics)。
总结与小计 (Summary & Notes)
核心主旨: 本章是作者罗伯特·马丁(Uncle Bob)根据Martin Fowler的《重构》一书以及自己多年的经验,整理出的一份“代码异味”清单。这不仅仅是一组规则,更是用来培养软件“工匠精神”和专业价值观的检查清单。
主要分类:
- 注释 (Comments):强调注释不应包含元数据、废弃信息或冗余内容,被注释掉的代码应立即删除。
- 环境 (Environment):构建和测试都应该是一个步骤即可完成的简单操作。
- 函数 (Functions):参数越少越好,避免输出参数和布尔标识参数,删除死函数。
- 一般性问题 (General):这是最长的一节,涵盖了重复代码(DRY)、抽象层级混杂、过度耦合、特性依恋、死代码、魔术数、意图模糊等核心设计问题。
- Java特性 (Java):关于导入列表、常量继承和枚举的使用建议。
- 命名 (Names):名称应具有描述性、无歧义,并处于正确的抽象层级。
- 测试 (Tests):测试必须覆盖边界条件、快速运行,且不能忽略失败的模式。
学习建议: 这一章可以作为代码审查(Code Review)时的参考手册。当感觉代码“不对劲”时,可以在这里找到具体的理论依据和改进方向。
第 17 章 味道与启发 (Smells and Heuristics)

In his wonderful book Refactoring,1 Martin Fowler identified many different “Code Smells.” The list that follows includes many of Martin’s smells and adds many more of my own. It also includes other pearls and heuristics that I use to practice my trade. 在他精彩的著作《重构》[1] 中,Martin Fowler 识别了许多不同的“代码异味”。接下来的列表包含了许多 Martin 提出的异味,并增加了许多我自己的。它还包含了我用来实践我的手艺的其他珍宝和启发式规则。
- [Refactoring].
- [Refactoring/重构]。
I compiled this list by walking through several different programs and refactoring them. As I made each change, I asked myself why I made that change and then wrote the reason down here. The result is a rather long list of things that smell bad to me when I read code. 我通过浏览几个不同的程序并对它们进行重构,从而汇编了这份列表。每当我做出修改时,我会问自己为什么要那样改,然后把理由记录在这里。结果就是这份长长的清单,列出了我在阅读代码时觉得味道糟糕的东西。
This list is meant to be read from top to bottom and also to be used as a reference. There is a cross-reference for each heuristic that shows you where it is referenced in the rest of the text in “Appendix C” on page 409. 这份列表既可以从头读到尾,也可以作为参考资料使用。在第 409 页的“附录 C”中,每一条启发式规则都有交叉引用,显示了它们在书中其他地方出现的位置。
COMMENTS (注释)
C1: Inappropriate InformationC1: 不恰当的信息
It is inappropriate for a comment to hold information better held in a different kind of system such as your source code control system, your issue tracking system, or any other record-keeping system. Change histories, for example, just clutter up source files with volumes of historical and uninteresting text. In general, meta-data such as authors, last-modified-date, SPR number, and so on should not appear in comments. Comments should be reserved for technical notes about the code and design. 如果注释中的信息更适合放在其他系统中(如源代码控制系统、问题追踪系统或其他记录系统),那么由于这些信息出现在注释中就是不恰当的。例如,修改历史记录只会用大量无趣的历史文本充斥源文件。通常,元数据(如作者、最后修改日期、SPR 编号等)不应出现在注释中。注释应该保留给关于代码和设计的技术说明。
C2: Obsolete CommentC2: 废弃的注释
A comment that has gotten old, irrelevant, and incorrect is obsolete. Comments get old quickly. It is best not to write a comment that will become obsolete. If you find an obsolete comment, it is best to update it or get rid of it as quickly as possible. Obsolete comments tend to migrate away from the code they once described. They become floating islands of irrelevance and misdirection in the code. 过时、无关或错误的注释就是废弃的注释。注释变老得很快。最好不要写那些会过时的注释。如果你发现了一个废弃的注释,最好尽快更新它或将其删除。废弃的注释往往会远离它们曾经描述的代码。它们变成了代码中无关紧要和误导的孤岛。
C3: Redundant CommentC3: 冗余的注释
A comment is redundant if it describes something that adequately describes itself. For example: 如果注释描述的是代码本身已经充分描述的内容,那么它就是冗余的。例如:
i++; // increment iAnother example is a Javadoc that says nothing more than (or even less than) the function signature: 另一个例子是 Javadoc,它说的并不比函数签名多(甚至更少):
/**
* @param sellRequest
* @return
* @throws ManagedComponentException
*/
public SellResponse beginSellItem(SellRequest sellRequest)
throws ManagedComponentExceptionComments should say things that the code cannot say for itself. 注释应该说那些代码无法自我表达的事情。
C4: Poorly Written CommentC4: 写得不好的注释
A comment worth writing is worth writing well. If you are going to write a comment, take the time to make sure it is the best comment you can write. Choose your words carefully. Use correct grammar and punctuation. Don’t ramble. Don’t state the obvious. Be brief. 值得写的注释就值得写好。如果你打算写注释,请花时间确保它是你能写出的最好的注释。仔细斟酌词句。使用正确的语法和标点符号。不要啰嗦。不要陈述显而易见的事实。要简洁。
C5: Commented-Out CodeC5: 注释掉的代码
It makes me crazy to see stretches of code that are commented out. Who knows how old it is? Who knows whether or not it’s meaningful? Yet no one will delete it because everyone assumes someone else needs it or has plans for it. 看到大段被注释掉的代码会让我抓狂。谁知道它有多老了?谁知道它是否还有意义?然而没人会删除它,因为每个人都假设别人需要它或者对它有安排。
That code sits there and rots, getting less and less relevant with every passing day. It calls functions that no longer exist. It uses variables whose names have changed. It follows conventions that are long obsolete. It pollutes the modules that contain it and distracts the people who try to read it. Commented-out code is an abomination. 那段代码就在那里腐烂,随着时间的推移变得越来越不相关。它调用的函数可能已经不存在了。它使用的变量名可能已经变了。它遵循的规范可能早已过时。它污染了包含它的模块,分散了试图阅读代码的人的注意力。注释掉的代码是一种令人憎恶的东西。
When you see commented-out code, delete it! Don’t worry, the source code control system still remembers it. If anyone really needs it, he or she can go back and check out a previous version. Don’t suffer commented-out code to survive. 当你看到注释掉的代码时,删除它!别担心,源代码控制系统仍然记得它。如果真有人需要它,他或她可以回去检出以前的版本。不要容忍注释掉的代码存活。
ENVIRONMENT (环境)
E1: Build Requires More Than One StepE1: 构建需要多步
Building a project should be a single trivial operation. You should not have to check many little pieces out from source code control. You should not need a sequence of arcane commands or context dependent scripts in order to build the individual elements. You should not have to search near and far for all the various little extra JARs, XML files, and other artifacts that the system requires. You should be able to check out the system with one simple command and then issue one other simple command to build it. 构建项目应该是一个单一且微不足道的操作。你不应该需要从源代码控制中检出许多小碎片。你不应该需要一系列晦涩的命令或依赖上下文的脚本来构建各个元素。你不应该需要四处寻找系统所需的各种额外 JAR 包、XML 文件和其他工件。你应该能够用一个简单的命令检出系统,然后发出另一个简单的命令来构建它。
svn get mySystem
cd mySystem
ant allE2: Tests Require More Than One StepE2: 测试需要多步
You should be able to run all the unit tests with just one command. In the best case you can run all the tests by clicking on one button in your IDE. In the worst case you should be able to issue a single simple command in a shell. Being able to run all the tests is so fundamental and so important that it should be quick, easy, and obvious to do. 你应该能够仅用一个命令就运行所有单元测试。最好的情况是,你可以通过点击 IDE 中的一个按钮来运行所有测试。最坏的情况是,你应该能够在 shell 中发出一个简单的命令。能够运行所有测试是如此基础和重要,以至于它应该是快速、简单且显而易见的。
FUNCTIONS (函数)
F1: Too Many ArgumentsF1: 参数过多
Functions should have a small number of arguments. No argument is best, followed by one, two, and three. More than three is very questionable and should be avoided with prejudice. (See “Function Arguments” on page 40.) 函数的参数应该很少。没有参数是最好的,其次是一个、两个和三个。超过三个是非常值得怀疑的,应该坚决避免。(参见第 40 页的“函数参数”。)
F2: Output ArgumentsF2: 输出参数
Output arguments are counterintuitive. Readers expect arguments to be inputs, not outputs. If your function must change the state of something, have it change the state of the object it is called on. (See “Output Arguments” on page 45.) 输出参数是违反直觉的。读者期望参数是输入,而不是输出。如果你的函数必须改变某种状态,让它改变调用它的那个对象的状态。(参见第 45 页的“输出参数”。)
F3: Flag ArgumentsF3: 标识参数
Boolean arguments loudly declare that the function does more than one thing. They are confusing and should be eliminated. (See “Flag Arguments” on page 41.) 布尔参数大声地宣告该函数做的事情不止一件。它们令人困惑,应该被消除。(参见第 41 页的“标识参数”。)
F4: Dead FunctionF4: 死函数
Methods that are never called should be discarded. Keeping dead code around is wasteful. Don’t be afraid to delete the function. Remember, your source code control system still remembers it. 从未被调用的方法应该被丢弃。保留死代码是浪费。不要害怕删除函数。记住,你的源代码控制系统仍然记得它。
GENERAL (一般性问题)
G1: Multiple Languages in One Source FileG1: 一个源文件中存在多种语言
Today’s modern programming environments make it possible to put many different languages into a single source file. For example, a Java source file might contain snippets of XML, HTML, YAML, JavaDoc, English, JavaScript, and so on. For another example, in addition to HTML a JSP file might contain Java, a tag library syntax, English comments, Javadocs, XML, JavaScript, and so forth. This is confusing at best and carelessly sloppy at worst. 现代编程环境使得在一个源文件中放入多种不同的语言成为可能。例如,一个 Java 源文件可能包含 XML、HTML、YAML、JavaDoc、英语、JavaScript 等片段。再比如,一个 JSP 文件除了 HTML 外,可能还包含 Java、标签库语法、英语注释、Javadocs、XML、JavaScript 等等。往好了说这是令人困惑的,往坏了说这是粗心大意。
The ideal is for a source file to contain one, and only one, language. Realistically, we will probably have to use more than one. But we should take pains to minimize both the number and extent of extra languages in our source files. 理想情况是,一个源文件应该包含且仅包含一种语言。现实中,我们可能不得不使用不止一种。但我们应该尽力将源文件中额外语言的数量和范围降到最低。
G2: Obvious Behavior Is UnimplementedG2: 明显的行为未被实现
Following “The Principle of Least Surprise,”2 any function or class should implement the behaviors that another programmer could reasonably expect. For example, consider a function that translates the name of a day to an enum that represents the day. 遵循“最小惊奇原则”[2],任何函数或类都应该实现其他程序员理应期望的行为。例如,考虑一个将日期名称转换为代表该日期的枚举的函数。
- Or “The Principle of Least Astonishment”: http://en.wikipedia.org/wiki/Principle_of_least_astonishment
- 或称“最小惊讶原则”:http://en.wikipedia.org/wiki/Principle_of_least_astonishment
Day day = DayDate.StringToDay(String dayName);We would expect the string “Monday” to be translated to Day.MONDAY. We would also expect the common abbreviations to be translated, and we would expect the function to ignore case. 我们会期望字符串“Monday”被转换为 Day.MONDAY。我们也会期望常见的缩写被转换,并且期望该函数忽略大小写。
When an obvious behavior is not implemented, readers and users of the code can no longer depend on their intuition about function names. They lose their trust in the original author and must fall back on reading the details of the code. 当明显的行为未被实现时,代码的读者和用户就无法再依赖他们对函数名称的直觉。他们会失去对原始作者的信任,不得不回过头去阅读代码细节。
G3: Incorrect Behavior at the BoundariesG3: 边界行为不正确
It seems obvious to say that code should behave correctly. The problem is that we seldom realize just how complicated correct behavior is. Developers often write functions that they think will work, and then trust their intuition rather than going to the effort to prove that their code works in all the corner and boundary cases. 说代码应该行为正确似乎是显而易见的。问题在于我们很少意识到正确的行为有多复杂。开发人员经常编写他们认为会工作的函数,然后相信他们的直觉,而不是努力证明他们的代码在所有角落和边界情况下都能工作。
There is no replacement for due diligence. Every boundary condition, every corner case, every quirk and exception represents something that can confound an elegant and intuitive algorithm. Don’t rely on your intuition. Look for every boundary condition and write a test for it. 尽职调查无可替代。每一个边界条件、每一个极端情况、每一个怪癖和异常都代表着可能搞乱优雅直观算法的东西。不要依赖你的直觉。寻找每一个边界条件并为之编写测试。
G4: Overridden SafetiesG4: 忽视安全机制
Chernobyl melted down because the plant manager overrode each of the safety mechanisms one by one. The safeties were making it inconvenient to run an experiment. The result was that the experiment did not get run, and the world saw it’s first major civilian nuclear catastrophe. 切尔诺贝利核电站熔毁是因为厂长逐一覆盖(关闭)了每一个安全机制。这些安全机制使得进行实验变得不方便。结果是实验没有进行,世界见证了第一次重大的民用核灾难。
It is risky to override safeties. Exerting manual control over serialVersionUID may be necessary, but it is always risky. Turning off certain compiler warnings (or all warnings!) may help you get the build to succeed, but at the risk of endless debugging sessions. Turning off failing tests and telling yourself you’ll get them to pass later is as bad as pretending your credit cards are free money. 覆盖安全机制是有风险的。手动控制 serialVersionUID 可能是必要的,但这总是有风险的。关闭某些编译器警告(或所有警告!)可能会帮助你构建成功,但风险是无休止的调试。关掉失败的测试并告诉自己以后会让它们通过,这就好比假装信用卡里的钱是白送的一样糟糕。
G5: DuplicationG5: 重复
This is one of the most important rules in this book, and you should take it very seriously. Virtually every author who writes about software design mentions this rule. Dave Thomas and Andy Hunt called it the DRY3 principle (Don’t Repeat Yourself). Kent Beck made it one of the core principles of Extreme Programming and called it: “Once, and only once.” Ron Jeffries ranks this rule second, just below getting all the tests to pass. 这是本书中最重要的规则之一,你应该非常认真地对待它。几乎每一位撰写软件设计的作者都会提到这条规则。Dave Thomas 和 Andy Hunt 称之为 DRY[3] 原则(Don't Repeat Yourself,不要重复自己)。Kent Beck 将其作为极限编程的核心原则之一,并称之为:“一次,且仅一次。”Ron Jeffries 将这条规则排在第二位,仅次于让所有测试通过。
- [PRAG].
- [PRAG](指《程序员修炼之道》)。
Every time you see duplication in the code, it represents a missed opportunity for abstraction. That duplication could probably become a subroutine or perhaps another class outright. By folding the duplication into such an abstraction, you increase the vocabulary of the language of your design. Other programmers can use the abstract facilities you create. Coding becomes faster and less error prone because you have raised the abstraction level. 每次你在代码中看到重复,都代表着一次错失的抽象机会。这种重复可能应该成为一个子程序,或者干脆成为另一个类。通过将重复折叠进这样的抽象中,你增加了设计语言的词汇量。其他程序员可以使用你创建的抽象设施。编码变得更快且更不容易出错,因为你提高了抽象层级。
The most obvious form of duplication is when you have clumps of identical code that look like some programmers went wild with the mouse, pasting the same code over and over again. These should be replaced with simple methods. 最明显的重复形式是当你有一堆相同的代码时,看起来像是一些程序员疯狂地使用鼠标,一遍又一遍地粘贴相同的代码。这些应该用简单的方法来替换。
A more subtle form is the switch/case or if/else chain that appears again and again in various modules, always testing for the same set of conditions. These should be replaced with polymorphism. 一种更微妙的形式是 switch/case 或 if/else 链,它们在各个模块中反复出现,总是测试同一组条件。这些应该用多态来替换。
Still more subtle are the modules that have similar algorithms, but that don’t share similar lines of code. This is still duplication and should be addressed by using the TEMPLATE METHOD,4 or STRATEGY5 pattern. 更微妙的是那些拥有相似算法但代码行不相似的模块。这仍然是重复,应该通过使用 TEMPLATE METHOD(模板方法)[4] 或 STRATEGY(策略)[5] 模式来解决。
- [GOF].
- [GOF].
- [GOF](设计模式四人帮)。
- [GOF]。
Indeed, most of the design patterns that have appeared in the last fifteen years are simply well-known ways to eliminate duplication. So too the Codd Normal Forms are a strategy for eliminating duplication in database schemae. OO itself is a strategy for organizing modules and eliminating duplication. Not surprisingly, so is structured programming. 事实上,过去十五年出现的大多数设计模式只是消除重复的众所周知的方法。同样,Codd 范式也是消除数据库模式中重复的策略。面向对象本身就是一种组织模块和消除重复的策略。不足为奇的是,结构化编程也是如此。
I think the point has been made. Find and eliminate duplication wherever you can. 我想观点已经表达清楚了。尽可能地发现并消除重复。
G6: Code at Wrong Level of AbstractionG6: 代码处于错误的抽象层级
It is important to create abstractions that separate higher level general concepts from lower level detailed concepts. Sometimes we do this by creating abstract classes to hold the higher level concepts and derivatives to hold the lower level concepts. When we do this, we need to make sure that the separation is complete. We want all the lower level concepts to be in the derivatives and all the higher level concepts to be in the base class. 创建抽象以分离高层通用概念和低层细节概念是很重要的。有时我们通过创建抽象类来容纳高层概念,并创建派生类来容纳低层概念。当我们这样做时,我们需要确保分离是彻底的。我们希望所有低层概念都在派生类中,所有高层概念都在基类中。
For example, constants, variables, or utility functions that pertain only to the detailed implementation should not be present in the base class. The base class should know nothing about them. 例如,仅与详细实现相关的常量、变量或实用函数不应出现在基类中。基类应该对它们一无所知。
This rule also pertains to source files, components, and modules. Good software design requires that we separate concepts at different levels and place them in different containers. Sometimes these containers are base classes or derivatives and sometimes they are source files, modules, or components. Whatever the case may be, the separation needs to be complete. We don’t want lower and higher level concepts mixed together. 这条规则也适用于源文件、组件和模块。良好的软件设计要求我们将不同层级的概念分离开来,并将它们放在不同的容器中。有时这些容器是基类或派生类,有时是源文件、模块或组件。无论情况如何,分离都需要是彻底的。我们不希望低层和高层概念混合在一起。
Consider the following code: 考虑下面的代码:
public interface Stack {
Object pop() throws EmptyException;
void push(Object o) throws FullException;
double percentFull();
class EmptyException extends Exception {}
class FullException extends Exception {}
}The percentFull function is at the wrong level of abstraction. Although there are many implementations of Stack where the concept of fullness is reasonable, there are other implementations that simply could not know how full they are. So the function would be better placed in a derivative interface such as BoundedStack. percentFull 函数处于错误的抽象层级。虽然在许多 Stack 的实现中,“满”的概念是合理的,但在其他实现中,它们根本无法知道自己有多满(例如无限栈)。所以这个函数最好放在像 BoundedStack 这样的派生接口中。
Perhaps you are thinking that the implementation could just return zero if the stack were boundless. The problem with that is that no stack is truly boundless. You cannot really prevent an OutOfMemoryException by checking for 也许你会想,如果是无限栈,实现可以只返回零。问题在于没有栈是真正无限的。你无法通过检查以下代码来真正防止 OutOfMemoryException:
stack.percentFull() < 50.0.Implementing the function to return 0 would be telling a lie. 实现该函数返回 0 将是在撒谎。
The point is that you cannot lie or fake your way out of a misplaced abstraction. Isolating abstractions is one of the hardest things that software developers do, and there is no quick fix when you get it wrong. 重点是,你不能通过撒谎或伪造来摆脱错误的抽象。隔离抽象是软件开发人员所做的最困难的事情之一,当你做错时没有快速的修复方法。
G7: Base Classes Depending on Their DerivativesG7: 基类依赖于派生类
The most common reason for partitioning concepts into base and derivative classes is so that the higher level base class concepts can be independent of the lower level derivative class concepts. Therefore, when we see base classes mentioning the names of their derivatives, we suspect a problem. In general, base classes should know nothing about their derivatives. 将概念划分为基类和派生类的最常见原因是,让高层的基类概念独立于低层的派生类概念。因此,当我们看到基类提到其派生类的名称时,我们就会怀疑有问题。通常,基类应该对其派生类一无所知。
There are exceptions to this rule, of course. Sometimes the number of derivatives is strictly fixed, and the base class has code that selects between the derivatives. We see this a lot in finite state machine implementations. However, in that case the derivatives and base class are strongly coupled and always deploy together in the same jar file. In the general case we want to be able to deploy derivatives and bases in different jar files. 当然,这条规则也有例外。有时派生类的数量是严格固定的,基类有代码在派生类之间进行选择。我们在有限状态机的实现中经常看到这种情况。然而,在那种情况下,派生类和基类是强耦合的,并且总是部署在同一个 jar 文件中。在一般情况下,我们希望能够将派生类和基类部署在不同的 jar 文件中。
Deploying derivatives and bases in different jar files and making sure the base jar files know nothing about the contents of the derivative jar files allow us to deploy our systems in discrete and independent components. When such components are modified, they can be redeployed without having to redeploy the base components. This means that the impact of a change is greatly lessened, and maintaining systems in the field is made much simpler. 将派生类和基类部署在不同的 jar 文件中,并确保基类 jar 文件对派生类 jar 文件内容一无所知,这允许我们以离散和独立的组件部署系统。当修改这些组件时,可以重新部署它们而无需重新部署基类组件。这意味着变更的影响大大降低,现场系统的维护变得更加简单。
G8: Too Much InformationG8: 信息过多
Well-defined modules have very small interfaces that allow you to do a lot with a little. Poorly defined modules have wide and deep interfaces that force you to use many different gestures to get simple things done. A well-defined interface does not offer very many functions to depend upon, so coupling is low. A poorly defined interface provides lots of functions that you must call, so coupling is high. 定义良好的模块具有非常小的接口,允许你以少量的操作做很多事情。定义糟糕的模块具有宽而深的接口,迫使你使用许多不同的动作来完成简单的事情。定义良好的接口不提供很多可依赖的函数,因此耦合度低。定义糟糕的接口提供了大量你必须调用的函数,因此耦合度高。
Good software developers learn to limit what they expose at the interfaces of their classes and modules. The fewer methods a class has, the better. The fewer variables a function knows about, the better. The fewer instance variables a class has, the better. 优秀的软件开发人员学会限制他们在类和模块接口上暴露的内容。一个类的方法越少越好。一个函数知道的变量越少越好。一个类的实例变量越少越好。
Hide your data. Hide your utility functions. Hide your constants and your temporaries. Don’t create classes with lots of methods or lots of instance variables. Don’t create lots of protected variables and functions for your subclasses. Concentrate on keeping interfaces very tight and very small. Help keep coupling low by limiting information. 隐藏你的数据。隐藏你的实用函数。隐藏你的常量和临时变量。不要创建具有大量方法或大量实例变量的类。不要为你的子类创建大量的受保护变量和函数。专注于保持接口非常紧凑和微小。通过限制信息来帮助保持低耦合。
G9: Dead CodeG9: 死代码
Dead code is code that isn’t executed. You find it in the body of an if statement that checks for a condition that can’t happen. You find it in the catch block of a try that never throws. You find it in little utility methods that are never called or switch/case conditions that never occur. 死代码是未执行的代码。你会在检查一个不可能发生的条件的 if 语句体中发现它。你会在一个从不抛出异常的 try 的 catch 块中发现它。你会在从未被调用的小实用方法或从未发生的 switch/case 条件中发现它。
The problem with dead code is that after awhile it starts to smell. The older it is, the stronger and sourer the odor becomes. This is because dead code is not completely updated when designs change. It still compiles, but it does not follow newer conventions or rules. It was written at a time when the system was different. When you find dead code, do the right thing. Give it a decent burial. Delete it from the system. 死代码的问题在于,过了一段时间它就开始发臭。它越老,味道就越浓、越酸。这是因为当设计改变时,死代码没有完全更新。它仍然可以编译,但它不遵循较新的约定或规则。它是在系统不同的时候编写的。当你发现死代码时,做正确的事。给它一个体面的葬礼。把它从系统中删除。
G10: Vertical SeparationG10: 垂直分离
Variables and function should be defined close to where they are used. Local variables should be declared just above their first usage and should have a small vertical scope. We don’t want local variables declared hundreds of lines distant from their usages. 变量和函数应在靠近它们被使用的地方定义。局部变量应在首次使用之前声明,并应具有较小的垂直作用域。我们不希望局部变量声明在距离其使用处数百行之外。
Private functions should be defined just below their first usage. Private functions belong to the scope of the whole class, but we’d still like to limit the vertical distance between the invocations and definitions. Finding a private function should just be a matter of scanning downward from the first usage. 私有函数应在首次使用之后定义。私有函数属于整个类的作用域,但我们仍然希望限制调用和定义之间的垂直距离。查找私有函数应该只是从首次使用处向下扫描的问题。
G11: InconsistencyG11: 不一致
If you do something a certain way, do all similar things in the same way. This goes back to the principle of least surprise. Be careful with the conventions you choose, and once chosen, be careful to continue to follow them. 如果你以某种方式做某事,请以相同的方式做所有类似的事情。这回到了最小惊奇原则。仔细选择你的约定,一旦选定,请务必继续遵循它们。
If within a particular function you use a variable named response to hold an HttpServletResponse, then use the same variable name consistently in the other functions that use HttpServletResponse objects. If you name a method processVerificationRequest, then use a similar name, such as processDeletionRequest, for the methods that process other kinds of requests. 如果在一个特定函数中,你使用名为 response 的变量来保存 HttpServletResponse,那么在其他使用 HttpServletResponse 对象的函数中也要一致地使用相同的变量名。如果你将一个方法命名为 processVerificationRequest,那么对于处理其他类型请求的方法,请使用类似的名称,如 processDeletionRequest。
Simple consistency like this, when reliably applied, can make code much easier to read and modify. 像这样简单的通过可靠应用的一致性,可以使代码更容易阅读和修改。
G12: ClutterG12: 杂乱
Of what use is a default constructor with no implementation? All it serves to do is clutter up the code with meaningless artifacts. Variables that aren’t used, functions that are never called, comments that add no information, and so forth. All these things are clutter and should be removed. Keep your source files clean, well organized, and free of clutter. 一个没有实现的默认构造函数有什么用?它所做的只是用毫无意义的工件充斥代码。未使用的变量、从未调用的函数、没有增加信息的注释等等。所有这些东西都是杂乱,应该被移除。保持你的源文件整洁、组织良好且没有杂物。
G13: Artificial CouplingG13: 人为耦合
Things that don’t depend upon each other should not be artificially coupled. For example, general enums should not be contained within more specific classes because this forces the whole application to know about these more specific classes. The same goes for general purpose static functions being declared in specific classes. 相互不依赖的事物不应该被人为地耦合在一起。例如,通用的枚举不应包含在更具体的类中,因为这迫使整个应用程序都要知道这些更具体的类。在特定类中声明通用静态函数也是如此。
In general an artificial coupling is a coupling between two modules that serves no direct purpose. It is a result of putting a variable, constant, or function in a temporarily convenient, though inappropriate, location. This is lazy and careless. 通常,人为耦合是两个模块之间的耦合,它没有直接目的。它是将变量、常量或函数放在暂时方便但不适当的位置的结果。这是懒惰和粗心的表现。
Take the time to figure out where functions, constants, and variables ought to be declared. Don’t just toss them in the most convenient place at hand and then leave them there. 花点时间弄清楚函数、常量和变量应该在哪里声明。不要只是把它们扔在手头最方便的地方,然后就不管了。
G14: Feature EnvyG14: 特性依恋
This is one of Martin Fowler’s code smells.6 The methods of a class should be interested in the variables and functions of the class they belong to, and not the variables and functions of other classes. When a method uses accessors and mutators of some other object to manipulate the data within that object, then it envies the scope of the class of that other object. It wishes that it were inside that other class so that it could have direct access to the variables it is manipulating. For example: 这是 Martin Fowler 的代码异味之一[6]。类的方法应该对自己所属类的变量和函数感兴趣,而不是其他类的变量和函数。当一个方法使用另一个对象的访问器和修改器来操作该对象中的数据时,它就嫉妒该对象的类的作用域。它希望自己在那个类里面,这样它就可以直接访问它正在操作的变量。例如:
- [Refactoring].
- [Refactoring/重构]。
public class HourlyPayCalculator {
public Money calculateWeeklyPay(HourlyEmployee e) {
int tenthRate = e.getTenthRate().getPennies();
int tenthsWorked = e.getTenthsWorked();
int straightTime = Math.min(400, tenthsWorked);
int overTime = Math.max(0, tenthsWorked - straightTime);
int straightPay = straightTime * tenthRate;
int overtimePay = (int)Math.round(overTime*tenthRate*1.5);
return new Money(straightPay + overtimePay);
}
}The calculateWeeklyPay method reaches into the HourlyEmployee object to get the data on which it operates. The calculateWeeklyPay method envies the scope of HourlyEmployee. It “wishes” that it could be inside HourlyEmployee. calculateWeeklyPay 方法深入到 HourlyEmployee 对象中获取它操作的数据。calculateWeeklyPay 方法嫉妒 HourlyEmployee 的作用域。它“希望”自己能在 HourlyEmployee 内部。
All else being equal, we want to eliminate Feature Envy because it exposes the internals of one class to another. Sometimes, however, Feature Envy is a necessary evil. Consider the following: 在其他条件相同的情况下,我们要消除特性依恋,因为它将一个类的内部暴露给了另一个类。然而,有时特性依恋是一种必要的恶。考虑以下情况:
public class HourlyEmployeeReport {
private HourlyEmployee employee ;
public HourlyEmployeeReport(HourlyEmployee e) {
this.employee = e;
}
String reportHours() {
return String.format(
“Name: %s\tHours:%d.%1d\n”,
employee.getName(),
employee.getTenthsWorked()/10,
employee.getTenthsWorked()%10);
}
}Clearly, the reportHours method envies the HourlyEmployee class. On the other hand, we don’t want HourlyEmployee to have to know about the format of the report. Moving that format string into the HourlyEmployee class would violate several principles of object oriented design.7 It would couple HourlyEmployee to the format of the report, exposing it to changes in that format. 显然,reportHours 方法嫉妒 HourlyEmployee 类。另一方面,我们不希望 HourlyEmployee 必须知道报告的格式。将该格式字符串移入 HourlyEmployee 类将违反面向对象设计的几个原则[7]。它会将 HourlyEmployee 与报告的格式耦合,使其暴露于该格式的变化中。
- Specifically, the Single Responsibility Principle, the Open Closed Principle, and the Common Closure Principle. See [PPP].
- 具体来说,是单一职责原则、开闭原则和共同闭包原则。参见 [PPP](《敏捷软件开发:原则、模式与实践》)。
G15: Selector ArgumentsG15: 选择器参数
There is hardly anything more abominable than a dangling false argument at the end of a function call. What does it mean? What would it change if it were true? Not only is the purpose of a selector argument difficult to remember, each selector argument combines many functions into one. Selector arguments are just a lazy way to avoid splitting a large function into several smaller functions. Consider: 几乎没有什么比函数调用末尾悬挂的 false 参数更可恶的了。它意味着什么?如果它是 true 会改变什么?选择器参数不仅目的难以记住,而且每个选择器参数都将多个函数合并为一个。选择器参数只是避免将大函数拆分为几个小函数的懒惰方式。考虑:
public int calculateWeeklyPay(boolean overtime) {
int tenthRate = getTenthRate();
int tenthsWorked = getTenthsWorked();
int straightTime = Math.min(400, tenthsWorked);
int overTime = Math.max(0, tenthsWorked - straightTime);
int straightPay = straightTime * tenthRate;
double overtimeRate = overtime ? 1.5 : 1.0 * tenthRate;
int overtimePay = (int)Math.round(overTime*overtimeRate);
return straightPay + overtimePay;
}You call this function with a true if overtime is paid as time and a half, and with a false if overtime is paid as straight time. It’s bad enough that you must remember what calculateWeeklyPay(false) means whenever you happen to stumble across it. But the real shame of a function like this is that the author missed the opportunity to write the following: 如果加班费是按1.5倍支付,你用 true 调用此函数;如果是按正常时间支付,你用 false 调用。每当你碰巧遇到 calculateWeeklyPay(false) 时,你必须记住它的含义,这已经够糟糕了。但这类函数真正的耻辱在于作者错过了编写如下代码的机会:
public int straightPay() {
return getTenthsWorked() * getTenthRate();
}
public int overTimePay() {
int overTimeTenths = Math.max(0, getTenthsWorked() - 400);
int overTimePay = overTimeBonus(overTimeTenths);
return straightPay() + overTimePay;
}
private int overTimeBonus(int overTimeTenths) {
double bonus = 0.5 * getTenthRate() * overTimeTenths;
return (int) Math.round(bonus);
}Of course, selectors need not be boolean. They can be enums, integers, or any other type of argument that is used to select the behavior of the function. In general it is better to have many functions than to pass some code into a function to select the behavior. 当然,选择器不一定是布尔值。它们可以是枚举、整数或任何其他类型的参数,用于选择函数的行为。通常,拥有多个函数比向一个函数传递代码来选择行为要好。
G16: Obscured IntentG16: 晦涩的意图
We want code to be as expressive as possible. Run-on expressions, Hungarian notation, and magic numbers all obscure the author’s intent. For example, here is the overTimePay function as it might have appeared: 我们希望代码尽可能具有表现力。冗长的表达式、匈牙利命名法和魔术数都会模糊作者的意图。例如,这就是 overTimePay 函数可能出现的样子:
public int m_otCalc() {
return iThsWkd * iThsRte +
(int) Math.round(0.5 * iThsRte *
Math.max(0, iThsWkd - 400)
);
}Small and dense as this might appear, it’s also virtually impenetrable. It is worth taking the time to make the intent of our code visible to our readers. 虽然这看起来很短小紧凑,但也几乎无法理解。花时间让我们的代码意图对读者可见是值得的。
G17: Misplaced ResponsibilityG17: 位置错误的职责
One of the most important decisions a software developer can make is where to put code. For example, where should the PI constant go? Should it be in the Math class? Perhaps it belongs in the Trigonometry class? Or maybe in the Circle class? 软件开发人员能做出的最重要的决定之一就是把代码放在哪里。例如,PI 常量应该放在哪里?应该在 Math 类中吗?也许它属于 Trigonometry(三角学)类?或者也许在 Circle 类中?
The principle of least surprise comes into play here. Code should be placed where a reader would naturally expect it to be. The PI constant should go where the trig functions are declared. The OVERTIME_RATE constant should be declared in the HourlyPay-Calculator class. 这里适用最小惊奇原则。代码应该放在读者自然期望它在的地方。PI 常量应该放在声明三角函数的地方。OVERTIME_RATE 常量应该在 HourlyPayCalculator 类中声明。
Sometimes we get “clever” about where to put certain functionality. We’ll put it in a function that’s convenient for us, but not necessarily intuitive to the reader. For example, perhaps we need to print a report with the total of hours that an employee worked. We could sum up those hours in the code that prints the report, or we could try to keep a running total in the code that accepts time cards. 有时我们会对把某些功能放在哪里感到“聪明”。我们会把它放在对我们要方便的函数中,但对读者来说不一定直观。例如,也许我们需要打印一份包含员工总工时的报告。我们可以在打印报告的代码中汇总这些工时,或者我们可以尝试在接收工时卡的代码中保持一个运行总计。
One way to make this decision is to look at the names of the functions. Let’s say that our report module has a function named getTotalHours. Let’s also say that the module that accepts time cards has a saveTimeCard function. Which of these two functions, by it’s name, implies that it calculates the total? The answer should be obvious. 做这个决定的一种方法是看函数的名字。假设我们的报告模块有一个名为 getTotalHours 的函数。再假设接收工时卡的模块有一个 saveTimeCard 函数。这两个函数中,哪一个从名字上看暗示了它计算总数?答案应该是显而易见的。
Clearly, there are sometimes performance reasons why the total should be calculated as time cards are accepted rather than when the report is printed. That’s fine, but the names of the functions ought to reflect this. For example, there should be a computeRunning-TotalOfHours function in the timecard module. 显然,有时出于性能原因,总数应该在接收工时卡时计算,而不是在打印报告时。这没问题,但函数的名字应该反映这一点。例如,在工时卡模块中应该有一个 computeRunningTotalOfHours 函数。
G18: Inappropriate StaticG18: 不恰当的静态方法
Math.max(double a, double b) is a good static method. It does not operate on a single instance; indeed, it would be silly to have to say new Math().max(a,b) or even a.max(b). All the data that max uses comes from its two arguments, and not from any “owning” object. More to the point, there is almost no chance that we’d want Math.max to be polymorphic. Math.max(double a, double b) 是一个很好的静态方法。它不对单个实例进行操作;实际上,如果不这样,不得不写 new Math().max(a,b) 甚至 a.max(b) 是很愚蠢的。max 使用的所有数据都来自它的两个参数,而不是来自任何“拥有者”对象。更重要的是,我们几乎没有任何机会希望 Math.max 具有多态性。
Sometimes, however, we write static functions that should not be static. For example, consider: 然而,有时我们编写了不应该是静态的静态函数。例如,考虑:
HourlyPayCalculator.calculatePay(employee, overtimeRate).Again, this seems like a reasonable static function. It doesn’t operate on any particular object and gets all it’s data from it’s arguments. However, there is a reasonable chance that we’ll want this function to be polymorphic. We may wish to implement several different algorithms for calculating hourly pay, for example, OvertimeHourlyPayCalculator and StraightTimeHourlyPayCalculator. So in this case the function should not be static. It should be a nonstatic member function of Employee. 再次,这看起来像是一个合理的静态函数。它不对任何特定对象操作,并从其参数中获取所有数据。然而,我们很有可能希望此函数具有多态性。我们可能希望实现几种不同的计算时薪的算法,例如 OvertimeHourlyPayCalculator 和 StraightTimeHourlyPayCalculator。所以在这种情况下,该函数不应该是静态的。它应该是 Employee 的非静态成员函数。
In general you should prefer nonstatic methods to static methods. When in doubt, make the function nonstatic. If you really want a function to be static, make sure that there is no chance that you’ll want it to behave polymorphically. 通常,你应该优先选择非静态方法而不是静态方法。如果有疑问,就让函数非静态。如果你真的想让一个函数是静态的,确保你绝不会希望它表现出多态行为。
G19: Use Explanatory VariablesG19: 使用解释性变量
Kent Beck wrote about this in his great book Smalltalk Best Practice Patterns8 and again more recently in his equally great book Implementation Patterns.9 One of the more powerful ways to make a program readable is to break the calculations up into intermediate values that are held in variables with meaningful names. Kent Beck 在他的好书《Smalltalk Best Practice Patterns》[8] 以及最近同样精彩的《Implementation Patterns》[9] 中写到了这一点。让程序可读的更有力的方法之一是将计算分解为中间值,并保存在具有明确名称的变量中。
- [Beck97], p. 108.
- [Beck07].
- [Beck97], 第 108 页.
- [Beck07].
Consider this example from FitNesse: 考虑这个来自 FitNesse 的例子:
Matcher match = headerPattern.matcher(line);
if(match.find())
{
String key = match.group(1);
String value = match.group(2);
headers.put(key.toLowerCase(), value);
}The simple use of explanatory variables makes it clear that the first matched group is the key, and the second matched group is the value. 简单使用解释性变量就清楚地表明了第一个匹配组是键,第二个匹配组是值。
It is hard to overdo this. More explanatory variables are generally better than fewer. It is remarkable how an opaque module can suddenly become transparent simply by breaking the calculations up into well-named intermediate values. 这很难做得过火。更多的解释性变量通常比更少的好。仅仅通过将计算分解为命名良好的中间值,一个不透明的模块会突然变得透明,这真是令人惊叹。
G20: Function Names Should Say What They DoG20: 函数名称应说明其功能
Look at this code: 看看这段代码:
Date newDate = date.add(5);Would you expect this to add five days to the date? Or is it weeks, or hours? Is the date instance changed or does the function just return a new Date without changing the old one? You can’t tell from the call what the function does. 你会期望这给日期增加五天吗?还是周,或者小时?日期实例被改变了吗,还是该函数只是返回一个新的日期而不改变旧的?你无法从调用中看出函数做了什么。
If the function adds five days to the date and changes the date, then it should be called addDaysTo or increaseByDays. If, on the other hand, the function returns a new date that is five days later but does not change the date instance, it should be called daysLater or daysSince. 如果函数给日期增加五天并改变了日期,那么它应该被称为 addDaysTo 或 increaseByDays。另一方面,如果函数返回一个五天后的新日期但不改变日期实例,它应该被称为 daysLater 或 daysSince。
If you have to look at the implementation (or documentation) of the function to know what it does, then you should work to find a better name or rearrange the functionality so that it can be placed in functions with better names. 如果你必须查看函数的实现(或文档)才能知道它做了什么,那么你应该努力寻找一个更好的名字,或者重新安排功能,以便将其放在具有更好名字的函数中。
G21: Understand the AlgorithmG21: 理解算法
Lots of very funny code is written because people don’t take the time to understand the algorithm. They get something to work by plugging in enough if statements and flags, without really stopping to consider what is really going on. 很多非常可笑的代码被写出来,是因为人们没有花时间去理解算法。他们通过插入足够的 if 语句和标志来让某些东西工作,而没有真正停下来考虑到底发生了什么。
Programming is often an exploration. You think you know the right algorithm for something, but then you wind up fiddling with it, prodding and poking at it, until you get it to “work.” How do you know it “works”? Because it passes the test cases you can think of. 编程通常是一种探索。你以为你知道某事的正确算法,但随后你开始摆弄它,修修补补,直到你让它“工作”。你怎么知道它“工作”了?因为它通过了你能想到的测试用例。
There is nothing wrong with this approach. Indeed, often it is the only way to get a function to do what you think it should. However, it is not sufficient to leave the quotation marks around the word “work.” 这种方法没有任何问题。事实上,这通常是让函数做你认为它应该做的事情的唯一方法。然而,仅仅让它停留在带引号的“工作”是不够的。
Before you consider yourself to be done with a function, make sure you understand how it works. It is not good enough that it passes all the tests. You must know10 that the solution is correct. 在你认为你完成了一个函数之前,确保你理解它是如何工作的。仅仅通过所有测试是不够的。你必须知道[10]解决方案是正确的。
- There is a difference between knowing how the code works and knowing whether the algorithm will do the job required of it. Being unsure that an algorithm is appropriate is often a fact of life. Being unsure what your code does is just laziness.
- 知道代码如何工作和知道算法是否能完成所需的任务是有区别的。不确定算法是否合适通常是生活中的事实。不确定你的代码在做什么则纯粹是懒惰。
Often the best way to gain this knowledge and understanding is to refactor the function into something that is so clean and expressive that it is obvious how it works. 通常,获得这种知识和理解的最好方法是将函数重构为非常整洁和富有表现力的东西,以至于它是如何工作的是显而易见的。
G22: Make Logical Dependencies PhysicalG22: 把逻辑依赖变为物理依赖
If one module depends upon another, that dependency should be physical, not just logical. The dependent module should not make assumptions (in other words, logical dependencies) about the module it depends upon. Rather it should explicitly ask that module for all the information it depends upon. 如果一个模块依赖于另一个模块,这种依赖应该是物理的,而不仅仅是逻辑的。依赖模块不应对其依赖的模块做出假设(换句话说,逻辑依赖)。相反,它应该明确地向该模块询问它所依赖的所有信息。
For example, imagine that you are writing a function that prints a plain text report of hours worked by employees. One class named HourlyReporter gathers all the data into a convenient form and then passes it to HourlyReportFormatter to print it. (See Listing 17-1.) 例如,假设你正在编写一个函数,打印员工工作时间的纯文本报告。一个名为 HourlyReporter 的类将所有数据收集成方便的形式,然后传递给 HourlyReportFormatter 来打印。(见清单 17-1。)
Listing 17-1 HourlyReporter.java 清单 17-1 HourlyReporter.java
public class HourlyReporter {
private HourlyReportFormatter formatter;
private List<LineItem> page;
private final int PAGE_SIZE = 55;
public HourlyReporter(HourlyReportFormatter formatter) {
this.formatter = formatter;
page = new ArrayList<LineItem>();
}
public void generateReport(List<HourlyEmployee> employees) {
for (HourlyEmployee e : employees) {
addLineItemToPage(e);
if (page.size() == PAGE_SIZE)
printAndClearItemList();
}
if (page.size() > 0)
printAndClearItemList();
}
private void printAndClearItemList() {
formatter.format(page);
page.clear();
}
private void addLineItemToPage(HourlyEmployee e) {
LineItem item = new LineItem();
item.name = e.getName();
item.hours = e.getTenthsWorked() / 10;
item.tenths = e.getTenthsWorked() % 10;
page.add(item);
}
public class LineItem {
public String name;
public int hours;
public int tenths;
}
}This code has a logical dependency that has not been physicalized. Can you spot it? It is the constant PAGE_SIZE. Why should the HourlyReporter know the size of the page? Page size should be the responsibility of the HourlyReportFormatter. 这段代码有一个未物理化的逻辑依赖。你能发现它吗?就是常量 PAGE_SIZE。为什么 HourlyReporter 应该知道页面大小?页面大小应该是 HourlyReportFormatter 的责任。
The fact that PAGE_SIZE is declared in HourlyReporter represents a misplaced responsibility [G17] that causes HourlyReporter to assume that it knows what the page size ought to be. Such an assumption is a logical dependency. HourlyReporter depends on the fact that HourlyReportFormatter can deal with page sizes of 55. If some implementation of HourlyReportFormatter could not deal with such sizes, then there would be an error. PAGE_SIZE 在 HourlyReporter 中声明的事实代表了职责错位 [G17],导致 HourlyReporter 假设它知道页面大小应该是多少。这种假设是一种逻辑依赖。HourlyReporter 依赖于 HourlyReportFormatter 能够处理 55 大小页面的事实。如果 HourlyReportFormatter 的某些实现无法处理这样的大小,那么就会出现错误。
We can physicalize this dependency by creating a new method in HourlyReport-Formatter named getMaxPageSize(). HourlyReporter will then call that function rather than using the PAGE_SIZE constant. 我们可以通过在 HourlyReportFormatter 中创建一个名为 getMaxPageSize() 的新方法来物理化这种依赖。然后 HourlyReporter 将调用该函数而不是使用 PAGE_SIZE 常量。
G23: Prefer Polymorphism to If/Else or Switch/CaseG23: 优先使用多态而非 If/Else 或 Switch/Case
This might seem a strange suggestion given the topic of Chapter 6. After all, in that chapter I make the point that switch statements are probably appropriate in the parts of the system where adding new functions is more likely than adding new types. 考虑到第 6 章的主题,这可能看起来是一个奇怪的建议。毕竟,在那一章中我指出,在系统中添加新函数比添加新类型更可能的部分,switch 语句可能是合适的。
First, most people use switch statements because it’s the obvious brute force solution, not because it’s the right solution for the situation. So this heuristic is here to remind us to consider polymorphism before using a switch. 首先,大多数人使用 switch 语句是因为它是显而易见的暴力解决方案,而不是因为它是针对该情况的正确解决方案。所以这个启发式规则在这里提醒我们在使用 switch 之前考虑多态。
Second, the cases where functions are more volatile than types are relatively rare. So every switch statement should be suspect. 其次,函数比类型更不稳定的情况相对较少。所以每个 switch 语句都应该是可疑的。
I use the following “ONE SWITCH” rule: There may be no more than one switch statement for a given type of selection. The cases in that switch statement must create polymorphic objects that take the place of other such switch statements in the rest of the system. 我使用以下“单次 SWITCH”规则:对于给定类型的选择,不得有超过一个 switch 语句。该 switch 语句中的 case 必须创建多态对象,以取代系统中其他地方的此类 switch 语句。
G24: Follow Standard ConventionsG24: 遵循标准约定
Every team should follow a coding standard based on common industry norms. This coding standard should specify things like where to declare instance variables; how to name classes, methods, and variables; where to put braces; and so on. The team should not need a document to describe these conventions because their code provides the examples. 每个团队都应该遵循基于常见行业规范的编码标准。这个编码标准应该指定诸如在哪里声明实例变量;如何命名类、方法和变量;大括号放在哪里;等等。团队不应该需要文档来描述这些约定,因为他们的代码提供了范例。
Everyone on the team should follow these conventions. This means that each team member must be mature enough to realize that it doesn’t matter a whit where you put your braces so long as you all agree on where to put them. 团队中的每个人都应该遵循这些约定。这意味着每个团队成员都必须足够成熟,意识到只要大家都同意大括号放在哪里,那么无论你把它放在哪里都无关紧要。
If you would like to know what conventions I follow, you’ll see them in the refactored code in Listing B-7 on page 394, through Listing B-14. 如果你想知道我遵循什么约定,你可以在第 394 页清单 B-7 到清单 B-14 的重构代码中看到它们。
G25: Replace Magic Numbers with Named ConstantsG25: 用命名常量替换魔术数
This is probably one of the oldest rules in software development. I remember reading it in the late sixties in introductory COBOL, FORTRAN, and PL/1 manuals. In general it is a bad idea to have raw numbers in your code. You should hide them behind well-named constants. 这可能是软件开发中最古老的规则之一。我记得在六十年代末的 COBOL、FORTRAN 和 PL/1 入门手册中读到过它。通常,在代码中使用原始数字是个坏主意。你应该将它们隐藏在命名良好的常量后面。
For example, the number 86,400 should be hidden behind the constant SECONDS_PER_DAY. If you are printing 55 lines per page, then the constant 55 should be hidden behind the constant LINES_PER_PAGE. 例如,数字 86,400 应该隐藏在常量 SECONDS_PER_DAY 后面。如果你每页打印 55 行,那么常量 55 应该隐藏在常量 LINES_PER_PAGE 后面。
Some constants are so easy to recognize that they don’t always need a named constant to hide behind so long as they are used in conjunction with very self-explanatory code. For example: 有些常量非常容易识别,只要它们与非常自解释的代码一起使用,就不总是需要命名常量来隐藏。例如:
double milesWalked = feetWalked/5280.0;
int dailyPay = hourlyRate * 8;
double circumference = radius * Math.PI * 2;Do we really need the constants FEET_PER_MILE, WORK_HOURS_PER_DAY, and TWO in the above examples? Clearly, the last case is absurd. There are some formulae in which constants are simply better written as raw numbers. You might quibble about the WORK_HOURS_PER_DAY case because the laws or conventions might change. On the other hand, that formula reads so nicely with the 8 in it that I would be reluctant to add 17 extra characters to the readers’ burden. And in the FEET_PER_MILE case, the number 5280 is so very well known and so unique a constant that readers would recognize it even if it stood alone on a page with no context surrounding it. 在上面的例子中,我们真的需要常量 FEET_PER_MILE、WORK_HOURS_PER_DAY 和 TWO 吗?显然,最后一种情况是荒谬的。有些公式中,常量直接写成原始数字会更好。你可能会对 WORK_HOURS_PER_DAY 的情况吹毛求疵,因为法律或惯例可能会改变。另一方面,那个公式里有个 8 读起来很顺畅,我不愿意给读者增加 17 个额外字符的负担。而在 FEET_PER_MILE 的情况下,数字 5280 是如此众所周知且独特的常量,即使它单独出现在页面上没有任何上下文,读者也能认出它。
Constants like 3.141592653589793 are also very well known and easily recognizable. However, the chance for error is too great to leave them raw. Every time someone sees 3.1415927535890793, they know that it is π, and so they fail to scrutinize it. (Did you catch the single-digit error?) We also don’t want people using 3.14, 3.14159, 3.142, and so forth. Therefore, it is a good thing that Math.PI has already been defined for us. 像 3.141592653589793 这样的常量也非常有名且易于识别。然而,让它们保持原始状态的出错几率太大了。每当有人看到 3.1415927535890793,他们就知道它是 π,因此他们不会去仔细检查它。(你发现那个单位数错误了吗?)我们也不希望人们使用 3.14、3.14159、3.142 等等。因此,Math.PI 已经被定义好了是一件好事。
The term “Magic Number” does not apply only to numbers. It applies to any token that has a value that is not self-describing. For example: 术语“魔术数”不仅适用于数字。它适用于任何具有非自描述值的标记。例如:
assertEquals(7777, Employee.find(“John Doe”).employeeNumber());There are two magic numbers in this assertion. The first is obviously 7777, though what it might mean is not obvious. The second magic number is “John Doe,” and again the intent is not clear. 在这个断言中有两个魔术数。第一个显然是 7777,虽然它可能意味着什么并不明显。第二个魔术数是“John Doe”,同样意图不明。
It turns out that “John Doe” is the name of employee #7777 in a well-known test database created by our team. Everyone in the team knows that when you connect to this database, it will have several employees already cooked into it with well-known values and attributes. It also turns out that “John Doe” represents the sole hourly employee in that test database. So this test should really read: 原来“John Doe”是我们团队创建的一个著名测试数据库中员工 #7777 的名字。团队中的每个人都知道,当你连接到这个数据库时,里面会有几个预设的员工,具有众所周知的值和属性。同样原来“John Doe”代表该测试数据库中唯一的计时员工。所以这个测试实际上应该写成:
assertEquals(
HOURLY_EMPLOYEE_ID,
Employee.find(HOURLY_EMPLOYEE_NAME).employeeNumber());G26: Be PreciseG26: 要精确
Expecting the first match to be the only match to a query is probably naive. Using floating point numbers to represent currency is almost criminal. Avoiding locks and/or transaction management because you don’t think concurrent update is likely is lazy at best. Declaring a variable to be an ArrayList when a List will due is overly constraining. Making all variables protected by default is not constraining enough. 期望查询的第一个匹配项是唯一匹配项可能是幼稚的。使用浮点数表示货币几乎是犯罪。因为你认为不太可能发生并发更新而避免使用锁和/或事务管理,往好了说也是懒惰。当 List 足够时声明变量为 ArrayList 是过度约束。默认将所有变量设为 protected 则约束不够。
When you make a decision in your code, make sure you make it precisely. Know why you have made it and how you will deal with any exceptions. Don’t be lazy about the precision of your decisions. If you decide to call a function that might return null, make sure you check for null. If you query for what you think is the only record in the database, make sure your code checks to be sure there aren’t others. If you need to deal with currency, use integers11 and deal with rounding appropriately. If there is the possibility of concurrent update, make sure you implement some kind of locking mechanism. 当你在代码中做出决定时,确保你做得精确。知道你为什么这样做以及你将如何处理任何异常。不要对你决定的精确性犯懒。如果你决定调用一个可能返回 null 的函数,确保你检查了 null。如果你查询你认为是数据库中唯一的记录,确保你的代码检查并确认没有其他记录。如果你需要处理货币,使用整数[11]并适当地处理舍入。如果有可能发生并发更新,确保你实现了某种锁定机制。
- Or better yet, a Money class that uses integers.
- 或者更好的是,一个使用整数的 Money 类。
Ambiguities and imprecision in code are either a result of disagreements or laziness. In either case they should be eliminated. 代码中的歧义和不精确要么是分歧的结果,要么是懒惰的结果。无论哪种情况,它们都应该被消除。
G27: Structure over ConventionG27: 结构优于约定
Enforce design decisions with structure over convention. Naming conventions are good, but they are inferior to structures that force compliance. For example, switch/cases with nicely named enumerations are inferior to base classes with abstract methods. No one is forced to implement the switch/case statement the same way each time; but the base classes do enforce that concrete classes have all abstract methods implemented. 用结构而非约定来强制执行设计决策。命名约定很好,但它们不如强制合规的结构。例如,带有命名良好的枚举的 switch/case 不如带有抽象方法的基类。没人被迫每次都以相同的方式实现 switch/case 语句;但基类确实强制具体类实现所有抽象方法。
G28: Encapsulate ConditionalsG28: 封装条件
Boolean logic is hard enough to understand without having to see it in the context of an if or while statement. Extract functions that explain the intent of the conditional. 布尔逻辑已经够难理解的了,不必非得在 if 或 while 语句的上下文中看它。提取能够解释条件意图的函数。
For example: 例如:
if (shouldBeDeleted(timer))is preferable to 优于
if (timer.hasExpired() && !timer.isRecurrent())G29: Avoid Negative ConditionalsG29: 避免否定性条件
Negatives are just a bit harder to understand than positives. So, when possible, conditionals should be expressed as positives. For example: 否定式只比肯定式稍微难理解一点。所以,如果可能,条件应表示为肯定式。例如:
if (buffer.shouldCompact())is preferable to 优于
if (!buffer.shouldNotCompact())G30: Functions Should Do One ThingG30: 函数应该只做一件事
It is often tempting to create functions that have multiple sections that perform a series of operations. Functions of this kind do more than one thing, and should be converted into many smaller functions, each of which does one thing. 经常有人试图创建包含多个部分的函数来执行一系列操作。这种函数做的事情不止一件,应该被转换为许多更小的函数,每个函数只做一件事。
For example: 例如:
public void pay() {
for (Employee e : employees) {
if (e.isPayday()) {
Money pay = e.calculatePay();
e.deliverPay(pay);
}
}
}This bit of code does three things. It loops over all the employees, checks to see whether each employee ought to be paid, and then pays the employee. This code would be better written as: 这段代码做了三件事。它遍历所有员工,检查每个员工是否应该被支付,然后支付员工。这段代码最好写成:
public void pay() {
for (Employee e : employees)
payIfNecessary(e);
}
private void payIfNecessary(Employee e) {
if (e.isPayday())
calculateAndDeliverPay(e);
}
private void calculateAndDeliverPay(Employee e) {
Money pay = e.calculatePay();
e.deliverPay(pay);
}Each of these functions does one thing. (See “Do One Thing” on page 35.) 这些函数中的每一个都只做一件事。(参见第 35 页的“只做一件事”。)
G31: Hidden Temporal CouplingsG31: 隐藏的时序耦合
Temporal couplings are often necessary, but you should not hide the coupling. Structure the arguments of your functions such that the order in which they should be called is obvious. Consider the following: 时序耦合通常是必要的,但不应隐藏这种耦合。构造函数的参数,使得它们被调用的顺序显而易见。考虑以下代码:
public class MoogDiver {
Gradient gradient;
List<Spline> splines;
public void dive(String reason) {
saturateGradient();
reticulateSplines();
diveForMoog(reason);
}
…
}The order of the three functions is important. You must saturate the gradient before you can reticulate the splines, and only then can you dive for the moog. Unfortunately, the code does not enforce this temporal coupling. Another programmer could call reticulate-Splines before saturateGradient was called, leading to an UnsaturatedGradientException. A better solution is: 这三个函数的顺序很重要。你必须在生成样条曲线之前饱和梯度,只有那样你才能潜水寻找 moog。不幸的是,代码没有强制执行这种时序耦合。另一个程序员可能会在 saturateGradient 被调用之前调用 reticulateSplines,导致 UnsaturatedGradientException。更好的解决方案是:
public class MoogDiver {
Gradient gradient;
List<Spline> splines;
public void dive(String reason) {
Gradient gradient = saturateGradient();
List<Spline> splines = reticulateSplines(gradient);
diveForMoog(splines, reason);
}
…
}This exposes the temporal coupling by creating a bucket brigade. Each function produces a result that the next function needs, so there is no reasonable way to call them out of order. 这通过创建一个“水桶传递”队列表露了时序耦合。每个函数产生下一个函数所需的结果,所以没有合理的方法可以乱序调用它们。
You might complain that this increases the complexity of the functions, and you’d be right. But that extra syntactic complexity exposes the true temporal complexity of the situation. 你可能会抱怨这增加了函数的复杂性,你是对的。但那额外的语法复杂性暴露了情况的真实时序复杂性。
Note that I left the instance variables in place. I presume that they are needed by private methods in the class. Even so, I want the arguments in place to make the temporal coupling explicit. 注意我保留了实例变量。我假设类中的私有方法需要它们。即便如此,我希望参数到位以使时序耦合明确。
G32: Don’t Be ArbitraryG32: 不要随意
Have a reason for the way you structure your code, and make sure that reason is communicated by the structure of the code. If a structure appears arbitrary, others will feel empowered to change it. If a structure appears consistently throughout the system, others will use it and preserve the convention. For example, I was recently merging changes to FitNesse and discovered that one of our committers had done this: 为你构建代码的方式找一个理由,并确保该理由通过代码结构传达出来。如果一个结构看起来是随意的,其他人就会觉得有权更改它。如果一个结构在整个系统中一致出现,其他人就会使用它并保留该约定。例如,我最近在合并 FitNesse 的更改时,发现我们的一个提交者做了这个:
public class AliasLinkWidget extends ParentWidget
{
public static class VariableExpandingWidgetRoot {
…
…
}The problem with this was that VariableExpandingWidgetRoot had no need to be inside the scope of AliasLinkWidget. Moreover, other unrelated classes made use of AliasLinkWidget.VariableExpandingWidgetRoot. These classes had no need to know about AliasLinkWidget. 这样做的问题在于 VariableExpandingWidgetRoot 不需要处于 AliasLinkWidget 的作用域内。而且,其他不相关的类使用了 AliasLinkWidget.VariableExpandingWidgetRoot。这些类不需要知道 AliasLinkWidget。
Perhaps the programmer had plopped the VariableExpandingWidgetRoot into AliasWidget as a matter of convenience, or perhaps he thought it really needed to be scoped inside AliasWidget. Whatever the reason, the result wound up being arbitrary. Public classes that are not utilities of some other class should not be scoped inside another class. The convention is to make them public at the top level of their package. 也许程序员是为了方便才把 VariableExpandingWidgetRoot 扔进 AliasWidget 的,或者也许他认为它真的需要在 AliasWidget 内部定界。无论什么原因,结果都是随意的。不是其他类工具的公共类不应该在另一个类内部定界。约定是让它们在包的顶层成为公共类。
G33: Encapsulate Boundary ConditionsG33: 封装边界条件
Boundary conditions are hard to keep track of. Put the processing for them in one place. Don’t let them leak all over the code. We don’t want swarms of +1s and -1s scattered hither and yon. Consider this simple example from FIT: 边界条件很难追踪。把它们的处理放在一个地方。不要让它们泄露到代码各处。我们不希望到处散布着 +1 和 -1。考虑这个来自 FIT 的简单例子:
if(level + 1 < tags.length)
{
parts = new Parse(body, tags, level + 1, offset + endTag);
body = null;
}Notice that level+1 appears twice. This is a boundary condition that should be encapsulated within a variable named something like nextLevel. 注意 level + 1 出现了两次。这是一个边界条件,应该封装在一个名为 nextLevel 之类的变量中。
int nextLevel = level + 1;
if(nextLevel < tags.length)
{
parts = new Parse(body, tags, nextLevel, offset + endTag);
body = null;
}G34: Functions Should Descend Only One Level of AbstractionG34: 函数应只下降一个抽象层级
The statements within a function should all be written at the same level of abstraction, which should be one level below the operation described by the name of the function. This may be the hardest of these heuristics to interpret and follow. Though the idea is plain enough, humans are just far too good at seamlessly mixing levels of abstraction. Consider, for example, the following code taken from FitNesse: 函数内的语句应该都写在同一抽象层级上,该层级应比函数名描述的操作低一层。这可能是这些启发式规则中最难解释和遵循的。虽然道理很简单,但人类实在太擅长无缝混合抽象层级了。例如,考虑下面取自 FitNesse 的代码:
public String render() throws Exception
{
StringBuffer html = new StringBuffer(“<hr”);
if(size > 0)
html.append(” size=\“”).append(size + 1).append(”\“”);
html.append(“>”);
return html.toString();
}A moment’s study and you can see what’s going on. This function constructs the HTML tag that draws a horizontal rule across the page. The height of that rule is specified in the size variable. 稍微研究一下你就能看出发生了什么。这个函数构造了在页面上绘制水平线的 HTML 标签。该线的高度由 size 变量指定。
Now look again. This method is mixing at least two levels of abstraction. The first is the notion that a horizontal rule has a size. The second is the syntax of the HR tag itself. This code comes from the HruleWidget module in FitNesse. This module detects a row of four or more dashes and converts it into the appropriate HR tag. The more dashes, the larger the size. 现在再看一遍。这个方法混合了至少两个抽象层级。第一个是水平线有大小的概念。第二个是 HR 标签本身的语法。这段代码来自 FitNesse 中的 HruleWidget 模块。该模块检测一行四个或更多的破折号,并将其转换为适当的 HR 标签。破折号越多,尺寸越大。
I refactored this bit of code as follows. Note that I changed the name of the size field to reflect its true purpose. It held the number of extra dashes. 我将这段代码重构如下。注意我更改了 size 字段的名称以反映其真实用途。它保存的是额外破折号的数量。
public String render() throws Exception
{
HtmlTag hr = new HtmlTag(“hr”);
if (extraDashes > 0)
hr.addAttribute(“size”, hrSize(extraDashes));
return hr.html();
}
private String hrSize(int height)
{
int hrSize = height + 1;
return String.format(“%d”, hrSize);
}This change separates the two levels of abstraction nicely. The render function simply constructs an HR tag, without having to know anything about the HTML syntax of that tag. The HtmlTag module takes care of all the nasty syntax issues. 这个更改很好地分离了两个抽象层级。render 函数只是构造一个 HR 标签,而无需知道任何关于该标签的 HTML 语法。HtmlTag 模块负责所有讨厌的语法问题。
Indeed, by making this change I caught a subtle error. The original code did not put the closing slash on the HR tag, as the XHTML standard would have it. (In other words, it emitted
instead of
.) The HtmlTag module had been changed to conform to XHTML long ago. 事实上,通过做这个更改,我捕捉到了一个微妙的错误。原始代码没有按照 XHTML 标准在 HR 标签上加上结束斜杠。(换句话说,它生成了
<hr> 而不是 <hr/>。)HtmlTag 模块很久以前就已经修改为符合 XHTML 了。Separating levels of abstraction is one of the most important functions of refactoring, and it’s one of the hardest to do well. As an example, look at the code below. This was my first attempt at separating the abstraction levels in the HruleWidget.render method. 分离抽象层级是重构最重要的功能之一,也是最难做好的功能之一。作为一个例子,看看下面的代码。这是我第一次尝试分离 HruleWidget.render 方法中的抽象层级。
public String render() throws Exception
{
HtmlTag hr = new HtmlTag(“hr”);
if (size > 0) {
hr.addAttribute(“size”, “”+(size+1));
}
return hr.html();
}My goal, at this point, was to create the necessary separation and get the tests to pass. I accomplished that goal easily, but the result was a function that still had mixed levels of abstraction. In this case the mixed levels were the construction of the HR tag and the interpretation and formatting of the size variable. This points out that when you break a function along lines of abstraction, you often uncover new lines of abstraction that were obscured by the previous structure. 此时我的目标是创建必要的分离并让测试通过。我很容易就实现了那个目标,但结果是一个仍然混合了抽象层级的函数。在这种情况下,混合的层级是 HR 标签的构造与 size 变量的解释和格式化。这指出了当你沿着抽象线分解函数时,你经常会发现被先前结构掩盖的新抽象线。
G35: Keep Configurable Data at High LevelsG35: 将可配置数据保持在高层级
If you have a constant such as a default or configuration value that is known and expected at a high level of abstraction, do not bury it in a low-level function. Expose it as an argument to that low-level function called from the high-level function. Consider the following code from FitNesse: 如果你有一个常量(如默认值或配置值),它在高抽象层级是已知和预期的,不要把它埋在低层级函数中。把它作为从高层级函数调用该低层级函数的参数暴露出来。考虑以下来自 FitNesse 的代码:
public static void main(String[] args) throws Exception
{
Arguments arguments = parseCommandLine(args);
…
}
public class Arguments
{
public static final String DEFAULT_PATH = “.”;
public static final String DEFAULT_ROOT = “FitNesseRoot”;
public static final int DEFAULT_PORT = 80;
public static final int DEFAULT_VERSION_DAYS = 14;
…
}The command-line arguments are parsed in the very first executable line of FitNesse. The default values of those arguments are specified at the top of the Argument class. You don’t have to go looking in low levels of the system for statements like this one: 命令行参数在 FitNesse 的第一行可执行代码中被解析。这些参数的默认值在 Argument 类的顶部指定。你不必去系统的低层寻找像这样的语句:
if (arguments.port == 0) // use 80 by defaultThe configuration constants reside at a very high level and are easy to change. They get passed down to the rest of the application. The lower levels of the application do not own the values of these constants. 配置常量驻留在非常高的层级,易于更改。它们被向下传递给应用程序的其余部分。应用程序的低层级不拥有这些常量的值。
G36: Avoid Transitive NavigationG36: 避免传递性导航
In general we don’t want a single module to know much about its collaborators. More specifically, if A collaborates with B, and B collaborates with C, we don’t want modules that use A to know about C. (For example, we don’t want a.getB().getC().doSomething();.) 通常我们不希望单个模块对其合作者知道得太多。更具体地说,如果 A 与 B 合作,B 与 C 合作,我们不希望使用 A 的模块知道 C。(例如,我们不希望 a.getB().getC().doSomething();。)
This is sometimes called the Law of Demeter. The Pragmatic Programmers call it “Writing Shy Code.”12 In either case it comes down to making sure that modules know only about their immediate collaborators and do not know the navigation map of the whole system. 这有时被称为德墨忒尔定律(Law of Demeter)。《程序员修炼之道》的作者称之为“编写害羞的代码”[12]。无论哪种情况,归根结底都是要确保模块只知道它们的直接合作者,而不知道整个系统的导航图。
- [PRAG], p. 138.
- [PRAG], 第 138 页.
If many modules used some form of the statement a.getB().getC(), then it would be difficult to change the design and architecture to interpose a Q between B and C. You’d have to find every instance of a.getB().getC() and convert it to a.getB().getQ().getC(). This is how architectures become rigid. Too many modules know too much about the architecture. 如果许多模块使用某种形式的语句 a.getB().getC(),那么就很难改变设计和架构,在 B 和 C 之间插入一个 Q。你必须找到 a.getB().getC() 的每一个实例并将其转换为 a.getB().getQ().getC()。这就是架构变得僵化的原因。太多的模块对架构知道得太多。
Rather we want our immediate collaborators to offer all the services we need. We should not have to roam through the object graph of the system, hunting for the method we want to call. Rather we should simply be able to say: 相反,我们希望我们的直接合作者提供我们需要的所有服务。我们不应该必须在系统的对象图中漫游,寻找我们想要调用的方法。相反,我们应该能够简单地说:
myCollaborator.doSomething().JAVA (Java特性)
J1: Avoid Long Import Lists by Using WildcardsJ1: 使用通配符避免过长的导入列表
If you use two or more classes from a package, then import the whole package with 如果你从一个包中使用了两个或更多的类,那么导入整个包:
import package.*;Long lists of imports are daunting to the reader. We don’t want to clutter up the tops of our modules with 80 lines of imports. Rather we want the imports to be a concise statement about which packages we collaborate with. 长长的导入列表会让读者望而生畏。我们不希望用 80 行导入来充斥我们模块的顶部。相反,我们希望导入是对我们与之合作的包的简明陈述。
Specific imports are hard dependencies, whereas wildcard imports are not. If you specifically import a class, then that class must exist. But if you import a package with a wildcard, no particular classes need to exist. The import statement simply adds the package to the search path when hunting for names. So no true dependency is created by such imports, and they therefore serve to keep our modules less coupled. 具体导入是硬依赖,而通配符导入不是。如果你具体导入一个类,那么那个类必须存在。但如果你用通配符导入一个包,不需要存在特定的类。导入语句只是在寻找名称时将包添加到搜索路径中。所以这种导入没有创建真正的依赖,因此它们有助于保持我们的模块耦合度更低。
There are times when the long list of specific imports can be useful. For example, if you are dealing with legacy code and you want to find out what classes you need to build mocks and stubs for, you can walk down the list of specific imports to find out the true qualified names of all those classes and then put the appropriate stubs in place. However, this use for specific imports is very rare. Furthermore, most modern IDEs will allow you to convert the wildcarded imports to a list of specific imports with a single command. So even in the legacy case it’s better to import wildcards. 有时长长的具体导入列表可能是有用的。例如,如果你正在处理遗留代码,并且想找出你需要为哪些类构建 mock 和 stub,你可以沿着具体导入列表找出所有这些类的真实限定名,然后放置适当的 stub。然而,具体导入的这种用途非常罕见。此外,大多数现代 IDE 都允许你用一个命令将通配符导入转换为具体导入列表。所以即使在遗留情况下,导入通配符也更好。
Wildcard imports can sometimes cause name conflicts and ambiguities. Two classes with the same name, but in different packages, will need to be specifically imported, or at least specifically qualified when used. This can be a nuisance but is rare enough that using wildcard imports is still generally better than specific imports. 通配符导入有时会导致名称冲突和歧义。两个同名但在不同包中的类,将需要被具体导入,或者至少在使用时具体限定。这可能是一个麻烦,但非常罕见,所以使用通配符导入通常还是比具体导入好。
J2: Don’t Inherit ConstantsJ2: 不要继承常量
I have seen this several times and it always makes me grimace. A programmer puts some constants in an interface and then gains access to those constants by inheriting that interface. Take a look at the following code: 我见过这种情况几次,每次都让我皱眉。程序员把一些常量放在接口中,然后通过继承该接口来获得对这些常量的访问权。看看下面的代码:
public class HourlyEmployee extends Employee {
private int tenthsWorked;
private double hourlyRate;
public Money calculatePay() {
int straightTime = Math.min(tenthsWorked, TENTHS_PER_WEEK);
int overTime = tenthsWorked - straightTime;
return new Money(
hourlyRate * (tenthsWorked + OVERTIME_RATE * overTime)
);
}
…
}Where did the constants TENTHS_PER_WEEK and OVERTIME_RATE come from? They might have come from class Employee; so let’s take a look at that: 常量 TENTHS_PER_WEEK 和 OVERTIME_RATE 从哪里来?它们可能来自 Employee 类;所以让我们看看那个类:
public abstract class Employee implements PayrollConstants {
public abstract boolean isPayday();
public abstract Money calculatePay();
public abstract void deliverPay(Money pay);
}Nope, not there. But then where? Look closely at class Employee. It implements PayrollConstants. 不,不在那里。那在哪里?仔细看 Employee 类。它实现了 PayrollConstants。
public interface PayrollConstants {
public static final int TENTHS_PER_WEEK = 400;
public static final double OVERTIME_RATE = 1.5;
}This is a hideous practice! The constants are hidden at the top of the inheritance hierarchy. Ick! Don’t use inheritance as a way to cheat the scoping rules of the language. Use a static import instead. 这是一种可怕的做法!常量隐藏在继承层次结构的顶端。真恶心!不要使用继承作为欺骗语言作用域规则的方法。改用静态导入。
import static PayrollConstants.*;
public class HourlyEmployee extends Employee {
private int tenthsWorked;
private double hourlyRate;
public Money calculatePay() {
int straightTime = Math.min(tenthsWorked, TENTHS_PER_WEEK);
int overTime = tenthsWorked - straightTime;
return new Money(
hourlyRate * (tenthsWorked + OVERTIME_RATE * overTime)
);
}
…
}J3: Constants versus EnumsJ3: 常量与枚举
Now that enums have been added to the language (Java 5), use them! Don’t keep using the old trick of public static final ints. The meaning of ints can get lost. The meaning of enums cannot, because they belong to an enumeration that is named. 既然枚举已经被添加到语言中(Java 5),使用它们!不要继续使用 public static final int 的老把戏。int 的含义可能会丢失。枚举的含义不会,因为它们属于一个被命名的枚举。
What’s more, study the syntax for enums carefully. They can have methods and fields. This makes them very powerful tools that allow much more expression and flexibility than ints. Consider this variation on the payroll code: 更重要的是,仔细研究枚举的语法。它们可以有方法和字段。这使它们成为非常强大的工具,比 int 允许更多的表达和灵活性。考虑薪资代码的这个变体:
public class HourlyEmployee extends Employee {
private int tenthsWorked;
HourlyPayGrade grade;
public Money calculatePay() {
int straightTime = Math.min(tenthsWorked, TENTHS_PER_WEEK);
int overTime = tenthsWorked - straightTime;
return new Money(
grade.rate() * (tenthsWorked + OVERTIME_RATE * overTime)
);
}
…
}
public enum HourlyPayGrade {
APPRENTICE {
public double rate() {
return 1.0;
}
},
LEUTENANT_JOURNEYMAN {
public double rate() {
return 1.2;
}
},
JOURNEYMAN {
public double rate() {
return 1.5;
}
},
MASTER {
public double rate() {
return 2.0;
}
};
public abstract double rate();
}NAMES (名称)
N1: Choose Descriptive NamesN1: 选择描述性名称
Don’t be too quick to choose a name. Make sure the name is descriptive. Remember that meanings tend to drift as software evolves, so frequently reevaluate the appropriateness of the names you choose. 不要太快选择名字。确保名字具有描述性。记住随着软件的演变,含义往往会漂移,所以要经常重新评估你选择的名字是否合适。
This is not just a “feel-good” recommendation. Names in software are 90 percent of what make software readable. You need to take the time to choose them wisely and keep them relevant. Names are too important to treat carelessly. 这不仅仅是一个“感觉良好”的建议。软件中的名称构成了软件可读性的 90%。你需要花时间明智地选择它们并保持它们的相关性。名字太重要了,不能草率对待。
Consider the code below. What does it do? If I show you the code with well-chosen names, it will make perfect sense to you, but like this it’s just a hodge-podge of symbols and magic numbers. 考虑下面的代码。它做什么?如果我向你展示带有精心选择名称的代码,你会觉得非常有道理,但像这样,它只是符号和魔术数的大杂烩。
public int x() {
int q = 0;
int z = 0;
for (int kk = 0; kk < 10; kk++) {
if (l[z] == 10)
{
q += 10 + (l[z + 1] + l[z + 2]);
z += 1;
}
else if (l[z] + l[z + 1] == 10)
{
q += 10 + l[z + 2];
z += 2;
} else {
q += l[z] + l[z + 1];
z += 2;
}
}
return q;
}Here is the code the way it should be written. This snippet is actually less complete than the one above. Yet you can infer immediately what it is trying to do, and you could very likely write the missing functions based on that inferred meaning. The magic numbers are no longer magic, and the structure of the algorithm is compellingly descriptive. 这是代码应该被写的样子。这个片段实际上比上面的那个不完整。然而你可以立即推断出它试图做什么,而且你很有可能根据推断出的含义写出缺失的函数。魔术数不再魔术,算法的结构具有令人信服的描述性。
public int score() {
int score = 0;
int frame = 0;
for (int frameNumber = 0; frameNumber < 10; frameNumber++) {
if (isStrike(frame)) {
score += 10 + nextTwoBallsForStrike(frame);
frame += 1;
} else if (isSpare(frame)) {
score += 10 + nextBallForSpare(frame);
frame += 2;
} else {
score += twoBallsInFrame(frame);
frame += 2;
}
}
return score;
}The power of carefully chosen names is that they overload the structure of the code with description. That overloading sets the readers’ expectations about what the other functions in the module do. You can infer the implementation of isStrike() by looking at the code above. When you read the isStrike method, it will be “pretty much what you expected.”13 精心选择名称的力量在于它们用描述重载了代码的结构。这种重载设定了读者对模块中其他函数功能的期望。你可以通过查看上面的代码推断 isStrike() 的实现。当你阅读 isStrike 方法时,它将是“差不多就是你期望的那样”[13]。
- See Ward Cunningham’s quote on page 11.
- 参见第 11 页 Ward Cunningham 的引言。
private boolean isStrike(int frame) {
return rolls[frame] == 10;
}N2: Choose Names at the Appropriate Level of AbstractionN2: 在适当的抽象层级选择名称
Don’t pick names that communicate implementation; choose names the reflect the level of abstraction of the class or function you are working in. This is hard to do. Again, people are just too good at mixing levels of abstractions. Each time you make a pass over your code, you will likely find some variable that is named at too low a level. You should take the opportunity to change those names when you find them. Making code readable requires a dedication to continuous improvement. Consider the Modem interface below: 不要选择传达实现的名称;选择反映你正在工作的类或函数抽象层级的名称。这很难做到。同样,人们太擅长混合抽象层级了。每次你检查你的代码时,你很可能会发现一些命名层级过低的变量。当你发现它们时,你应该借机更改这些名称。让代码可读需要致力于持续改进。考虑下面的 Modem 接口:
public interface Modem {
boolean dial(String phoneNumber);
boolean disconnect();
boolean send(char c);
char recv();
String getConnectedPhoneNumber();
}At first this looks fine. The functions all seem appropriate. Indeed, for many applications they are. But now consider an application in which some modems aren’t connected by dialling. Rather they are connected permanently by hard wiring them together (think of the cable modems that provide Internet access to most homes nowadays). Perhaps some are connected by sending a port number to a switch over a USB connection. Clearly the notion of phone numbers is at the wrong level of abstraction. A better naming strategy for this scenario might be: 起初这看起来不错。函数似乎都很合适。确实,对于许多应用程序来说它们是合适的。但现在考虑一个应用程序,其中有些调制解调器不是通过拨号连接的。相反,它们是通过硬连线永久连接在一起的(想想现在大多数家庭提供互联网接入的有线调制解调器)。也许有些是通过 USB 连接向交换机发送端口号来连接的。显然,电话号码的概念处于错误的抽象层级。这种场景下更好的命名策略可能是:
public interface Modem {
boolean connect(String connectionLocator);
boolean disconnect();
boolean send(char c);
char recv();
String getConnectedLocator();
}Now the names don’t make any commitments about phone numbers. They can still be used for phone numbers, or they could be used for any other kind of connection strategy. 现在这些名称没有对电话号码做出任何承诺。它们仍然可以用于电话号码,或者它们可以用于任何其他类型的连接策略。
N3: Use Standard Nomenclature Where PossibleN3: 尽可能使用标准命名法
Names are easier to understand if they are based on existing convention or usage. For example, if you are using the DECORATOR pattern, you should use the word Decorator in the names of the decorating classes. For example, AutoHangupModemDecorator might be the name of a class that decorates a Modem with the ability to automatically hang up at the end of a session. 如果基于现有的约定或用法,名称更容易理解。例如,如果你正在使用 DECORATOR(装饰器)模式,你应该在装饰类的名称中使用 Decorator 这个词。例如,AutoHangupModemDecorator 可能是一个用自动挂断能力装饰 Modem 的类的名称。
Patterns are just one kind of standard. In Java, for example, functions that convert objects to string representations are often named toString. It is better to follow conventions like these than to invent your own. 模式只是标准的一种。例如在 Java 中,将对象转换为字符串表示的函数通常命名为 toString。遵循像这样的约定比发明你自己的要好。
Teams will often invent their own standard system of names for a particular project. Eric Evans refers to this as a ubiquitous language for the project.14 Your code should use the terms from this language extensively. In short, the more you can use names that are overloaded with special meanings that are relevant to your project, the easier it will be for readers to know what your code is talking about. 团队通常会为特定项目发明他们自己的标准命名系统。Eric Evans 将此称为项目的通用语言[14]。你的代码应该广泛使用这种语言中的术语。简而言之,你越能使用那些承载了与你项目相关的特殊含义的名称,读者就越容易知道你的代码在说什么。
- [DDD].
- [DDD](领域驱动设计)。
N4: Unambiguous NamesN4: 无歧义的名称
Choose names that make the workings of a function or variable unambiguous. Consider this example from FitNesse: 选择使函数或变量的工作方式无歧义的名称。考虑这个来自 FitNesse 的例子:
private String doRename() throws Exception
{
if(refactorReferences)
renameReferences();
renamePage();
pathToRename.removeNameFromEnd();
pathToRename.addNameToEnd(newName);
return PathParser.render(pathToRename);
}The name of this function does not say what the function does except in broad and vague terms. This is emphasized by the fact that there is a function named renamePage inside the function named doRename! What do the names tell you about the difference between the two functions? Nothing. 这个函数的名字除了宽泛和模糊的术语外,没有说明函数做了什么。名为 doRename 的函数内部还有一个名为 renamePage 的函数,这更加强调了这一点!这些名字告诉你这两个函数之间的区别了吗?没有。
A better name for that function is renamePageAndOptionallyAllReferences. This may seem long, and it is, but it’s only called from one place in the module, so it’s explanatory value outweighs the length. 该函数更好的名字是 renamePageAndOptionallyAllReferences。这可能看起来很长,确实很长,但它只在模块中的一个地方被调用,所以它的解释价值超过了长度。
N5: Use Long Names for Long ScopesN5: 为长作用域使用长名称
The length of a name should be related to the length of the scope. You can use very short variable names for tiny scopes, but for big scopes you should use longer names. 名称的长度应该与作用域的长度相关。你可以在极小的作用域中使用非常短的变量名,但在大作用域中你应该使用更长的名字。
Variable names like i and j are just fine if their scope is five lines long. Consider this snippet from the old standard “Bowling Game”: 如果变量 i 和 j 的作用域只有五行长,那它们就很好。考虑这个来自老标准“保龄球游戏”的片段:
private void rollMany(int n, int pins)
{
for (int i=0; i<n; i++)
g.roll(pins);
}This is perfectly clear and would be obfuscated if the variable i were replaced with something annoying like rollCount. On the other hand, variables and functions with short names lose their meaning over long distances. So the longer the scope of the name, the longer and more precise the name should be. 这非常清楚,如果把变量 i 换成像 rollCount 这样恼人的东西,反而会变得模糊。另一方面,短名称的变量和函数在长距离上会失去其意义。所以名称的作用域越长,名称就应该越长、越精确。
N6: Avoid EncodingsN6: 避免编码
Names should not be encoded with type or scope information. Prefixes such as m_ or f are useless in today’s environments. Also project and/or subsystem encodings such as vis_ (for visual imaging system) are distracting and redundant. Again, today’s environments provide all that information without having to mangle the names. Keep your names free of Hungarian pollution. 名称不应编码类型或作用域信息。在今天的环境中,像 m_ 或 f 这样的前缀是无用的。同样,项目和/或子系统的编码如 vis_(用于视觉成像系统)也是干扰和冗余的。再次强调,今天的环境提供了所有这些信息,而无需破坏名称。让你的名字免受匈牙利命名法的污染。
N7: Names Should Describe Side-EffectsN7: 名称应描述副作用
Names should describe everything that a function, variable, or class is or does. Don’t hide side effects with a name. Don’t use a simple verb to describe a function that does more than just that simple action. For example, consider this code from TestNG: 名称应描述函数、变量或类是什么或做什么的一切。不要用名称隐藏副作用。不要用简单的动词来描述一个不仅仅做那个简单动作的函数。例如,考虑这段来自 TestNG 的代码:
public ObjectOutputStream getOos() throws IOException {
if (m_oos == null) {
m_oos = new ObjectOutputStream(m_socket.getOutputStream());
}
return m_oos;
}This function does a bit more than get an “oos”; it creates the “oos” if it hasn’t been created already. Thus, a better name might be createOrReturnOos. 这个函数做的不仅仅是获取一个“oos”;如果还没创建,它会创建“oos”。因此,一个更好的名字可能是 createOrReturnOos。
TESTS (测试)
T1: Insufficient TestsT1: 测试不足
How many tests should be in a test suite? Unfortunately, the metric many programmers use is “That seems like enough.” A test suite should test everything that could possibly break. The tests are insufficient so long as there are conditions that have not been explored by the tests or calculations that have not been validated. 测试套件中应该有多少测试?不幸的是,许多程序员使用的指标是“看起来够了”。一个测试套件应该测试所有可能崩溃的东西。只要还有未被测试探索的条件或未被验证的计算,测试就是不足的。
T2: Use a Coverage Tool!T2: 使用覆盖率工具!
Coverage tools reports gaps in your testing strategy. They make it easy to find modules, classes, and functions that are insufficiently tested. Most IDEs give you a visual indication, marking lines that are covered in green and those that are uncovered in red. This makes it quick and easy to find if or catch statements whose bodies haven’t been checked. 覆盖率工具报告你测试策略中的缺口。它们使查找测试不足的模块、类和函数变得容易。大多数 IDE 会给你视觉指示,用绿色标记已覆盖的行,用红色标记未覆盖的行。这使得查找未被检查的 if 或 catch 语句体变得快速容易。
T3: Don’t Skip Trivial TestsT3: 不要跳过琐碎的测试
They are easy to write and their documentary value is higher than the cost to produce them. 它们很容易写,而且它们的文档价值高于编写它们的成本。
T4: An Ignored Test Is a Question about an AmbiguityT4: 被忽略的测试是对歧义的疑问
Sometimes we are uncertain about a behavioral detail because the requirements are unclear. We can express our question about the requirements as a test that is commented out, or as a test that annotated with @Ignore. Which you choose depends upon whether the ambiguity is about something that would compile or not. 有时因为需求不明确,我们对行为细节不确定。我们可以将关于需求的疑问表达为被注释掉的测试,或者用 @Ignore 注解的测试。你选择哪种取决于歧义是否关于某些可以编译的东西。
T5: Test Boundary ConditionsT5: 测试边界条件
Take special care to test boundary conditions. We often get the middle of an algorithm right but misjudge the boundaries. 特别注意测试边界条件。我们经常搞对了算法的中间部分,但误判了边界。
T6: Exhaustively Test Near BugsT6: 在 Bug 附近进行穷尽测试
Bugs tend to congregate. When you find a bug in a function, it is wise to do an exhaustive test of that function. You’ll probably find that the bug was not alone. Bug 倾向于扎堆。当你在一个函数中发现一个 bug 时,对该函数进行穷尽测试是明智的。你可能会发现那个 bug 并不孤单。
T7: Patterns of Failure Are RevealingT7: 失败的模式具有启示性
Sometimes you can diagnose a problem by finding patterns in the way the test cases fail. This is another argument for making the test cases as complete as possible. Complete test cases, ordered in a reasonable way, expose patterns. 有时你可以通过发现测试用例失败方式的模式来诊断问题。这是使测试用例尽可能完整的另一个理由。完整的、按合理方式排序的测试用例会暴露模式。
As a simple example, suppose you noticed that all tests with an input larger than five characters failed? Or what if any test that passed a negative number into the second argument of a function failed? Sometimes just seeing the pattern of red and green on the test report is enough to spark the “Aha!” that leads to the solution. Look back at page 267 to see an interesting example of this in the SerialDate example. 举个简单的例子,假设你注意到所有输入超过五个字符的测试都失败了?或者如果任何将负数传递给函数第二个参数的测试都失败了呢?有时仅仅看到测试报告上的红绿模式就足以激发通向解决方案的“顿悟”。回顾第 267 页,在 SerialDate 示例中可以看到这方面的一个有趣例子。
T8: Test Coverage Patterns Can Be RevealingT8: 测试覆盖率模式具有启示性
Looking at the code that is or is not executed by the passing tests gives clues to why the failing tests fail. 查看通过的测试执行或未执行的代码,可以提供失败测试为何失败的线索。
T9: Tests Should Be FastT9: 测试应该快
A slow test is a test that won’t get run. When things get tight, it’s the slow tests that will be dropped from the suite. So do what you must to keep your tests fast. 慢速测试是不会被运行的测试。当时间紧迫时,慢速测试将是从套件中被丢弃的那些。所以尽你所能保持你的测试快速。
CONCLUSION (结论)
This list of heuristics and smells could hardly be said to be complete. Indeed, I’m not sure that such a list can ever be complete. But perhaps completeness should not be the goal, because what this list does do is imply a value system. 这份启发式规则和异味列表很难说是完整的。确实,我不确定这样的列表是否能有完整的时候。但也许完整性不应该是目标,因为这份列表真正做的是暗示一种价值体系。
Indeed, that value system has been the goal, and the topic, of this book. Clean code is not written by following a set of rules. You don’t become a software craftsman by learning a list of heuristics. Professionalism and craftsmanship come from values that drive disciplines. 事实上,那种价值体系一直是本书的目标和主题。整洁的代码不是通过遵循一套规则写出来的。你不会通过学习一份启发式规则列表而成为软件工匠。专业精神和工匠精神来自于驱动纪律的价值观。