💡 总结与小计 (Summary & Key Takeaways)
本章的核心主题是如何编写整洁的类 (Clean Classes)。如果在函数层面我们关注的是逻辑的表达,那么在类层面我们关注的是代码的组织结构和抽象层次。
关键知识点:
- 类的组织结构:遵循标准的 Java 约定(常量 -> 静态变量 -> 实例变量 -> 公共函数 -> 私有工具函数)。遵循“自顶向下”原则。
- 类应该短小 (Classes Should Be Small):
- 单一职责原则 (SRP):类应该只有一个修改的理由。
- 衡量标准:不是代码行数,而是职责的数量。类名如果无法简洁地命名(包含 "Manager", "Processor" 或 "And"),通常意味着职责过多。
- 内聚性 (Cohesion):
- 类应该只有少量的实例变量。
- 类中的方法应该操作类中的变量。方法操作的变量越多,内聚性越高。
- 保持内聚性会导致许多小类:当你试图将一个大函数拆分成小函数时,如果发现多个函数共享某些变量,这通常是将其拆分为新类的信号。
- 为了修改而组织 (Organizing for Change):
- 开闭原则 (OCP):类应该对扩展开放,对修改关闭。
- 通过将具体实现(如 SQL 生成)拆分为子类,可以在添加新功能时不修改现有代码,降低风险。
- 隔离修改 (Isolating from Change):
- 依赖倒置原则 (DIP):依赖于抽象(接口),而不是具体细节(实现类)。
- 引入接口和抽象类可以隔离系统对外部易变因素(如股票交易所 API)的依赖,从而提高可测试性和灵活性。
📖 中英对照翻译
第 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. 到目前为止,本书主要关注如何写好代码行和代码块。我们深入探讨了函数的合理构成及其相互关系。但是,即便我们对代码语句的表达能力及其组成的函数倾注了大量心血,若不关注更高层次的代码组织,仍然无法得到整洁的代码。让我们来谈谈整洁的类。
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 static constants),应该最先出现。然后是私有静态变量,紧接着是私有实例变量。很少有正当理由去使用公共变量。
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),以便测试可以访问它们。对我们来说,测试至上。如果同一个包内的测试需要调用某个函数或访问某个变量,我们会将其设置为 protected 或包级作用域。然而,我们会首先寻找保持隐私的方式。放松封装始终是最后的手段。
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 对于函数,我们通过计算物理行数来衡量大小。对于类,我们使用不同的衡量标准。我们计算职责。
- [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 (职责过多)
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? (够小了吗?)
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 个词写出对该类的简短描述,且不使用“如果(if)”、“和(and)”、“或(or)”或者“但是(but)”等词。我们会如何描述 SuperDashboard 呢?“SuperDashboard 提供了对最后持有焦点的组件的访问能力,并且它还允许我们跟踪版本号和构建号。” 这里的第一个“并且(and)”就是一个暗示,表明 SuperDashboard 的职责过多。
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)规定,一个类或模块应该有且只有一个修改的理由。这个原则既给了我们职责的定义,也给了我们关于类大小的指导方针。类应该只有一个职责——即一个修改的理由。
- You can read much more about this principle in [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 的派生类,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 (单一职责类)
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. 重申一下前面的观点以示强调:我们希望系统由许多小类组成,而不是由少量大类组成。每个小类封装单一的职责,只有一个修改的理由,并与少数其他类协作以实现预期的系统行为。
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. 类应该只有少量的实例变量。类的每个方法都应该操作一个或多个这种变量。通常而言,一个方法操作的变量越多,该方法对类的内聚性就越高。如果一个类中的每个变量都被每个方法所使用,那么该类就是极大内聚(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. (内聚的类)
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. 保持函数短小和参数列表简短的策略,有时会导致被一部分方法所使用的实例变量激增。当这种情况发生时,几乎总是意味着至少有另一个类正试图从这个大类中挣脱出来。你应该尝试将这些变量和方法分离到两个或更多的类中,从而使新类具有更高的内聚性。
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)中的经典例子。代码清单 10-5 展示了 Knuth 的 PrintPrimes 程序的 Java 译本。为了对 Knuth 公平起见,这不是他编写的原始程序,而是他的 WEB 工具输出的结果。我使用它是因为它是将一个大函数分解为许多小函数和类的绝佳起点。
- [Knuth92].
Listing 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;
}
}
}
}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) (重构后)
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
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
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. 这种改变是通过编写一个验证第一个程序精确行为的测试套件来实现的。然后进行了无数次微小的修改,一次改一点。每次修改后都会执行程序,以确保行为没有改变。一步接一步,第一个程序被清理并转化为第二个程序。
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 (必须打开以进行修改的类)
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 类被认为是逻辑完整的,那么我们就不必担心分离职责。如果在可预见的未来我们不需要 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 (一组封闭的类)
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 的 Sql 新子类中编写构建 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。它还支持另一个关键的面向对象类设计原则,即开闭原则(Open-Closed Principle, OCP):类应该对扩展开放,但对修改关闭。我们重组后的 Sql 类是开放的,允许通过子类化添加新功能,但我们可以在保持所有其他类关闭的情况下进行此更改。我们只需放入 UpdateSql 类即可。
- [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. 我们希望构建这样的系统:当用新特性或变更特性更新它们时,我们尽可能少地去乱动(muck with)现有代码。在一个理想的系统中,我们通过扩展系统来整合新特性,而不是通过修改现有代码。
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! 依赖于具体细节会给测试我们的系统带来挑战。如果我们正在构建一个 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,它声明了一个方法:
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 引用作为参数:
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. 现在,我们的测试可以创建一个可测试的 StockExchange 接口实现,来模拟 TokyoStockExchange。这个测试实现将固定我们在测试中使用的任何股票代码的当前值。如果我们的测试演示了为投资组合购买 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());
}
}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 认为我们的类应该依赖于抽象,而不是依赖于具体细节。
- [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 接口代表了查询股票代码当前价格的抽象概念。这个抽象隔离了获取该价格的所有具体细节,包括价格从哪里获得。