第 17 章 Smells and Heuristics 味道与启发
# 第 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.
Martin Fowler 在其妙书 Refectoring:Improving the Design of Existing Code[1]中指出了许多不同的“代码味道”。下面的清单包括很多 Martin 提出的味道,还添加了更多我自己提出的,也包括我借以历练本业的其他珍宝与启发。
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.
清单应按顺序阅读,并作为一种参考来使用。
# 17.1 COMMENTS 注释
C1: Inappropriate Information
C1:不恰当的信息
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
C2:废弃的注释
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
C3:冗余注释
A comment is redundant if it describes something that adequately describes itself. For example:
如果注释描述的是某种充分自我描述了的东西,那么注释就是多余的。例如:
i++; // increment i
Another 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 ManagedComponentException
2
3
4
5
6
7
Comments should say things that the code cannot say for itself.
注释应该谈及代码自身没提到的东西。
C4: Poorly Written Comment
C4:糟糕的注释
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
C5:注释掉的代码
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
E1:需要多步才能实现的构建
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 all
2
3
E2: Tests Require More Than One Step
E2:需要多步才能做到的测试
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.
你应当能够发出单个指令就可以运行全部单元测试。能够运行全部测试是如此基础和重要,应该快速、轻易和直截了当地做到。
# 17.3 FUNCTIONS 函数
F1: Too Many Arguments
F1:过多的参数
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.)
函数的参数量应该少。没参数最好,一个次之,两个、三个再次之。三个以上的参数非常值得质疑,应坚决避免。(参见前文“函数参数”一节。)
F2: Output Arguments
F2:输出参数
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.)
输出参数违反直觉。读者期望参数用于输入而非输出。如果函数非要修改什么东西的状态不可,就修改它所在对象的状态好了。(参见前文“输出参数”一节。)
F3: Flag Arguments
F3:标识参数
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.)
布尔值参数大声宣告函数做了不止一件事。它们令人迷惑,应该消灭掉。(参见前文“标识参数”一节。)
F4: Dead Function
F4:死函数
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
G1:一个源文件中存在多种语言
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、标签库语法、英文注释、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
G2:明显的行为未被实现
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.
遵循“最小惊异原则”(The Principle of Least Surprise)[2],函数或类应该实现其他程序员有理由期待的行为。例如,考虑一个将日期名称翻译为表示该日期的枚举的函数。
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
G3:不正确的边界行为
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
G4:忽视安全
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
G5:重复
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 原则(Don’t Repeat Yourself,别重复自己)[3]。Kent Beck 将它列为极限编程核心原则之一,并称之为“一次,也只一次” 。Ron Jeffries 将这条规则列在第二位,地位只低于通过所有测试。
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.
更隐蔽的形态是采用类似算法但具体代码行不同的模块。这也是一种重复,可以使用模板方法模式[4]或策略模式[5]来修正。
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.
的确,过去 15 年内出现的多数设计模式都是消除重复的有名手段。考德范式(Codd Normal Forms)是消除数据库规划中的重复的策略。OO 自身也是组织模块和消除重复的策略。毫不出奇,结构化编程也是。
I think the point has been made. Find and eliminate duplication wherever you can.
重点已经在那里了。尽可能找到并消除重复。
G6: Code at Wrong Level of Abstraction
G6:在错误的抽象层级上的代码
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 {}
}
2
3
4
5
6
7
8
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 位于错误的抽象层级。尽管存在许多在其中“充满”(fullness)概念有意义的 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
你或许会认为,如果堆栈无边界,实现可以返回 0。问题是,不存在真的无边界的堆栈。你不能真的避免在做以下检查时出现 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 Derivatives
G7:基类依赖于派生类
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
G8:信息过多
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
G9:死代码
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
G10:垂直分隔
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
G11:前后不一致
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,则给处理其他请求类型的方法取类似的名字,例如 processDeletion Request。
Simple consistency like this, when reliably applied, can make code much easier to read and modify.
如此简单的前后一致,一旦坚决贯彻,就能让代码更加易于阅读和修改。
G12: Clutter
G12:混淆视听
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
G13:人为耦合
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.
不互相依赖的东西不该耦合。例如,普通的 enum 不应在特殊类中包括,因为这样一来应用程序就要了解这些更为特殊的类。对于在特殊类中声明一般目的的 static 函数也是如此。
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
G14:特性依恋
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]。类的方法只应对其所属类中的变量和函数感兴趣,不该垂青其他类中的变量和函数。当方法通过某个其他对象的访问器和修改器来操作该对象内部数据,则它就依恋于该对象所属类的范围。它期望自己在那个类里面,这样就能直接访问它操作的变量。例如:
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);
}
}
2
3
4
5
6
7
8
9
10
11
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);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 与报告的格式耦合起来,向该格式的修改暴露这个类。
G15: Selector Arguments
G15:选择算子参数
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 参数更为可憎的事情了。那个 false 是什么意思?如果它是 true,会有什么变化吗?不仅是一个选择算子(selector)参数的目的难以记住,每个选择算子参数将多个函数绑到了一起。选择算子参数只是一种避免把大函数切分为多个小函数的偷懒做法。考虑下面这段代码:
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;
}
2
3
4
5
6
7
8
9
10
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) 表示什么,这已经足够糟糕了。但这种函数真正的坏处在于作者错过了这样写的机会:
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);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
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.
当然,选择算子不一定是 boolean 类型。可能是枚举元素、整数或任何一种用于选择函数行为的参数。使用多个函数,通常优于向单个函数传递某些代码来选择函数行为。
G16: Obscured Intent
G16:晦涩的意图
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)
);
}
2
3
4
5
6
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
G17:位置错误的权责
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
G18:不恰当的静态方法
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)是个良好的静态方法。它并不在单个实体上操作;的确,不得不写 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.
这看起来像是个有道理的 static 函数。它并不在任何特定对象上操作,而且从参数中获得全部数据。然而,我们却有理由希望这个函数是多态的。我们可能希望为计算每小时支付工资实现几种不同算法,例如 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
G19:使用解释性变量
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]中都写到这个。让程序可读的最有力方法之一就是将计算过程打散成在用有意义的单词命名的变量中放置的中间值。
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);
}
2
3
4
5
6
7
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.
解释性变量的这种简单用法,说明了第一个匹配组是 key,而第二个匹配组是 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
G20:函数名称应该表达其行为
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.
你会期望它向日期添加 5 天吗?或者是 5 个星期?5 个小时?该 date 实体会变化吗?或者该函数只是返回一个新的 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.
如果函数向日期添加 5 天并且修改该日期,就该命名为 addDaysTo 或 increaseByDays。如果函数返回一个表示 5 天后的日期,而不修改日期实体,就该叫做 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
G21:理解算法
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]解决方案是正确的。
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
G22:把逻辑依赖改为物理依赖
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;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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.
AGE_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.
可以通过创建 HourlyReport 中名为 getMaxPageSize() 的新方法来物理化这种依赖。HourlyReporter 将调用这个方法,而不是使用 PAGE_SIZE 常量。
G23: Prefer Polymorphism to If/Else or Switch/Case
G23:用多态替代 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 Conventions
G24:遵循标准约定
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.
如果你想知道我遵循哪些约定,可以查看代码清单 B-7~B-14 中重构之后的代码。
G25: Replace Magic Numbers with Named Constants
G25:用命名常量替代魔术数
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.
这大概是软件开发中最古老的规则之一了。我记得,在 20 世纪 60 年代介绍 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.
例如,数字 86400 应当藏在常量 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;
2
3
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.141592653589793,都会知道那是 p 值,从而不会去仔细查看。(你发现那个错误的数字了吗?)我们不想要人们使用 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 的雇员。团队中每个成员都知道,当连接到数据库时,里面已经有数个雇员信息,其值和属性都是大家熟知的。所以,这个测试应该读作:
assertEquals(
HOURLY_EMPLOYEE_ID,
Employee.find(HOURLY_EMPLOYEE_NAME).employeeNumber());
2
3
G26: Be Precise
G26:准确
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],并恰当地处理四舍五入。如果可能有并发更新,确认你实现了某种锁定机制。
Ambiguities and imprecision in code are either a result of disagreements or laziness. In either case they should be eliminated.
代码中的含糊和不准确要么是意见不同的结果,要么源于懒惰。无论原因是什么,都要消除。
G27: Structure over Convention
G27:结构甚于约定
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
G28:封装条件
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 Conditionals
G29:避免否定性条件
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 Thing
G30:函数只该做一件事
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);
}
}
}
2
3
4
5
6
7
8
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);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
Each of these functions does one thing. (See “Do One Thing” on page 35.)
上列每个函数都只做一件事。(见前文“只做一件事”一节。)
G31: Hidden Temporal Couplings
》 G31:掩蔽时序耦合
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);
}
…
}
2
3
4
5
6
7
8
9
10
11
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:
三个函数的次序很重要。捕鱼之前先织网,织网之前先编绳。不幸的是,代码并没有强制这种时序耦合。其他程序员可以在调用 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);
}
…
}
2
3
4
5
6
7
8
9
10
11
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
G32:别随意
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 {
…
…
}
2
3
4
5
6
7
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 里面,或者他真认为这么做是对的。不管原因是什么,结果都显得随心所欲。不作为类工具的公共类,不应该放到其他类里面。惯例是将它置为 public,并且放在代码包的顶部。
G33: Encapsulate Boundary Conditions
G33:封装边界条件
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;
}
2
3
4
5
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;
}
2
3
4
5
6
G34: Functions Should Descend Only One Level of Abstraction
G34:函数应该只在一个抽象层级上
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();
}
2
3
4
5
6
7
8
9
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 模块。该模块检测一行 4 个或更多个破折号,并将其转换为恰当的 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);
}
2
3
4
5
6
7
8
9
10
11
12
13
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 <hr>
instead of <hr/>
.) The HtmlTag module had been changed to conform to XHTML long ago.
做出修改时,我发现了一处微小的错误。原始代码没有加上 hr 标记的结束斜线符,而 XHTML 标准要求这样做。(换言之,代码使用了
<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.rendermethod 中的抽象层级的结果。
public String render() throws Exception
{
HtmlTag hr = new HtmlTag(“hr”);
if (size > 0) {
hr.addAttribute(“size”, “”+(size+1));
}
return hr.html();
}
2
3
4
5
6
7
8
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
G35:在较高层级放置可配置数据
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;
…
}
2
3
4
5
6
7
8
9
10
11
12
13
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 default
The 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
G36:避免传递浏览
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.
这就是所谓得墨忒耳律。The Pragmatic Programmers(中译版《程序员修炼之道》)称之为“编写害羞代码”[12]。两者都归结为确保模块只了解其直接协作者,不了解整个系统的游览图。
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().
myCollaborator.doSomething().
# 17.5 JAVA
J1: Avoid Long Import Lists by Using Wildcards
J1:通过使用通配符避免过长的导入清单
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.
有时,长长的具体导入清单也会有用。例如,如果你在处理遗留下来的代码,想要找出需要为哪些类构造替身类和占位代码,就可以遍历导入清单,找出这些类的真名,再恰当地放置占位代码。不过,这种用法很罕见。而且,多数现代 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
J2:不要继承常量
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)
);
}
…
}
2
3
4
5
6
7
8
9
10
11
12
13
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);
}
2
3
4
5
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;
}
2
3
4
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)
);
}
…
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
J3: Constants versus Enums
J3:常量 vs. 枚举
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.
现在 enum 已经加入 Java 语言(Java 5),放心用吧!别再用那个 public static final int 老花招。那样做 int 的意义就丧失了,而用 enum 则不然,因为它们隶属于有名称的枚举。
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:
而且,仔细研究 enum 的语法。它可以拥有方法和字段,从而成为能比 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();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 17.6 NAMES 名称
N1: Choose Descriptive Names
N1:采用描述性名称
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;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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( )的实现。读到 isStrick 方法时,它“深合你意”[13]。
private boolean isStrike(int frame) {
return rolls[frame] == 10;
}
2
3
N2: Choose Names at the Appropriate Level of Abstraction
N2:名称应与抽象层级相符
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();
}
2
3
4
5
6
7
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:
粗看还行。函数看来都很合适,对于多数应用程序来说是这样。不过,想想看某个应用中有些调制解调器并不用拨号连接的情形。有些用线缆直连(就像如今为多数家庭提供 Internet 连接的线缆解调器)的情形。有些通过向 USB 口发送端口信息连接。显然,有关电话号码的信息就是位于错误的抽象层级了。对于这种情形,更好的命名策略可能是:
public interface Modem {
boolean connect(String connectionLocator);
boolean disconnect();
boolean send(char c);
char recv();
String getConnectedLocator();
}
2
3
4
5
6
7
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
N3:尽可能使用标准命名法
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 字样。例如, 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]。代码应该使用来自这种语言的术语。简言之,具有与项目有关的特定意义的名称用得越多,读者就越容易明白你的代码是做什么的。
N4: Unambiguous Names
N4:无歧义的名称
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);
}
2
3
4
5
6
7
8
9
10
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
N5:为较大作用范围选用较长名称
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 之类的变量名对于作用范围在 5 行之内的情形没问题。看看以下来自老“标准保龄球游戏”的代码片段:
private void rollMany(int n, int pins)
{
for (int i=0; i<n; i++)
g.roll(pins);
}
2
3
4
5
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.
这段代码很明白,如果用 rollCount 之类烦人的名称代替变量 i,反而是徒增混乱。另一方面,在较长距离上,使用短名称的变量和函数会丧失其含义。名称的作用范围越大,名称就该越长、越准确。
N6: Avoid Encodings
N6:避免编码
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
》 N7:名称应该说明副作用
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;
}
2
3
4
5
6
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
T1:测试不足
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 Tests
T3:别略过小测试
小测试易于编写,其文档上的价值高于编写成本。
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
T4:被忽略的测试就是对不确定事物的疑问
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
T5:测试边界条件
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
T6:全面测试相近的缺陷
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.
缺陷趋向于扎堆。在某个函数中发现一个缺陷时,最好全面测试那个函数。你可能会发现缺陷不止一个。
T7: Patterns of Failure Are Revealing
T7:测试失败的模式有启发性
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.
简单举例,假设你注意到所有长于 5 个字符的输入都会导致测试失败,或者向函数的第二个参数传入负数都会导致测试失败。有时,只要看看测试报告的红绿模式,就足以绽放出那句带来解决方法的“啊哈!”回头看看第 16 章“重构 SerialDate”中的有趣例子吧。
T8: Test Coverage Patterns Can Be Revealing
T8:测试覆盖率的模式有启发性
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
T9:测试应该快速
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.
那套价值体系才该是目标,也是本书的主题所在。整洁代码并非遵循一套规则写就。学习一系列启发并不足以让你成为软件匠人。专业性和技艺来自于驱动规程的价值观。