Skip to content

这里是《Clean Code》(代码整洁之道)第12章 "Emergence" 的中英对照翻译。


💡 总结与小计 (Summary & Key Takeaways)

本章核心主旨: 本章探讨了“迭进式设计”(Emergent Design)的概念,即良好的软件设计并非必须由预先的大规模设计(Big Design Up Front)产生,而是可以通过遵循 Kent Beck 的简单设计四条规则(Four Rules of Simple Design) 在开发过程中自然“涌现”出来。

关键知识点 (Key Takeaways):

  1. 简单设计的四条规则(按重要性排序):

    1. 运行所有测试 (Runs all the tests):这是根本。不可测试的系统不可验证,不可部署。测试迫使我们编写低耦合、高内聚的代码。
    2. 消除重复 (Contains no duplication):重复是良好设计的大敌。通过提取共性(如使用模板方法模式)来降低复杂性。
    3. 表达意图 (Expresses the intent):代码应当清晰地表达作者的意图。使用好名字、短函数和标准命名法,以此降低维护成本。
    4. 最小化类和方法的数量 (Minimizes the number of classes and methods):在遵守前三条规则的前提下,避免过度设计和无意义的教条主义,保持系统精简。
  2. 重构的重要性:有了测试作为安全网,我们可以无所畏惧地重构代码。重构是应用软件设计原则(如 SRP、DIP)的时刻。

  3. 设计是动态的:遵循这些简单的实践,可以替代多年的经验积累,让良好的设计自然浮现。


📖 中英对照翻译

第 12 章 Emergence (迭进)

by Jeff Langr

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(依赖倒置原则)等原则,那会怎样?如果这四条规则能促进优良设计的迭进(emergence),那会怎样?

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. [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. 这些规则是按重要程度排列的。

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. 值得注意的是,遵循“拥有测试并持续运行测试”这一简单明显的规则,会对系统遵循“低耦合、高内聚”这一主要的面向对象目标产生深远影响。编写测试能引致更好的设计。

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. 在重构步骤中,我们可以应用关于良好软件设计的所有知识。我们可以增加内聚、降低耦合、分离关注点、模块化系统关注点、缩小函数和类、选择更好的名字等等。这也是我们应用简单设计后三条规则的地方:消除重复、保证表达力、最小化类和方法的数量。

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: 为了保持系统整洁,我们应该消除 scaleToOneDimensionrotate 方法之间少量的重复:

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)模式是移除高层级重复的常用技术。例如:

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. accrueUSDivisionVacationaccrueEuropeanDivisionVacation 中的代码大部分相同,除了计算法定最低标准的部分。算法的那一小部分根据雇员类型而变化。

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 算法中的“空缺”,提供了唯一不重复的信息片段。

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. 你还可以通过使用标准命名法来表达自己。例如,设计 patterns 很大程度上就是关于沟通和表达力的。通过在实现那些模式的类名中使用标准模式名称,如 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. 所以,为你的手艺感到一点自豪吧。在你的每个函数和类上花点时间。选择更好的名字,将大函数拆分成小函数,总之,用心照料你创造的东西。用心是一种宝贵的资源。

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. 我们的目标是在保持函数和类短小的同时,保持整个系统的精简。然而请记住,这条规则在简单设计的四条规则中优先级最低。所以,虽然保持类和函数数量少很重要,但拥有测试、消除重复和表达自己更为重要。

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. 是否有一套简单的实践可以替代经验?显然没有。另一方面,本章和本书中描述的实践是作者们数十年经验的结晶。遵循简单设计的实践,确实能够鼓励并使开发者坚持遵循良好的原则和模式,而这些原则和模式否则可能需要数年才能学会。

基于 MIT 许可发布