Skip to content

第 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?

假如有四条简单的规则,你可以在工作时遵循它们来帮助自己创建出良好的设计,会怎样?假如遵循这些规则能让你洞察代码的结构和设计,使得 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 的四条简单设计规则[1]对于创建设计良好的软件有着极大的帮助。

  1. [XPE].

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 简单设计规则一:通过所有测试

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.

值得注意的是,遵循一条简单而明显的规则——我们需要有测试并持续运行它们——会影响我们的系统对面向对象主要目标的遵从度,即低耦合和高内聚。编写测试带来更好的设计。

12.3 SIMPLE DESIGN RULES 2–4: REFACTORING 简单设计规则二至四:重构

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:

重复是良好设计系统的头号大敌。它代表了额外的工作量、额外的风险和额外不必要的复杂度。重复以多种形式表现出来。看起来完全相同的代码行当然是重复。看起来相似的代码行也往往可以被修整得更加相似,以便更容易重构。重复还可以存在于其他形式中,比如实现的重复。例如,我们可能在某个集合类中有两个方法:

java
   int size() {}
   boolean isEmpty() {}

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 的定义来消除这种重复:

java
   boolean isEmpty() {
      return 0 == size();
   }

Creating a clean system requires the will to eliminate duplication, even in just a few lines of code. For example, consider the following code:

创建整洁的系统需要消除重复的意志,哪怕只是几行代码。例如,考虑以下代码:

java
   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;
   }

To keep this system clean, we should eliminate the small amount of duplication between the scaleToOneDimension and rotate methods:

为了保持这个系统的整洁,我们应该消除 scaleToOneDimension 和 rotate 方法之间少量的重复:

java
   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));
   }
   private void replaceImage(RenderedOp newImage) {
      image.dispose();
      System.gc();
      image = newImage;
   }

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:

模板方法(TEMPLATE METHOD)[2]模式是一种消除更高层次重复的常见技术。例如:

java
   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
         // …
      }
   }

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 和 accrueEuropeanDivisionVacation 中的代码大体相同,只有计算法定最低值的部分不同。算法中这部分会根据员工类型而变化。

We can eliminate the obvious duplication by applying the TEMPLATE METHOD pattern.

我们可以通过应用模板方法模式来消除这种显而易见的重复。

java
   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
      }
   }

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.

有没有一组简单的实践可以取代经验?显然没有。另一方面,本章和本书中描述的实践是作者们数十年经验的结晶形式。遵循简单设计的实践能够也确实在鼓励和促使开发者遵从良好的原则和模式,而这些原则和模式本来需要多年才能学会。