第 10 章 Classes 类
# 第 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.
公共函数应跟在变量列表之后。我们喜欢把由某个公共函数调用的私有工具函数紧随在该公共函数后面。这符合了自顶向下原则,让程序读起来就像一篇报纸文章。
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.
我们喜欢保持变量和工具函数的私有性,但并不执着于此。有时,我们也需要用到受护(protected)变量或工具函数,好让测试可以访问到。对我们来说,测试说了算。若同一程序包内的某个测试需要调用一个函数或变量,我们就会将该函数或变量置为受护或在整个程序包内可访问。然而,我们首先会想办法使之保有隐私。放松封装总是下策。
# 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.
对于函数,我们通过计算代码行数衡量大小。对于类,我们采用不同的衡量方法,计算权责(responsibility)。
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
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 …
}
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
But what if SuperDashboard contained only the methods shown in Listing 10-2?
如果 SuperDashboard 类只包括代码清单 10-2 中的方法呢?
Listing 10-2 Small Enough?
代码清单 10-2 足够短小了吗?
public class SuperDashboard extends JFrame implements MetaDataUser {
public Component getLastFocusedComponent() {
}
public void setLastFocused(Component lastFocused) {
}
public int getMajorVersionNumber() {
}
public int getMinorVersionNumber() {
}
public int getBuildNumber() {
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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.
5 个方法不算多,在这里,虽然方法数量较少,可 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 个单词简要描述一个类,且不用“若(if)”、“与(and)”、“或(or)”或者“但(but)”等词汇。我们该如何描述 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)认为,类或模块应有且只有一条加以修改的理由。该原则既给出了权责的定义,又是关于类的长度的指导方针。类只应有一个权责——只有一条修改的理由。
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,顶层 GUI 窗口的 Swing 表现形态)。每次修改 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 单一权责类
public class Version {
public int getMajorVersionNumber() {
}
public int getMinorVersionNumber() {
}
public int getBuildNumber() {
}
}
2
3
4
5
6
7
8
9
10
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 是 OO 设计中最为重要的概念之一,也是较为容易理解和遵循的概念之一。奇怪的是 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(一个内聚类)
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;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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?
仅仅是将较大的函数切割为小函数,就将导致更多的类出现。想想看一个有许多变量的大函数。你想把该函数中某一小部分拆解成单独的函数。不过,你想要拆出来的代码使用了该函数中声明的 4 个变量。是否必须将这 4 个变量都作为参数传递到新函数中去呢?
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.
完全没必要!只要将 4 个变量提升为类的实体变量,完全无需传递任何变量就能拆解代码了。应该很容易将函数拆分为小块。
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.
可惜这也意味着类丧失了内聚性,因为堆积了越来越多只为允许少量函数共享而存在的实体变量。等一下!如果有些函数想要共享某些变量,为什么不让它们拥有自己的类呢?当类丧失了内聚性,就拆分它!所以,将大函数拆为许多小函数,往往也是将类拆分为多个小类的时机。程序会更加有组织,也会拥有更为透明的结构。
Listing 10-5 PrintPrimes.java
代码清单 10-5 PrintPrimes.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;
}
}
}
}
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
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(重构后)
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);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Listing 10-7 RowColumnPagePrinter.java
代码清单 10-7 RowColumnPagePrinter.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;
}
}
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
Listing 10-8 PrimeGenerator.java
代码清单 10-8 PrimeGenerator.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;
}
}
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
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.
你可能注意到的第一件事就是程序比原来长了许多,从 1 页多增加到了将近 3 页。这有几个原因。其一,重构后的程序采用了更长、更有描述性的变量名。其二,重构后的程序将函数和类声明当作是给代码添加注释的一种手段。其三,我们采用了空格和格式技巧让程序更可读。
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.
留意程序是如何被拆分为 3 个主要权责的。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 一个必须打开修改的类
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) {
}
}
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
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 类在逻辑上已具足,则无需担心对权责的拆分。如果在可预见的未来无需增加 update 功能,就该不去动 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 一组封闭类
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() {
}
}
}
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
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 语句时,现存类无需做任何修改,这也同等重要!我们在 Sql 类的新子类 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):类应当对扩展开放,对修改封闭。通过子类化手段,重新架构的 Sql 类对添加新功能是开放的,而且可以同时不触及其他类。只要将 UpdateSql 类放置到位就行了。
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.
我们希望将系统打造成在添加或修改特性时尽可能少惹麻烦的架子。在理想系统中,我们通过扩展系统而非修改现有代码来添加新特性。
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.
需求会改变,所以代码也会改变。在 OO 101 中,我们学习到,具体类包含实现细节(代码),而抽象类则只呈现概念。依赖于具体细节的客户类,当细节改变时,就会有风险。我们可以借助接口和抽象类来隔离这些细节带来的影响。
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!
对具体细节的依赖给对系统的测试带来了挑战。如果我们构建一个依赖于外部 TokyoStockExchange API 的 Portfolio 类,代表投资组合的价值,则测试用例就会受到价值查询的连带影响。如果每 5 分钟就有新说法,就很难写出测试来。
Instead of designing Portfolio so that it directly depends upon TokyoStockExchange, we create an interface, StockExchange, that declares a single method:
与其设计直接依赖于 TokyoStockExchange 的 Portfolio 类,不如创建 StockExchange 接口,其中只声明一个方法:
public interface StockExchange {
Money currentPrice(String symbol);
}
2
3
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 引用:
public Portfolio {
private StockExchange exchange;
public Portfolio(StockExchange exchange) {
this.exchange = exchange;
}
// …
}
2
3
4
5
6
7
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.
现在就可以为 StockExchange 接口创建可测试的尝试性实现了。该尝试性实现将返回固定的现值。如果测试中购买了 5 股微软股票,则尝试性实现总是返回每股 100 美元的现值。对于 StockExchange 接口的尝试性实现简化为简单的表格查找。然后再编写一个总投资价值为 500 美元的测试。
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());
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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.
通过降低连接度,我们的类就遵循了另一条类设计原则,依赖倒置原则(Dependency Inversion Principle,DIP)。本质而言,DIP 认为类应当依赖于抽象而不是依赖于具体细节。
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 接口呈现的是有关询问某只股票价格的抽象概念。这种抽象隔离了所有询价的特定细节,包括价格数据来自何处之类。