Appearance
第 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.
在他精彩的著作《Refactoring》[1]中,Martin Fowler 识别了许多不同的"代码味道"。下面的列表包括了许多 Martin 的味道,也添加了许多我自己的。它还包括了我在实践中使用的其他珠玑和启发。
- [Refactoring].
[1] [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"中它在文本其余部分的引用位置。
17.1 COMMENTS 注释
C1: Inappropriate Information 不合适的信息
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 Comment 过时的注释
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 Comment 冗余的注释
A comment is redundant if it describes something that adequately describes itself. For example:
如果注释描述了某样东西本身就充分描述了的内容,那么它就是冗余的。例如:
java
i++; // increment iAnother example is a Javadoc that says nothing more than (or even less than) the function signature:
另一个例子是 Javadoc 除了函数签名之外什么也没说(甚至更少):
java
/**
* @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 Comment 写得不好的注释
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 Code 被注释掉的代码
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.
当你看到被注释掉的代码时,删除它!别担心,源代码控制系统仍然记得它。如果有人真的需要它,他或她可以回去签出以前的版本。不要容忍被注释掉的代码存活。
17.2 ENVIRONMENT 环境
E1: Build Requires More Than One Step 构建需要不止一个步骤
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 文件和其他工件。你应该能够用一个简单的命令签出系统,然后发出另一个简单的命令来构建它。
sh
svn get mySystem
cd mySystem
ant allE2: Tests Require More Than One Step 测试需要不止一个步骤
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 中发出一个简单的命令。能够运行所有测试是如此基础和重要,它应该是快速、简单和显而易见的。
17.3 FUNCTIONS 函数
F1: Too Many Arguments 太多参数
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 Arguments 输出参数
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 Arguments 标志参数
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 Function 死函数
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.
从未被调用的方法应该被丢弃。保留死代码是浪费的。不要害怕删除函数。记住,你的源代码控制系统仍然记得它。
17.4 GENERAL 一般问题
G1: Multiple Languages in One Source File 一个源文件中有多种语言
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 等片段。另一个例子是,除了 HTML 之外,JSP 文件可能包含 Java、标签库语法、英语注释、Javadoc、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 Unimplemented 明显的行为未被实现
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
[2] 或"最小惊讶原则":http://en.wikipedia.org/wiki/Principle_of_least_astonishment
java
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 Boundaries 边界处的不正确行为
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 Safeties 覆盖安全机制
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: Duplication 重复
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].
[3] [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].
[4] [GOF]。
- [GOF].
[5] [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 Abstraction 代码处于错误的抽象层次
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:
考虑以下代码:
java
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
也许你在想,如果栈是无界的,实现可以只返回零。问题是没有栈是真正无界的。你不能通过检查
java
stack.percentFull() < 50.0.Implementing the function to return 0 would be telling a lie.
来真正防止 OutOfMemoryException。实现该函数返回 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 Derivatives 基类依赖于它们的派生类
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 Information 信息太多
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 Code 死代码
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 Separation 垂直分离
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: Inconsistency 不一致
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: Clutter 杂乱
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 Coupling 人工耦合
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 Envy 特性依恋
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].
[6] [Refactoring]。
java
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:
在其他条件相同的情况下,我们希望消除特性依恋,因为它将一个类的内部暴露给另一个类。然而,有时特性依恋是一种必要的恶。考虑以下:
java
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].
[7] 具体来说,单一职责原则、开闭原则和共同封闭原则。参见 [PPP]。
G15: Selector Arguments 选择器参数
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 会改变什么?选择器参数的目的不仅难以记忆,每个选择器参数还将多个函数合并为一个。选择器参数只是避免将大函数拆分为几个更小函数的懒惰方式。考虑:
java
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:
如果加班按一倍半支付,你用 true 调用这个函数;如果加班按正常时间支付,用 false 调用。你必须记住 calculateWeeklyPay(false) 是什么意思,这已经够糟糕了。但像这样的函数真正的遗憾是作者错过了编写以下代码的机会:
java
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 Intent 模糊的意图
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 函数可能出现的样子:
java
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 Responsibility 放错位置的职责
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 Static 不合适的静态
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:
然而,有时我们编写不应该是静态的静态函数。例如,考虑:
java
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 Variables 使用解释性变量
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.
[8] [Beck97], p. 108。
- [Beck07].
[9] [Beck07]。
Consider this example from FitNesse:
考虑这个来自 FitNesse 的例子:
java
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 Do 函数名称应该说明它们做什么
Look at this code:
看这段代码:
java
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.
你期望这是向日期添加五天吗?还是几周,或几小时?日期实例是被更改了还是函数只返回一个新的 Date 而不更改旧的?你无法从调用中看出函数做什么。
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 Algorithm 理解算法
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.
[10] 知道代码如何工作与知道算法是否能完成所需的工作是有区别的。不确定算法是否合适通常是生活的事实。不确定你的代码做什么只是懒惰。
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 Physical 使逻辑依赖变为物理依赖
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
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/Case 优先使用多态而不是 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 语句中的情况必须创建多态对象,以取代系统中其他此类 switch 语句。
G24: Follow Standard Conventions 遵循标准约定
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 Constants 用命名常量替换魔法数字
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:
一些常量很容易识别,只要它们与非常自解释的代码结合使用,就不总是需要命名常量来隐藏。例如:
java
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:
术语"魔法数字"不仅适用于数字。它适用于任何具有非自描述值的标记。例如:
java
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"代表该测试数据库中唯一的小时工。所以这个测试应该真正读作:
java
assertEquals(
HOURLY_EMPLOYEE_ID,
Employee.find(HOURLY_EMPLOYEE_NAME).employeeNumber());G26: Be Precise 要精确
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 是过度约束的。默认将所有变量设为受保护的是约束不够的。
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.
[11] 或者更好的是,一个使用整数的 Money 类。
Ambiguities and imprecision in code are either a result of disagreements or laziness. In either case they should be eliminated.
代码中的歧义和不精确要么是分歧的结果,要么是懒惰的结果。无论哪种情况,都应该消除。
G27: Structure over Convention 结构优于约定
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 Conditionals 封装条件
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:
例如:
java
if (shouldBeDeleted(timer))is preferable to
优于
java
if (timer.hasExpired() && !timer.isRecurrent())G29: Avoid Negative Conditionals 避免否定条件
Negatives are just a bit harder to understand than positives. So, when possible, conditionals should be expressed as positives. For example:
否定形式比肯定形式稍微难理解一些。所以,在可能的情况下,条件应该用肯定形式表达。例如:
java
if (buffer.shouldCompact())is preferable to
优于
java
if (!buffer.shouldNotCompact())G30: Functions Should Do One Thing 函数应该做一件事
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:
例如:
java
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:
这段代码做了三件事。它循环遍历所有员工,检查每个员工是否应该被支付,然后支付员工。这段代码可以更好地写成:
java
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 Couplings 隐藏的时序耦合
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:
时序耦合通常是必要的,但你不应该隐藏耦合。构建函数的参数,使它们应该被调用的顺序是显而易见的。考虑以下:
java
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。更好的解决方案是:
java
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 Arbitrary 不要随意
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 的更改时发现我们的一个提交者做了这个:
java
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 Conditions 封装边界条件
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 的简单例子:
java
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 之类的变量中的边界条件。
java
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 Abstraction 函数应该只下降一层抽象
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 的代码:
java
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 字段的名称更改为反映其真正的目的。它持有额外破折号的数量。
java
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 标签上放闭合斜杠。(换句话说,它发出
而不是
。)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 方法中的抽象层次。
java
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 Levels 将可配置数据保持在高层
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 的代码:
java
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 类的顶部指定。你不必在系统的低层寻找像这样的语句:
java
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 Navigation 避免传递性导航
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.
[12] [PRAG], p. 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:
相反,我们希望我们的直接协作者提供我们需要的所有服务。我们不应该在系统的对象图中游荡,寻找我们想要调用的方法。相反,我们应该能够简单地说:
java
myCollaborator.doSomething().17.5 JAVA Java 相关
J1: Avoid Long Import Lists by Using Wildcards 使用通配符避免长导入列表
If you use two or more classes from a package, then import the whole package with
如果你从一个包中使用两个或更多类,那么用以下方式导入整个包
java
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.
有时长长的特定导入列表可能有用。例如,如果你正在处理遗留代码,你想找出你需要为哪些类构建模拟和存根,你可以沿着特定导入列表找出所有这些类的真正限定名,然后放置适当的存根。然而,这种用途非常罕见。此外,大多数现代 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 Constants 不要继承常量
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:
我已经见过几次了,它总是让我做鬼脸。一个程序员将一些常量放在接口中,然后通过继承该接口来访问这些常量。看看以下代码:
java
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 类;让我们看看:
java
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。
java
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.
这是一种可怕的做法!常量隐藏在继承层次的顶部。呸!不要使用继承作为欺骗语言作用域规则的方式。使用静态导入代替。
java
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 Enums 常量与枚举
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 ints 的老把戏。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 允许更多的表达和灵活性。考虑这个工资代码的变体:
java
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();
}17.6 NAMES 命名
N1: Choose Descriptive Names 选择描述性的名称
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.
考虑下面的代码。它做什么?如果我给你看选择良好名称的代码,它对你来说将完全有意义,但像这样它只是一堆符号和魔法数字的大杂烩。
java
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.
这是应该编写代码的方式。这个片段实际上不如上面的完整。然而你可以立即推断出它试图做什么,你很可能可以根据那个推断的含义编写缺失的函数。魔法数字不再是魔法,算法的结构具有令人信服的描述性。
java
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.
[13] 参见第 11 页 Ward Cunningham 的引言。
java
private boolean isStrike(int frame) {
return rolls[frame] == 10;
}N2: Choose Names at the Appropriate Level of Abstraction 选择适当抽象层次的名称
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 接口:
java
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 连接向交换机发送端口号来连接的。显然电话号码的概念处于错误的抽象层次。这个场景更好的命名策略可能是:
java
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 Possible 尽可能使用标准术语
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].
[14] [DDD]。
N4: Unambiguous Names 无歧义的名称
Choose names that make the workings of a function or variable unambiguous. Consider this example from FitNesse:
选择使函数或变量的工作无歧义的名称。考虑这个来自 FitNesse 的例子:
java
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 Scopes 对长作用域使用长名称
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 这样的变量名就很好。考虑这个来自老标准"Bowling Game"的片段:
java
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 Encodings 避免编码
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-Effects 名称应该描述副作用
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 的代码:
java
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。
17.7 TESTS 测试
T1: Insufficient Tests 测试不足
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! 使用覆盖率工具!
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 Tests 不要跳过琐碎的测试
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 Ambiguity 被忽略的测试是对歧义的疑问
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 Conditions 测试边界条件
Take special care to test boundary conditions. We often get the middle of an algorithm right but misjudge the boundaries.
特别注意测试边界条件。我们经常把算法的中间部分弄对了,但误判了边界。
T6: Exhaustively Test Near Bugs 在 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 Revealing 失败模式具有启发性
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 Revealing 测试覆盖率模式具有启发性
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 Fast 测试应该快速
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.
慢的测试是不会被运行的测试。当情况紧张时,慢的测试将从套件中被丢弃。所以做你必须做的事情来保持你的测试快速。
17.8 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.
事实上,那个价值体系一直是本书的目标和主题。整洁代码不是通过遵循一组规则来编写的。你不是通过学习一个启发列表来成为软件工匠的。专业精神和工匠精神来自驱动规范的价值观。