第 12 章 Emergence 迭进
# 第 12 章 Emergence 迭进
by Jeff Langr
# 12.1 GETTING CLEAN VIA EMERGENT DESIGN 通过迭进设计达到整洁目的
What if there were four simple rules that you could follow that would help you create good designs as you worked? What if by following these rules you gained insights into the structure and design of your code, making it easier to apply principles such as SRP and DIP? What if these four rules facilitated the emergence of good designs?
假使有 4 条简单的规矩,跟着做就能帮助你创建优良的设计,会如何?假使遵循这些规矩你就能洞见代码的结构和设计,更轻易地应用 SRP 和 DIP 之类原则,又会如何?
Many of us feel that Kent Beck’s four rules of Simple Design1 are of significant help in creating well-designed software.
我们中的许多人认为,Kent Beck 关于简单设计的四条规则,对于创建具有良好设计的软件有着莫大的帮助。
According to Kent, a design is “simple” if it follows these rules:
据 Kent 所述,只要遵循以下规则,设计就能变得“简单”:
- Runs all the tests
- Contains no duplication
- Expresses the intent of the programmer
- Minimizes the number of classes and methods
- 运行所有测试;
- 不可重复;
- 表达了程序员的意图;
- 尽可能减少类和方法的数量;
The rules are given in order of importance.
以上规则按其重要程度排列。
# 12.2 SIMPLE DESIGN RULE 1: RUNS ALL THE TESTS 简单设计规则 1:运行所有测试
First and foremost, a design must produce a system that acts as intended. A system might have a perfect design on paper, but if there is no simple way to verify that the system actually works as intended, then all the paper effort is questionable.
设计必须制造出如预期一般工作的系统,这是首要因素。系统也许有一套绝佳设计,但如果缺乏验证系统是否真按预期那样工作的简单方法,那就无异于纸上谈兵。
A system that is comprehensively tested and passes all of its tests all of the time is a testable system. That’s an obvious statement, but an important one. Systems that aren’t testable aren’t verifiable. Arguably, a system that cannot be verified should never be deployed.
全面测试并持续通过所有测试的系统,就是可测试的系统。看似浅显,但却重要。不可测试的系统同样不可验证。不可验证的系统,绝不应部署。
Fortunately, making our systems testable pushes us toward a design where our classes are small and single purpose. It’s just easier to test classes that conform to the SRP. The more tests we write, the more we’ll continue to push toward things that are simpler to test. So making sure our system is fully testable helps us create better designs.
幸运的是,只要系统可测试,就会导向保持类短小且目的单一的设计方案。遵循 SRP 的类,测试起来较为简单。测试编写得越多,就越能持续走向编写较易测试的代码。所以,确保系统完全可测试能帮助我们创建更好的设计。
Tight coupling makes it difficult to write tests. So, similarly, the more tests we write, the more we use principles like DIP and tools like dependency injection, interfaces, and abstraction to minimize coupling. Our designs improve even more.
紧耦合的代码难以编写测试。同样,编写测试越多,就越会遵循 DIP 之类规则,使用依赖注入、接口和抽象等工具尽可能减少耦合。如此一来,设计就有长足进步。
Remarkably, following a simple and obvious rule that says we need to have tests and run them continuously impacts our system’s adherence to the primary OO goals of low coupling and high cohesion. Writing tests leads to better designs.
遵循有关编写测试并持续运行测试的简单、明确的规则,系统就会更贴近 OO 低耦合度、高内聚度的目标。编写测试引致更好的设计。
# 12.3 SIMPLE DESIGN RULES 2–4: REFACTORING 简单设计规则 2 ~ 4:重构
Once we have tests, we are empowered to keep our code and classes clean. We do this by incrementally refactoring the code. For each few lines of code we add, we pause and reflect on the new design. Did we just degrade it? If so, we clean it up and run our tests to demonstrate that we haven’t broken anything. The fact that we have these tests eliminates the fear that cleaning up the code will break it!
有了测试,就能保持代码和类的整洁,方法就是递增式地重构代码。添加了几行代码后,就要暂停,琢磨一下变化了的设计。设计退步了吗?如果是,就要清理它,并且运行测试,保证没有破坏任何东西。测试消除了对清理代码就会破坏代码的恐惧。
During this refactoring step, we can apply anything from the entire body of knowledge about good software design. We can increase cohesion, decrease coupling, separate concerns, modularize system concerns, shrink our functions and classes, choose better names, and so on. This is also where we apply the final three rules of simple design: Eliminate duplication, ensure expressiveness, and minimize the number of classes and methods.
在重构过程中,可以应用有关优秀软件设计的一切知识。提升内聚性,降低耦合度,切分关注面,模块化系统性关注面,缩小函数和类的尺寸,选用更好的名称,如此等等。这也是应用简单设计后三条规则的地方:消除重复,保证表达力,尽可能减少类和方法的数量。
# 12.4 NO DUPLICATION 不可重复
Duplication is the primary enemy of a well-designed system. It represents additional work, additional risk, and additional unnecessary complexity. Duplication manifests itself in many forms. Lines of code that look exactly alike are, of course, duplication. Lines of code that are similar can often be massaged to look even more alike so that they can be more easily refactored. And duplication can exist in other forms such as duplication of implementation. For example, we might have two methods in a collection class:
重复是拥有良好设计系统的大敌。它代表着额外的工作、额外的风险和额外且不必要的复杂度。重复有多种表现。极其雷同的代码行当然是重复。类似的代码往往可以调整得更相似,这样就能更容易地进行重构。重复也有实现上的重复等其他一些形态。例如,在某个群集类中可能会有两个方法:
int size() {}
boolean isEmpty() {}
2
We could have separate implementations for each method. The isEmpty method could track a boolean, while size could track a counter. Or, we can eliminate this duplication by tying isEmpty to the definition of size:
这两个方法可以分别实现。isEmpty 方法跟踪一个布尔值,而 size 方法则跟踪一个计数器。或者,也可以通过在 isEmpty 中使用 size 方法来消除重复:
boolean isEmpty() {
return 0 == size();
}
2
3
Creating a clean system requires the will to eliminate duplication, even in just a few lines of code. For example, consider the following code:
要想创建整洁的系统,需要有消除重复的意愿,即便对于短短几行也是如此。例如以下代码:
public void scaleToOneDimension(
float desiredDimension, float imageDimension) {
if (Math.abs(desiredDimension - imageDimension) < errorThreshold)
return;
float scalingFactor = desiredDimension / imageDimension;
scalingFactor = (float)(Math.floor(scalingFactor * 100) * 0.01f);
RenderedOp newImage = ImageUtilities.getScaledImage(
image, scalingFactor, scalingFactor);
image.dispose();
System.gc();
image = newImage;
}
public synchronized void rotate(int degrees) {
RenderedOp newImage = ImageUtilities.getRotatedImage(
image, degrees);
image.dispose();
System.gc();
image = newImage;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
To keep this system clean, we should eliminate the small amount of duplication between the scaleToOneDimension and rotate methods:
要保持系统整洁,应该消除 scaleToOneDimension 和 rotate 方法里面的少量重复:
public void scaleToOneDimension(
float desiredDimension, float imageDimension) {
if (Math.abs(desiredDimension - imageDimension) < errorThreshold)
return;
float scalingFactor = desiredDimension / imageDimension;
scalingFactor = (float)(Math.floor(scalingFactor * 100) * 0.01f);
replaceImage(ImageUtilities.getScaledImage(
image, scalingFactor, scalingFactor));
}
public synchronized void rotate(int degrees) {
replaceImage(ImageUtilities.getRotatedImage(image, degrees));
}
privatex void replaceImage(RenderedOp newImage) {
image.dispose();
System.gc();
image = newImage;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
As we extract commonality at this very tiny level, we start to recognize violations of SRP. So we might move a newly extracted method to another class. That elevates its visibility. Someone else on the team may recognize the opportunity to further abstract the new method and reuse it in a different context. This “reuse in the small” can cause system complexity to shrink dramatically. Understanding how to achieve reuse in the small is essential to achieving reuse in the large.
做了一点点共性抽取后,我们意识到已经违反了 SRP 原则。所以,可以把一个新方法分解到另外的类中,从而提升其可见性。团队中的其他成员也许会发现进一步抽象新方法的机会,并且在其他场景中复用之。“小规模复用”可大量降低系统复杂性。要想实现大规模复用,必须理解如何实现小规模复用。
The TEMPLATE METHOD2 pattern is a common technique for removing higher-level duplication. For example:
模板方法模式是一种移除高层级重复的通用技巧。例如:
public class VacationPolicy {
public void accrueUSDivisionVacation() {
// code to calculate vacation based on hours worked to date
// …
// code to ensure vacation meets US minimums
// …
// code to apply vaction to payroll record
// …
}
public void accrueEUDivisionVacation() {
// code to calculate vacation based on hours worked to date
// …
// code to ensure vacation meets EU minimums
// …
// code to apply vaction to payroll record
// …
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
The code across accrueUSDivisionVacation and accrueEuropeanDivisionVacation is largely the same, with the exception of calculating legal minimums. That bit of the algorithm changes based on the employee type.
除了计算法定最少数量假期的部分,accrueUSDivisionVacation 和 accrueEuropeanDivision Vacation 中有大量代码雷同。那部分的算法,依据员工类型而变。
We can eliminate the obvious duplication by applying the TEMPLATE METHOD pattern.
可以通过应用模板方法模式来消除明显的重复。
abstract public class VacationPolicy {
public void accrueVacation() {
calculateBaseVacationHours();
alterForLegalMinimums();
applyToPayroll();
}
private void calculateBaseVacationHours() { /* … */ };
abstract protected void alterForLegalMinimums();
private void applyToPayroll() { /* … */ };
}
public class USVacationPolicy extends VacationPolicy {
@Override protected void alterForLegalMinimums() {
// US specific logic
}
}
public class EUVacationPolicy extends VacationPolicy {
@Override protected void alterForLegalMinimums() {
// EU specific logic
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
The subclasses fill in the “hole” in the accrueVacation algorithm, supplying the only bits of information that are not duplicated.
子类填充了 accrueVacation 算法中的“空洞”,提供不重复的信息。
# 12.5 EXPRESSIVE 表达力
Most of us have had the experience of working on convoluted code. Many of us have produced some convoluted code ourselves. It’s easy to write code that we understand, because at the time we write it we’re deep in an understanding of the problem we’re trying to solve. Other maintainers of the code aren’t going to have so deep an understanding.
我们中的大多数人都经历过费解代码的纠缠。我们中的许多人自己就编写过费解的代码。写出自己能理解的代码很容易,因为在写这些代码时,我们正深入于要解决的问题中。代码的其他维护者不会那么深入,也就不易理解代码。
The majority of the cost of a software project is in long-term maintenance. In order to minimize the potential for defects as we introduce change, it’s critical for us to be able to understand what a system does. As systems become more complex, they take more and more time for a developer to understand, and there is an ever greater opportunity for a misunderstanding. Therefore, code should clearly express the intent of its author. The clearer the author can make the code, the less time others will have to spend understanding it. This will reduce defects and shrink the cost of maintenance.
软件项目的主要成本在于长期维护。为了在修改时尽量降低出现缺陷的可能性,很有必要理解系统是做什么的。当系统变得越来越复杂,开发者就需要越来越多的时间来理解它,而且也极有可能误解。所以,代码应当清晰地表达其作者的意图。作者把代码写得越清晰,其他人花在理解代码上的时间也就越少,从而减少缺陷,缩减维护成本。
You can express yourself by choosing good names. We want to be able to hear a class or function name and not be surprised when we discover its responsibilities.
可以通过选用好名称来表达。我们想要听到好类名和好函数名,而且在查看其权责时不会大吃一惊。
You can also express yourself by keeping your functions and classes small. Small classes and functions are usually easy to name, easy to write, and easy to understand.
也可以通过保持函数和类尺寸短小来表达。短小的类和函数通常易于命名,易于编写,易于理解。
You can also express yourself by using standard nomenclature. Design patterns, for example, are largely about communication and expressiveness. By using the standard pattern names, such as COMMAND or VISITOR, in the names of the classes that implement those patterns, you can succinctly describe your design to other developers.
还可以通过采用标准命名法来表达。例如,设计模式很大程度上就关乎沟通和表达。通过在实现这些模式的类的名称中采用标准模式名,例如 COMMAND 或 VISITOR,就能充分地向其他开发者描述你的设计。
Well-written unit tests are also expressive. A primary goal of tests is to act as documentation by example. Someone reading our tests should be able to get a quick understanding of what a class is all about.
编写良好的单元测试也具有表达性。测试的主要目的之一就是通过实例起到文档的作用。读到测试的人应该能很快理解某个类是做什么的。
But the most important way to be expressive is to try. All too often we get our code working and then move on to the next problem without giving sufficient thought to making that code easy for the next person to read. Remember, the most likely next person to read the code will be you.
不过,做到有表达力的最重要方式却是尝试。有太多时候,我们写出能工作的代码,就转移到下一个问题上,没有下足功夫调整代码,让后来者易于阅读。记住,下一位读代码的人最有可能是你自己。
So take a little pride in your workmanship. Spend a little time with each of your functions and classes. Choose better names, split large functions into smaller functions, and generally just take care of what you’ve created. Care is a precious resource.
所以,多少尊重一下你的手艺吧。花一点点时间在每个函数和类上。选用较好的名称,将大函数切分为小函数,时时照拂自己创建的东西。用心是最珍贵的资源。
# 12.6 MINIMAL CLASSES AND METHODS 尽可能少的类和方法
Even concepts as fundamental as elimination of duplication, code expressiveness, and the SRP can be taken too far. In an effort to make our classes and methods small, we might create too many tiny classes and methods. So this rule suggests that we also keep our function and class counts low.
即便是消除重复、代码表达力和 SRP 等最基础的概念也会被过度使用。为了保持类和函数短小,我们可能会造出太多的细小类和方法。所以这条规则也主张函数和类的数量要少。
High class and method counts are sometimes the result of pointless dogmatism. Consider, for example, a coding standard that insists on creating an interface for each and every class. Or consider developers who insist that fields and behavior must always be separated into data classes and behavior classes. Such dogma should be resisted and a more pragmatic approach adopted.
类和方法的数量太多,有时是由毫无意义的教条主义导致的。例如,某个编码标准就坚称应当为每个类创建接口。也有开发者认为,字段和行为必须切分到数据类和行为类中。应该抵制这类教条,采用更实用的手段。
Our goal is to keep our overall system small while we are also keeping our functions and classes small. Remember, however, that this rule is the lowest priority of the four rules of Simple Design. So, although it’s important to keep class and function count low, it’s more important to have tests, eliminate duplication, and express yourself.
我们的目标是在保持函数和类短小的同时,保持整个系统短小精悍。不过要记住,这在关于简单设计的四条规则里面是优先级最低的一条。所以,尽管使类和函数的数量尽量少是很重要的,但更重要的却是测试、消除重复和表达力。
# 12.7 CONCLUSION 小结
Is there a set of simple practices that can replace experience? Clearly not. On the other hand, the practices described in this chapter and in this book are a crystallized form of the many decades of experience enjoyed by the authors. Following the practice of simple design can and does encourage and enable developers to adhere to good principles and patterns that otherwise take years to learn.
有没有能替代经验的一套简单实践手段呢?当然不会有。另一方面,本章中写到的实践来自于本书作者数十年经验的精练总结。遵循简单设计的实践手段,开发者不必经年学习就能掌握好的原则和模式。