Appearance
第 10 章 Classes 类
with Jeff Langr

So far in this book we have focused on how to write lines and blocks of code well. We have delved into proper composition of functions and how they interrelate. But for all the attention to the expressiveness of code statements and the functions they comprise, we still don't have clean code until we've paid attention to higher levels of code organization. Let's talk about clean classes.
到目前为止,在本书中我们一直专注于如何写好一行行和一块块的代码。我们深入探讨了函数的恰当组合以及它们之间的相互关系。但是,尽管我们关注了代码语句及其组成函数的表达力,在我们关注更高层次的代码组织之前,我们仍然没有整洁的代码。让我们来谈谈整洁的类。
10.1 CLASS ORGANIZATION 类的组织
Following the standard Java convention, a class should begin with a list of variables. Public static constants, if any, should come first. Then private static variables, followed by private instance variables. There is seldom a good reason to have a public variable.
遵循标准的 Java 约定,类应该以变量列表开头。公共静态常量(如果有的话)应该排在最前面。然后是私有静态变量,接着是私有实例变量。很少有充分的理由使用公共变量。
Public functions should follow the list of variables. We like to put the private utilities called by a public function right after the public function itself. This follows the stepdown rule and helps the program read like a newspaper article.
公共函数应该跟在变量列表之后。我们喜欢将公共函数调用的私有工具函数紧跟在公共函数之后。这遵循了向下规则,有助于程序像报纸文章一样阅读。
10.1.1 Encapsulation 封装
We like to keep our variables and utility functions private, but we're not fanatic about it. Sometimes we need to make a variable or utility function protected so that it can be accessed by a test. For us, tests rule. If a test in the same package needs to call a function or access a variable, we'll make it protected or package scope. However, we'll first look for a way to maintain privacy. Loosening encapsulation is always a last resort.
我们喜欢将变量和工具函数保持为私有的,但我们不会对此过于狂热。有时我们需要将变量或工具函数设为受保护的,以便测试可以访问它们。对我们来说,测试至上。如果同一个包中的测试需要调用函数或访问变量,我们会将其设为受保护的或包范围的。然而,我们会首先寻找保持私密性的方法。放松封装始终是最后的手段。
10.2 CLASSES SHOULD BE SMALL! 类应该小!
The first rule of classes is that they should be small. The second rule of classes is that they should be smaller than that. No, we're not going to repeat the exact same text from the Functions chapter. But as with functions, smaller is the primary rule when it comes to designing classes. As with functions, our immediate question is always "How small?"
类的第一条规则是它们应该小。类的第二条规则是它们应该更小。不,我们不会重复函数章节中完全相同的文字。但与函数一样,在设计类时,更小是首要规则。与函数一样,我们最直接的问题总是"多小?"
With functions we measured size by counting physical lines. With classes we use a different measure. We count responsibilities.1
对于函数,我们通过计算物理行数来衡量大小。对于类,我们使用不同的度量方式。我们计算职责。[1]
- [RDD]
[1] [RDD]
Listing 10-1 outlines a class, SuperDashboard, that exposes about 70 public methods. Most developers would agree that it's a bit too super in size. Some developers might refer to SuperDashboard as a "God class."
代码清单 10-1 概述了一个类 SuperDashboard,它暴露了大约 70 个公共方法。大多数开发者会同意它在大小上有点太"超级"了。有些开发者可能将 SuperDashboard 称为"上帝类"。
Listing 10-1 Too Many Responsibilities
代码清单 10-1 太多职责
java
public class SuperDashboard extends JFrame implements MetaDataUser
public String getCustomizerLanguagePath()
public void setSystemConfigPath(String systemConfigPath)
public String getSystemConfigDocument()
public void setSystemConfigDocument(String systemConfigDocument)
public boolean getGuruState()
public boolean getNoviceState()
public boolean getOpenSourceState()
public void showObject(MetaObject object)
public void showProgress(String s)
public boolean isMetadataDirty()
public void setIsMetadataDirty(boolean isMetadataDirty)
public Component getLastFocusedComponent()
public void setLastFocused(Component lastFocused)
public void setMouseSelectState(boolean isMouseSelected)
public boolean isMouseSelected()
public LanguageManager getLanguageManager()
public Project getProject()
public Project getFirstProject()
public Project getLastProject()
public String getNewProjectName()
public void setComponentSizes(Dimension dim)
public String getCurrentDir()
public void setCurrentDir(String newDir)
public void updateStatus(int dotPos, int markPos)
public Class[] getDataBaseClasses()
public MetadataFeeder getMetadataFeeder()
public void addProject(Project project)
public boolean setCurrentProject(Project project)
public boolean removeProject(Project project)
public MetaProjectHeader getProgramMetadata()
public void resetDashboard()
public Project loadProject(String fileName, String projectName)
public void setCanSaveMetadata(boolean canSave)
public MetaObject getSelectedObject()
public void deselectObjects()
public void setProject(Project project)
public void editorAction(String actionName, ActionEvent event)
public void setMode(int mode)
public FileManager getFileManager()
public void setFileManager(FileManager fileManager)
public ConfigManager getConfigManager()
public void setConfigManager(ConfigManager configManager)
public ClassLoader getClassLoader()
public void setClassLoader(ClassLoader classLoader)
public Properties getProps()
public String getUserHome()
public String getBaseDir()
public int getMajorVersionNumber()
public int getMinorVersionNumber()
public int getBuildNumber()
public MetaObject pasting(
MetaObject target, MetaObject pasted, MetaProject project)
public void processMenuItems(MetaObject metaObject)
public void processMenuSeparators(MetaObject metaObject)
public void processTabPages(MetaObject metaObject)
public void processPlacement(MetaObject object)
public void processCreateLayout(MetaObject object)
public void updateDisplayLayer(MetaObject object, int layerIndex)
public void propertyEditedRepaint(MetaObject object)
public void processDeleteObject(MetaObject object)
public boolean getAttachedToDesigner()
public void processProjectChangedState(boolean hasProjectChanged)
public void processObjectNameChanged(MetaObject object)
public void runProject()
public void setAçowDragging(boolean allowDragging)
public boolean allowDragging()
public boolean isCustomizing()
public void setTitle(String title)
public IdeMenuBar getIdeMenuBar()
public void showHelper(MetaObject metaObject, String propertyName)
// … many non-public methods follow …
}But what if SuperDashboard contained only the methods shown in Listing 10-2?
但如果 SuperDashboard 只包含代码清单 10-2 中所示的方法呢?
Listing 10-2 Small Enough?
代码清单 10-2 足够小了吗?
java
public class SuperDashboard extends JFrame implements MetaDataUser
public Component getLastFocusedComponent()
public void setLastFocused(Component lastFocused)
public int getMajorVersionNumber()
public int getMinorVersionNumber()
public int getBuildNumber()
}Five methods isn't too much, is it? In this case it is because despite its small number of methods, SuperDashboard has too many responsibilities.
五个方法不算太多,对吧?在这种情况下确实太多,因为尽管方法数量少,SuperDashboard 有太多职责。
The name of a class should describe what responsibilities it fulfills. In fact, naming is probably the first way of helping determine class size. If we cannot derive a concise name for a class, then it's likely too large. The more ambiguous the class name, the more likely it has too many responsibilities. For example, class names including weasel words like Processor or Manager or Super often hint at unfortunate aggregation of responsibilities.
类的名称应该描述它履行什么职责。事实上,命名可能是帮助确定类大小的第一种方式。如果我们无法为类起一个简洁的名称,那么它可能太大了。类名越模糊,它就越可能有太多职责。例如,包含像 Processor、Manager 或 Super 这样含糊词汇的类名通常暗示着职责的不幸聚合。
We should also be able to write a brief description of the class in about 25 words, without using the words "if," "and," "or," or "but." How would we describe the SuperDashboard? "The SuperDashboard provides access to the component that last held the focus, and it also allows us to track the version and build numbers." The first "and" is a hint that SuperDashboard has too many responsibilities.
我们还应该能够用大约 25 个词写出类的简要描述,而不使用"如果"、"和"、"或"或"但是"这些词。我们如何描述 SuperDashboard?"SuperDashboard 提供对最后持有焦点的组件的访问,它还允许我们跟踪版本和构建号。"第一个"和"暗示 SuperDashboard 有太多职责。
10.2.1 The Single Responsibility Principle 单一职责原则
The Single Responsibility Principle (SRP)2 states that a class or module should have one, and only one, reason to change. This principle gives us both a definition of responsibility, and a guidelines for class size. Classes should have one responsibility—one reason to change.
单一职责原则(SRP)[2]指出,一个类或模块应该有且仅有一个修改原因。这个原则既给了我们职责的定义,也给了类大小的指导原则。类应该有一个职责——一个修改原因。
- You can read much more about this principle in [PPP].
[2] 你可以在 [PPP] 中阅读更多关于这个原则的内容。
The seemingly small SuperDashboard class in Listing 10-2 has two reasons to change. First, it tracks version information that would seemingly need to be updated every time the software gets shipped. Second, it manages Java Swing components (it is a derivative of JFrame, the Swing representation of a top-level GUI window). No doubt we'll want to update the version number if we change any of the Swing code, but the converse isn't necessarily true: We might change the version information based on changes to other code in the system.
代码清单 10-2 中看似很小的 SuperDashboard 类有两个修改原因。首先,它跟踪版本信息,似乎每次发布软件时都需要更新。其次,它管理 Java Swing 组件(它是 JFrame 的派生类,Swing 顶层 GUI 窗口的表示)。毫无疑问,如果我们更改任何 Swing 代码,我们会想要更新版本号,但反过来不一定成立:我们可能会根据系统中其他代码的更改来更改版本信息。
Trying to identify responsibilities (reasons to change) often helps us recognize and create better abstractions in our code. We can easily extract all three SuperDashboard methods that deal with version information into a separate class named Version. (See Listing 10-3.) The Version class is a construct that has a high potential for reuse in other applications!
尝试识别职责(修改原因)通常有助于我们在代码中识别和创建更好的抽象。我们可以轻松地将处理版本信息的所有三个 SuperDashboard 方法提取到一个名为 Version 的单独类中。(见代码清单 10-3。)Version 类是一个在其他应用程序中具有很高复用潜力的构造!
Listing 10-3 A single-responsibility class
代码清单 10-3 单一职责类
java
public class Version {
public int getMajorVersionNumber()
public int getMinorVersionNumber()
public int getBuildNumber()
}SRP is one of the more important concept in OO design. It's also one of the simpler concepts to understand and adhere to. Yet oddly, SRP is often the most abused class design principle. We regularly encounter classes that do far too many things. Why?
SRP 是面向对象设计中更重要的概念之一。它也是更简单易懂和遵循的概念之一。然而奇怪的是,SRP 往往是最被滥用的类设计原则。我们经常遇到做太多事情的类。为什么?
Getting software to work and making software clean are two very different activities. Most of us have limited room in our heads, so we focus on getting our code to work more than organization and cleanliness. This is wholly appropriate. Maintaining a separation of concerns is just as important in our programming activities as it is in our programs.
让软件工作和让软件整洁是两种非常不同的活动。我们大多数人的大脑空间有限,所以我们更关注让代码工作而不是组织和整洁性。这是完全恰当的。在我们的编程活动中保持关注点分离与在我们的程序中一样重要。
The problem is that too many of us think that we are done once the program works. We fail to switch to the other concern of organization and cleanliness. We move on to the next problem rather than going back and breaking the overstuffed classes into decoupled units with single responsibilities.
问题在于我们太多人认为程序一旦工作就算完成了。我们没有切换到组织和整洁性这个关注点。我们转向下一个问题,而不是回头将过度臃肿的类分解为具有单一职责的解耦单元。
At the same time, many developers fear that a large number of small, single-purpose classes makes it more difficult to understand the bigger picture. They are concerned that they must navigate from class to class in order to figure out how a larger piece of work gets accomplished.
与此同时,许多开发者担心大量小型、单一用途的类会使理解全局变得更加困难。他们担心必须从一个类导航到另一个类才能弄清楚更大的工作是如何完成的。
However, a system with many small classes has no more moving parts than a system with a few large classes. There is just as much to learn in the system with a few large classes. So the question is: Do you want your tools organized into toolboxes with many small drawers each containing well-defined and well-labeled components? Or do you want a few drawers that you just toss everything into?
然而,一个有许多小类的系统并不比一个有几个大类的系统有更多可动部分。在有几个大类的系统中需要学习的东西同样多。所以问题是:你想要你的工具被组织到工具箱中,有许多小抽屉,每个抽屉包含定义明确、标签清晰的组件吗?还是你想要几个抽屉,你只是把所有东西都扔进去?
Every sizable system will contain a large amount of logic and complexity. The primary goal in managing such complexity is to organize it so that a developer knows where to look to find things and need only understand the directly affected complexity at any given time. In contrast, a system with larger, multipurpose classes always hampers us by insisting we wade through lots of things we don't need to know right now.
每个有相当规模的系统都会包含大量的逻辑和复杂性。管理这种复杂性的主要目标是组织它,使开发者知道去哪里找东西,并且在任何给定时间只需要理解直接受影响的复杂性。相比之下,具有较大的、多功能类的系统总是阻碍我们,坚持要求我们涉过大量我们现在不需要知道的东西。
To restate the former points for emphasis: We want our systems to be composed of many small classes, not a few large ones. Each small class encapsulates a single responsibility, has a single reason to change, and collaborates with a few others to achieve the desired system behaviors.
重申前面的要点以示强调:我们希望我们的系统由许多小类组成,而不是几个大类。每个小类封装一个单一职责,有一个修改原因,并与其他几个类协作以实现所需的系统行为。
10.2.2 Cohesion 内聚性
Classes should have a small number of instance variables. Each of the methods of a class should manipulate one or more of those variables. In general the more variables a method manipulates the more cohesive that method is to its class. A class in which each variable is used by each method is maximally cohesive.
类应该有少量的实例变量。类的每个方法应该操作其中一个或多个变量。通常,方法操作的变量越多,该方法与其类的内聚性就越高。一个每个变量都被每个方法使用的类是最大内聚的。
In general it is neither advisable nor possible to create such maximally cohesive classes; on the other hand, we would like cohesion to be high. When cohesion is high, it means that the methods and variables of the class are co-dependent and hang together as a logical whole.
通常,创建这样最大内聚的类既不可取也不可能;另一方面,我们希望内聚性高。当内聚性高时,意味着类的方法和变量是相互依赖的,作为一个逻辑整体紧密结合在一起。
Consider the implementation of a Stack in Listing 10-4. This is a very cohesive class. Of the three methods only size() fails to use both the variables.
考虑代码清单 10-4 中 Stack 的实现。这是一个非常内聚的类。在三个方法中,只有 size() 没有使用两个变量。
Listing 10-4 Stack.java A cohesive class.
代码清单 10-4 Stack.java 一个内聚的类。
java
public class Stack {
private int topOfStack = 0;
List<Integer> elements = new LinkedList<Integer>();
public int size() {
return topOfStack;
}
public void push(int element) {
topOfStack++;
elements.add(element);
}
public int pop() throws PoppedWhenEmpty {
if (topOfStack == 0)
throw new PoppedWhenEmpty();
int element = elements.get(--topOfStack);
elements.remove(topOfStack);
return element;
}
}The strategy of keeping functions small and keeping parameter lists short can sometimes lead to a proliferation of instance variables that are used by a subset of methods. When this happens, it almost always means that there is at least one other class trying to get out of the larger class. You should try to separate the variables and methods into two or more classes such that the new classes are more cohesive.
保持函数小和参数列表短的策略有时会导致被方法子集使用的实例变量激增。当这种情况发生时,几乎总是意味着至少有另一个类试图从较大的类中挣脱出来。你应该尝试将变量和方法分离到两个或更多类中,使得新的类更加内聚。
10.2.3 Maintaining Cohesion Results in Many Small Classes 保持内聚性会产生许多小类
Just the act of breaking large functions into smaller functions causes a proliferation of classes. Consider a large function with many variables declared within it. Let's say you want to extract one small part of that function into a separate function. However, the code you want to extract uses four of the variables declared in the function. Must you pass all four of those variables into the new function as arguments?
将大函数分解为更小函数的行为本身就会导致类的激增。考虑一个在其中声明了许多变量的大函数。假设你想将该函数的一小部分提取到一个单独的函数中。然而,你想提取的代码使用了函数中声明的四个变量。你必须将所有四个变量作为参数传递给新函数吗?
Not at all! If we promoted those four variables to instance variables of the class, then we could extract the code without passing any variables at all. It would be easy to break the function up into small pieces.
完全不必!如果我们将这四个变量提升为类的实例变量,那么我们可以在不传递任何变量的情况下提取代码。将函数分解成小块会很容易。
Unfortunately, this also means that our classes lose cohesion because they accumulate more and more instance variables that exist solely to allow a few functions to share them. But wait! If there are a few functions that want to share certain variables, doesn't that make them a class in their own right? Of course it does. When classes lose cohesion, split them!
不幸的是,这也意味着我们的类失去了内聚性,因为它们积累了越来越多仅为了让少数函数共享而存在的实例变量。但是等等!如果有几个函数想要共享某些变量,这难道不使它们本身成为一个类吗?当然如此。当类失去内聚性时,拆分它们!
So breaking a large function into many smaller functions often gives us the opportunity to split several smaller classes out as well. This gives our program a much better organization and a more transparent structure.
所以将一个大函数分解为许多更小的函数通常也给了我们机会拆分出几个更小的类。这使我们的程序有了更好的组织和更透明的结构。
As a demonstration of what I mean, let's use a time-honored example taken from Knuth's wonderful book Literate Programming.3 Listing 10-5 shows a translation into Java of Knuth's PrintPrimes program. To be fair to Knuth, this is not the program as he wrote it but rather as it was output by his WEB tool. I'm using it because it makes a great starting place for breaking up a big function into many smaller functions and classes.
为了演示我的意思,让我们使用一个来自 Knuth 精彩著作《Literate Programming》[3]的经典例子。代码清单 10-5 展示了 Knuth 的 PrintPrimes 程序的 Java 翻译版。公平地说,这不是 Knuth 编写的程序,而是由他的 WEB 工具输出的。我使用它是因为它为将一个大函数分解为许多更小的函数和类提供了一个很好的起点。
- [Knuth92].
[3] [Knuth92]。
Listing 10-5 PrintPrimes.java
代码清单 10-5 PrintPrimes.java
java
package literatePrimes;
public class PrintPrimes {
public static void main(String[] args) {
final int M = 1000;
final int RR = 50;
final int CC = 4;
final int WW = 10;
final int ORDMAX = 30;
int P[] = new int[M + 1];
int PAGENUMBER;
int PAGEOFFSET;
int ROWOFFSET;
int C;
int J;
int K;
boolean JPRIME;
int ORD;
int SQUARE;
int N;
int MULT[] = new int[ORDMAX + 1];
J = 1;
K = 1;
P[1] = 2;
ORD = 2;
SQUARE = 9;
while (K < M) {
do {
J = J + 2;
if (J == SQUARE) {
ORD = ORD + 1;
SQUARE = P[ORD] * P[ORD];
MULT[ORD - 1] = J;
}
N = 2;
JPRIME = true;
while (N < ORD && JPRIME) {
while (MULT[N] < J)
MULT[N] = MULT[N] + P[N] + P[N];
if (MULT[N] == J)
JPRIME = false;
N = N + 1;
}
} while (!JPRIME);
K = K + 1;
P[K] = J;
}
{
PAGENUMBER = 1;
PAGEOFFSET = 1;
while (PAGEOFFSET <= M) {
System.out.println("The First " + M +
" Prime Numbers --- Page " + PAGENUMBER);
System.out.println("");
for (ROWOFFSET = PAGEOFFSET; ROWOFFSET < PAGEOFFSET + RR; ROWOFFSET++){
for (C = 0; C < CC;C++)
if (ROWOFFSET + C * RR <= M)
System.out.format("%10d", P[ROWOFFSET + C * RR]);
System.out.println("");
}
System.out.println("\f");
PAGENUMBER = PAGENUMBER + 1;
PAGEOFFSET = PAGEOFFSET + RR * CC;
}
}
}
}This program, written as a single function, is a mess. It has a deeply indented structure, a plethora of odd variables, and a tightly coupled structure. At the very least, the one big function should be split up into a few smaller functions.
这个程序,作为一个单独的函数编写,是一团糟。它有深度缩进的结构、大量奇怪的变量和紧密耦合的结构。至少,这个大函数应该被拆分成几个更小的函数。
Listing 10-6 through Listing 10-8 show the result of splitting the code in Listing 10-5 into smaller classes and functions, and choosing meaningful names for those classes, functions, and variables.
代码清单 10-6 到代码清单 10-8 展示了将代码清单 10-5 中的代码拆分为更小的类和函数,并为这些类、函数和变量选择有意义的名称的结果。
Listing 10-6 PrimePrinter.java (refactored)
代码清单 10-6 PrimePrinter.java(重构版本)
java
package literatePrimes;
public class PrimePrinter {
public static void main(String[] args) {
final int NUMBER_OF_PRIMES = 1000;
int[] primes = PrimeGenerator.generate(NUMBER_OF_PRIMES);
final int ROWS_PER_PAGE = 50;
final int COLUMNS_PER_PAGE = 4;
RowColumnPagePrinter tablePrinter =
new RowColumnPagePrinter(ROWS_PER_PAGE,
COLUMNS_PER_PAGE,
"The First " + NUMBER_OF_PRIMES +
" Prime Numbers");
tablePrinter.print(primes);
}
}Listing 10-7 RowColumnPagePrinter.java
代码清单 10-7 RowColumnPagePrinter.java
java
package literatePrimes;
import java.io.PrintStream;
public class RowColumnPagePrinter {
private int rowsPerPage;
private int columnsPerPage;
private int numbersPerPage;
private String pageHeader;
private PrintStream printStream;
public RowColumnPagePrinter(int rowsPerPage,
int columnsPerPage,
String pageHeader) {
this.rowsPerPage = rowsPerPage;
this.columnsPerPage = columnsPerPage;
this.pageHeader = pageHeader;
numbersPerPage = rowsPerPage * columnsPerPage;
printStream = System.out;
}
public void print(int data[]) {
int pageNumber = 1;
for (int firstIndexOnPage = 0;
firstIndexOnPage < data.length;
firstIndexOnPage += numbersPerPage) {
int lastIndexOnPage =
Math.min(firstIndexOnPage + numbersPerPage - 1,
data.length - 1);
printPageHeader(pageHeader, pageNumber);
printPage(firstIndexOnPage, lastIndexOnPage, data);
printStream.println("\f");
pageNumber++;
}
}
private void printPage(int firstIndexOnPage,
int lastIndexOnPage,
int[] data) {
int firstIndexOfLastRowOnPage =
firstIndexOnPage + rowsPerPage - 1;
for (int firstIndexInRow = firstIndexOnPage;
firstIndexInRow <= firstIndexOfLastRowOnPage;
firstIndexInRow++) {
printRow(firstIndexInRow, lastIndexOnPage, data);
printStream.println("");
}
}
private void printRow(int firstIndexInRow,
int lastIndexOnPage,
int[] data) {
for (int column = 0; column < columnsPerPage; column++) {
int index = firstIndexInRow + column * rowsPerPage;
if (index <= lastIndexOnPage)
printStream.format("%10d", data[index]);
}
}
private void printPageHeader(String pageHeader,
int pageNumber) {
printStream.println(pageHeader + " --- Page " + pageNumber);
printStream.println("");
}
public void setOutput(PrintStream printStream) {
this.printStream = printStream;
}
}Listing 10-8 PrimeGenerator.java
代码清单 10-8 PrimeGenerator.java
java
package literatePrimes;
import java.util.ArrayList;
public class PrimeGenerator {
private static int[] primes;
private static ArrayList<Integer> multiplesOfPrimeFactors;
protected static int[] generate(int n) {
primes = new int[n];
multiplesOfPrimeFactors = new ArrayList<Integer>();
set2AsFirstPrime();
checkOddNumbersForSubsequentPrimes();
return primes;
}
private static void set2AsFirstPrime() {
primes[0] = 2;
multiplesOfPrimeFactors.add(2);
}
private static void checkOddNumbersForSubsequentPrimes() {
int primeIndex = 1;
for (int candidate = 3;
primeIndex < primes.length;
candidate += 2) {
if (isPrime(candidate))
primes[primeIndex++] = candidate;
}
}
private static boolean isPrime(int candidate) {
if (isLeastRelevantMultipleOfNextLargerPrimeFactor(candidate)) {
multiplesOfPrimeFactors.add(candidate);
return false;
}
return isNotMultipleOfAnyPreviousPrimeFactor(candidate);
}
private static boolean
isLeastRelevantMultipleOfNextLargerPrimeFactor(int candidate) {
int nextLargerPrimeFactor = primes[multiplesOfPrimeFactors.size()];
int leastRelevantMultiple = nextLargerPrimeFactor * nextLargerPrimeFactor;
return candidate == leastRelevantMultiple;
}
private static boolean
isNotMultipleOfAnyPreviousPrimeFactor(int candidate) {
for (int n = 1; n < multiplesOfPrimeFactors.size(); n++) {
if (isMultipleOfNthPrimeFactor(candidate, n))
return false;
}
return true;
}
private static boolean
isMultipleOfNthPrimeFactor(int candidate, int n) {
return
candidate == smallestOddNthMultipleNotLessThanCandidate(candidate, n);
}
private static int
smallestOddNthMultipleNotLessThanCandidate(int candidate, int n) {
int multiple = multiplesOfPrimeFactors.get(n);
while (multiple < candidate)
multiple += 2 * primes[n];
multiplesOfPrimeFactors.set(n, multiple);
return multiple;
}
}The first thing you might notice is that the program got a lot longer. It went from a little over one page to nearly three pages in length. There are several reasons for this growth. First, the refactored program uses longer, more descriptive variable names. Second, the refactored program uses function and class declarations as a way to add commentary to the code. Third, we used whitespace and formatting techniques to keep the program readable.
你可能首先注意到的是程序变得更长了。它从略超过一页增长到近三页。这种增长有几个原因。首先,重构后的程序使用更长、更具描述性的变量名。其次,重构后的程序使用函数和类声明作为向代码添加注释的方式。第三,我们使用空白和格式化技术来保持程序的可读性。
Notice how the program has been split into three main responsibilities. The main program is contained in the PrimePrinter class all by itself. Its responsibility is to handle the execution environment. It will change if the method of invocation changes. For example, if this program were converted to a SOAP service, this is the class that would be affected.
注意程序如何被拆分为三个主要职责。主程序单独包含在 PrimePrinter 类中。它的职责是处理执行环境。如果调用方式改变,它将会改变。例如,如果这个程序被转换为 SOAP 服务,这就是会受到影响的类。
The RowColumnPagePrinter knows all about how to format a list of numbers into pages with a certain number of rows and columns. If the formatting of the output needed changing, then this is the class that would be affected.
RowColumnPagePrinter 知道如何将数字列表格式化为具有特定行数和列数的页面。如果输出的格式需要更改,那么这就是会受到影响的类。
The PrimeGenerator class knows how to generate a list prime numbers. Notice that it is not meant to be instantiated as an object. The class is just a useful scope in which its variables can be declared and kept hidden. This class will change if the algorithm for computing prime numbers changes.
PrimeGenerator 类知道如何生成素数列表。注意它不是为了作为对象实例化而设计的。类只是一个有用的作用域,其变量可以在其中声明并保持隐藏。如果计算素数的算法改变,这个类将会改变。
This was not a rewrite! We did not start over from scratch and write the program over again. Indeed, if you look closely at the two different programs, you'll see that they use the same algorithm and mechanics to get their work done.
这不是重写!我们没有从头开始重新编写程序。事实上,如果你仔细观察两个不同的程序,你会发现它们使用相同的算法和机制来完成工作。
The change was made by writing a test suite that verified the precise behavior of the first program. Then a myriad of tiny little changes were made, one at a time. After each change the program was executed to ensure that the behavior had not changed. One tiny step after another, the first program was cleaned up and transformed into the second.
变更是通过编写一个验证第一个程序精确行为的测试套件来完成的。然后进行了无数个小的更改,一次一个。每次更改后都执行程序以确保行为没有改变。一个接一个的小步骤,第一个程序被清理并转换为第二个程序。
10.3 ORGANIZING FOR CHANGE 为变更而组织
For most systems, change is continual. Every change subjects us to the risk that the remainder of the system no longer works as intended. In a clean system we organize our classes so as to reduce the risk of change.
对于大多数系统来说,变更是持续的。每次变更都使我们面临系统其余部分不再按预期工作的风险。在整洁的系统中,我们组织类以减少变更的风险。
The Sql class in Listing 10-9 is used to generate properly formed SQL strings given appropriate metadata. It's a work in progress and, as such, doesn't yet support SQL functionality like update statements. When the time comes for the Sql class to support an update statement, we'll have to "open up" this class to make modifications. The problem with opening a class is that it introduces risk. Any modifications to the class have the potential of breaking other code in the class. It must be fully retested.
代码清单 10-9 中的 Sql 类用于在给定适当元数据的情况下生成格式正确的 SQL 字符串。它是一个正在进行中的工作,因此还不支持像 update 语句这样的 SQL 功能。当 Sql 类需要支持 update 语句时,我们将不得不"打开"这个类进行修改。打开一个类的问题是它引入了风险。对类的任何修改都有可能破坏类中的其他代码。它必须被完全重新测试。
Listing 10-9 A class that must be opened for change
代码清单 10-9 一个必须为变更而打开的类
java
public class Sql { public Sql(String table, Column[] columns)
public String create()
public String insert(Object[] fields)
public String selectAll()
public String findByKey(String keyColumn, String keyValue)
public String select(Column column, String pattern)
public String select(Criteria criteria)
public String preparedInsert()
private String columnList(Column[] columns)
private String valuesList(Object[] fields, final Column[] columns)
private String selectWithCriteria(String criteria)
private String placeholderList(Column[] columns)
}The Sql class must change when we add a new type of statement. It also must change when we alter the details of a single statement type—for example, if we need to modify the select functionality to support subselects. These two reasons to change mean that the Sql class violates the SRP.
当我们添加新类型的语句时,Sql 类必须更改。当我们更改单个语句类型的细节时——例如,如果我们需要修改 select 功能以支持子查询——它也必须更改。这两个修改原因意味着 Sql 类违反了 SRP。
We can spot this SRP violation from a simple organizational standpoint. The method outline of Sql shows that there are private methods, such as selectWithCriteria, that appear to relate only to select statements.
我们可以从简单的组织角度发现这个 SRP 违规。Sql 的方法大纲显示,有一些私有方法,如 selectWithCriteria,似乎只与 select 语句相关。
Private method behavior that applies only to a small subset of a class can be a useful heuristic for spotting potential areas for improvement. However, the primary spur for taking action should be system change itself. If the Sql class is deemed logically complete, then we need not worry about separating the responsibilities. If we won't need update functionality for the foreseeable future, then we should leave Sql alone. But as soon as we find ourselves opening up a class, we should consider fixing our design.
仅适用于类的小子集的私有方法行为可以作为发现潜在改进领域的有用启发式方法。然而,采取行动的主要动力应该是系统变更本身。如果 Sql 类被认为是逻辑完整的,那么我们不需要担心分离职责。如果我们在可预见的将来不需要更新功能,那么我们应该让 Sql 保持不变。但一旦我们发现自己在打开一个类,就应该考虑修复设计。
What if we considered a solution like that in Listing 10-10? Each public interface method defined in the previous Sql from Listing 10-9 is refactored out to its own derivative of the Sql class. Note that the private methods, such as valuesList, move directly where they are needed. The common private behavior is isolated to a pair of utility classes, Where and ColumnList.
如果我们考虑代码清单 10-10 中的解决方案呢?之前代码清单 10-9 中 Sql 定义的每个公共接口方法都被重构到 Sql 类自己的派生类中。注意,私有方法,如 valuesList,直接移动到它们需要的地方。公共的私有行为被隔离到一对工具类 Where 和 ColumnList 中。
Listing 10-10 A set of closed classes
代码清单 10-10 一组封闭的类
java
abstract public class Sql {
public Sql(String table, Column[] columns)
abstract public String generate();
}
public class CreateSql extends Sql {
public CreateSql(String table, Column[] columns)
@Override public String generate()
}
public class SelectSql extends Sql {
public SelectSql(String table, Column[] columns)
@Override public String generate()
}
public class InsertSql extends Sql {
public InsertSql(String table, Column[] columns, Object[] fields)
@Override public String generate()
private String valuesList(Object[] fields, final Column[] columns)
}
public class SelectWithCriteriaSql extends Sql {
public SelectWithCriteriaSql(
String table, Column[] columns, Criteria criteria)
@Override public String generate()
}
public class SelectWithMatchSql extends Sql {
public SelectWithMatchSql(
String table, Column[] columns, Column column, String pattern)
@Override public String generate()
}
public class FindByKeySql extends Sql
public FindByKeySql(
String table, Column[] columns, String keyColumn, String keyValue)
@Override public String generate()
}
public class PreparedInsertSql extends Sql {
public PreparedInsertSql(String table, Column[] columns)
@Override public String generate() {
private String placeholderList(Column[] columns)
}
public class Where {
public Where(String criteria)
public String generate()
}
public class ColumnList {
public ColumnList(Column[] columns)
public String generate()
}The code in each class becomes excruciatingly simple. Our required comprehension time to understand any class decreases to almost nothing. The risk that one function could break another becomes vanishingly small. From a test standpoint, it becomes an easier task to prove all bits of logic in this solution, as the classes are all isolated from one another.
每个类中的代码变得极其简单。我们理解任何类所需的时间几乎降为零。一个函数可能破坏另一个函数的风险变得微乎其微。从测试的角度来看,证明这个解决方案中的所有逻辑位变得更容易,因为所有类都是相互隔离的。
Equally important, when it's time to add the update statements, none of the existing classes need change! We code the logic to build update statements in a new subclass of Sql named UpdateSql. No other code in the system will break because of this change.
同样重要的是,当需要添加 update 语句时,现有的类都不需要更改!我们在一个名为 UpdateSql 的新子类中编写构建 update 语句的逻辑。系统中的其他代码不会因为这个更改而被破坏。
Our restructured Sql logic represents the best of all worlds. It supports the SRP. It also supports another key OO class design principle known as the Open-Closed Principle, or OCP:4 Classes should be open for extension but closed for modification. Our restructured Sql class is open to allow new functionality via subclassing, but we can make this change while keeping every other class closed. We simply drop our UpdateSql class in place.
我们重构后的 Sql 逻辑代表了最好的方案。它支持 SRP。它还支持另一个关键的面向对象类设计原则,即开闭原则(OCP)[4]:类应该对扩展开放,但对修改关闭。我们重构后的 Sql 类是开放的,允许通过子类化添加新功能,但我们可以进行这个更改的同时保持其他每个类关闭。我们只需将 UpdateSql 类放入即可。
- [PPP].
[4] [PPP]。
We want to structure our systems so that we muck with as little as possible when we update them with new or changed features. In an ideal system, we incorporate new features by extending the system, not by making modifications to existing code.
我们希望构建系统,使得在用新功能或更改的功能更新系统时,我们尽可能少地改动。在理想的系统中,我们通过扩展系统来整合新功能,而不是通过修改现有代码。
10.3.1 Isolating from Change 隔离变化
Needs will change, therefore code will change. We learned in OO 101 that there are concrete classes, which contain implementation details (code), and abstract classes, which represent concepts only. A client class depending upon concrete details is at risk when those details change. We can introduce interfaces and abstract classes to help isolate the impact of those details.
需求会改变,因此代码也会改变。我们在面向对象基础课程中学到,有具体类,包含实现细节(代码),以及抽象类,仅表示概念。依赖具体细节的客户端类在这些细节改变时面临风险。我们可以引入接口和抽象类来帮助隔离这些细节的影响。
Dependencies upon concrete details create challenges for testing our system. If we're building a Portfolio class and it depends upon an external TokyoStockExchange API to derive the portfolio's value, our test cases are impacted by the volatility of such a lookup. It's hard to write a test when we get a different answer every five minutes!
依赖具体细节给测试系统带来了挑战。如果我们正在构建一个 Portfolio 类,它依赖于外部的 TokyoStockExchange API 来推导投资组合的价值,我们的测试用例会受到这种查询波动性的影响。当每五分钟得到一个不同的答案时,很难编写测试!
Instead of designing Portfolio so that it directly depends upon TokyoStockExchange, we create an interface, StockExchange, that declares a single method:
我们不是设计 Portfolio 直接依赖于 TokyoStockExchange,而是创建一个接口 StockExchange,它声明一个方法:
java
public interface StockExchange {
Money currentPrice(String symbol);
}We design TokyoStockExchange to implement this interface. We also make sure that the constructor of Portfolio takes a StockExchange reference as an argument:
我们设计 TokyoStockExchange 来实现这个接口。我们还确保 Portfolio 的构造函数接受一个 StockExchange 引用作为参数:
java
public Portfolio {
private StockExchange exchange;
public Portfolio(StockExchange exchange) {
this.exchange = exchange;
}
// …
}Now our test can create a testable implementation of the StockExchange interface that emulates the TokyoStockExchange. This test implementation will fix the current value for any symbol we use in testing. If our test demonstrates purchasing five shares of Microsoft for our portfolio, we code the test implementation to always return $100 per share of Microsoft. Our test implementation of the StockExchange interface reduces to a simple table lookup. We can then write a test that expects $500 for our overall portfolio value.
现在我们的测试可以创建一个模拟 TokyoStockExchange 的 StockExchange 接口的可测试实现。这个测试实现将固定我们在测试中使用的任何符号的当前值。如果我们的测试演示了为投资组合购买五股微软股票,我们编写测试实现始终返回每股微软 100 美元。我们的 StockExchange 接口测试实现简化为简单的表查找。然后我们可以编写一个期望投资组合总价值为 500 美元的测试。
java
public class PortfolioTest {
private FixedStockExchangeStub exchange;
private Portfolio portfolio;
@Before
protected void setUp() throws Exception {
exchange = new FixedStockExchangeStub();
exchange.fix("MSFT", 100);
portfolio = new Portfolio(exchange);
}
@Test
public void GivenFiveMSFTTotalShouldBe500() throws Exception {
portfolio.add(5, "MSFT");
Assert.assertEquals(500, portfolio.value());
}
}If a system is decoupled enough to be tested in this way, it will also be more flexible and promote more reuse. The lack of coupling means that the elements of our system are better isolated from each other and from change. This isolation makes it easier to understand each element of the system.
如果一个系统解耦到足以以这种方式进行测试,它也将更加灵活并促进更多的复用。缺乏耦合意味着我们系统的元素彼此之间以及与变更之间更好地隔离。这种隔离使理解系统的每个元素变得更容易。
By minimizing coupling in this way, our classes adhere to another class design principle known as the Dependency Inversion Principle (DIP).5 In essence, the DIP says that our classes should depend upon abstractions, not on concrete details.
通过以这种方式最小化耦合,我们的类遵循了另一个类设计原则,即依赖倒置原则(DIP)[5]。本质上,DIP 说我们的类应该依赖于抽象,而不是具体细节。
- [PPP].
[5] [PPP]。
Instead of being dependent upon the implementation details of the TokyoStock-Exchange class, our Portfolio class is now dependent upon the StockExchange interface. The StockExchange interface represents the abstract concept of asking for the current price of a symbol. This abstraction isolates all of the specific details of obtaining such a price, including from where that price is obtained.
我们的 Portfolio 类现在不再依赖于 TokyoStockExchange 类的实现细节,而是依赖于 StockExchange 接口。StockExchange 接口表示请求符号当前价格的抽象概念。这种抽象隔离了获取此类价格的所有具体细节,包括从哪里获取该价格。