wip-7.1

2026-06-05 ⏳4.6分钟(1.9千字)

7.1 Identifying the code to refactor

7.1 识别需要重构的代码

It’s rarely possible to significantly improve a test suite without refactoring the underlying code. There’s no way around it—test and production code are intrinsically connected. In this section, you’ll see how to categorize your code into the four types in order to outline the direction of the refactoring. The subsequent sections show a comprehensive example.

如果不重构底层代码,通常很难显著改进测试套件。这一点无法绕开——测试代码和生产代码本质上是相互连接的。本节中,你会看到如何把代码划分为四种类型,从而勾勒重构方向。后续小节会展示一个完整示例。

7.1.1 The four types of code

7.1.1 四种代码类型

In this section, I describe the four types of code that serve as a foundation for the rest of this chapter.

本节会描述四种代码类型,它们构成本章剩余内容的基础。

All production code can be categorized along two dimensions:

所有生产代码都可以沿两个维度分类:

Code complexity is defined by the number of decision-making (branching) points in the code. The greater that number, the higher the complexity.

代码复杂度由代码中决策(分支)点的数量定义。这个数量越大,复杂度越高。

How to calculate cyclomatic complexity
In computer science, there’s a special term that describes code complexity: cyclomatic complexity. Cyclomatic complexity indicates the number of branches in a given program or method. This metric is calculated as

如何计算圈复杂度 在计算机科学中,有一个描述代码复杂度的专门术语:圈复杂度。圈复杂度表示给定程序或方法中的分支数量。该指标计算如下:

1 + <number of branching points>

Thus, a method with no control flow statements (such as if statements or conditional loops) has a cyclomatic complexity of 1 + 0 = 1.

因此,一个没有控制流语句(例如 if 语句或条件循环)的方法,其圈复杂度为 1 + 0 = 1

There’s another meaning to this metric. You can think of it in terms of the number of independent paths through the method from an entry to an exit, or the number of tests needed to get a 100% branch coverage.

这个指标还有另一层含义。你可以把它理解为从方法入口到出口的独立路径数量,或者达到 100% 分支覆盖率所需的测试数量。

Note that the number of branching points is counted as the number of simplest predicates involved. For instance, a statement like IF condition1 AND condition2 THEN ... is equivalent to IF condition1 THEN IF condition2 THEN ... Therefore, its complexity would be 1 + 2 = 3.

注意,分支点数量按其中涉及的最简单谓词数量计算。例如,IF condition1 AND condition2 THEN ... 等价于 IF condition1 THEN IF condition2 THEN ...。因此,它的复杂度是 1 + 2 = 3

Domain significance shows how significant the code is for the problem domain of your project. Normally, all code in the domain layer has a direct connection to the end users’ goals and thus exhibits a high domain significance. On the other hand, utility code doesn’t have such a connection.

领域重要性表示代码对项目问题领域的重要程度。通常,领域层中的所有代码都与最终用户目标有直接联系,因此具有较高领域重要性。另一方面,工具代码没有这样的联系。

Complex code and code that has domain significance benefit from unit testing the most because the corresponding tests have great protection against regressions. Note that the domain code doesn’t have to be complex, and complex code doesn’t have to exhibit domain significance to be test-worthy. The two components are independent of each other. For example, a method calculating an order price can contain no conditional statements and thus have the cyclomatic complexity of 1. Still, it’s important to test such a method because it represents business-critical functionality.

复杂代码和具有领域重要性的代码最能从单元测试中获益,因为相应测试具有很强的防止回归能力。注意,领域代码不一定复杂,复杂代码也不一定具有领域重要性才值得测试。这两个组成部分彼此独立。例如,一个计算订单价格的方法可以不包含任何条件语句,因此圈复杂度为 1。尽管如此,测试这个方法仍然很重要,因为它代表业务关键功能。

The second dimension is the number of collaborators a class or a method has. As you may remember from chapter 2, a collaborator is a dependency that is either mutable or out-of-process (or both). Code with a large number of collaborators is expensive to test. That’s due to the maintainability metric, which depends on the size of the test. It takes space to bring collaborators to an expected condition and then check their state or interactions with them afterward. And the more collaborators there are, the larger the test becomes.

第二个维度是类或方法拥有的协作者数量。你可能还记得第 2 章,协作者是可变依赖、进程外依赖,或二者兼具。拥有大量协作者的代码测试成本高。这与可维护性指标有关,而可维护性取决于测试大小。把协作者置于预期状态,然后检查它们的状态或与它们的交互,都需要占用空间。协作者越多,测试越大。

The type of the collaborators also matters. Out-of-process collaborators are a no-go when it comes to the domain model. They add additional maintenance costs due to the necessity to maintain complicated mock machinery in tests. You also have to be extra prudent and only use mocks to verify interactions that cross the application boundary in order to maintain proper resistance to refactoring (refer to chapter 5 for more details). It’s better to delegate all communications with out-of-process dependencies to classes outside the domain layer. The domain classes then will only work with in-process dependencies.

协作者的类型也很重要。对于领域模型来说,进程外协作者是禁区。它们会增加额外维护成本,因为测试中必须维护复杂的 mock 机制。你还必须格外谨慎,只使用 mock 验证跨越应用边界的交互,才能维持适当的抗重构能力(更多细节见第 5 章)。最好把所有与进程外依赖的通信委托给领域层之外的类。这样领域类就只会与进程内依赖协作。

Notice that both implicit and explicit collaborators count toward this number. It doesn’t matter if the system under test (SUT) accepts a collaborator as an argument or refers to it implicitly via a static method, you still have to set up this collaborator in tests. Conversely, immutable dependencies (values or value objects) don’t count. Such dependencies are much easier to set up and assert against.

注意,隐式和显式协作者都会计入这个数量。无论被测系统(SUT)是把协作者作为参数接收,还是通过静态方法隐式引用它,你在测试中都仍然必须设置这个协作者。相反,不可变依赖(值或值对象)不计入其中。这类依赖更容易设置和断言。

The combination of code complexity, its domain significance, and the number of collaborators give us the four types of code shown in figure 7.1:

代码复杂度、领域重要性和协作者数量的组合,给出了图 7.1 所示的四种代码类型:

Figure 7.1

Unit testing the top-left quadrant (domain model and algorithms) gives you the best return for your efforts. The resulting unit tests are highly valuable and cheap. They’re valuable because the underlying code carries out complex or important logic, thus increasing tests’ protection against regressions. And they’re cheap because the code has few collaborators (ideally, none), thus decreasing tests’ maintenance costs.

对左上象限(领域模型和算法)进行单元测试,能让你的投入获得最佳回报。由此得到的单元测试价值高且成本低。它们有价值,是因为底层代码执行复杂或重要逻辑,从而提升测试的防止回归能力。它们成本低,是因为代码协作者很少(理想情况下没有),从而降低测试维护成本。

Trivial code shouldn’t be tested at all; such tests have a close-to-zero value. As for controllers, you should test them briefly as part of a much smaller set of the overarching integration tests (I cover this topic in part 3).

平凡代码完全不应该测试;这类测试价值接近于零。至于控制器,你应该把它们作为规模小得多的总体集成测试的一部分进行简要测试(我会在第 3 部分讨论这个主题)。

The most problematic type of code is the overcomplicated quadrant. It’s hard to unit test but too risky to leave without test coverage. Such code is one of the main reasons many people struggle with unit testing. This whole chapter is primarily devoted to how you can bypass this dilemma. The general idea is to split overcomplicated code into two parts: algorithms and controllers (figure 7.2), although the actual implementation can be tricky at times.

最有问题的代码类型是过度复杂象限。它很难单元测试,但不覆盖又太冒险。这类代码是许多人在单元测试上挣扎的主要原因之一。本章主要讨论如何绕过这个困境。总体思路是把过度复杂代码拆成两部分:算法和控制器(图 7.2),尽管实际实现有时会比较棘手。

Figure 7.2

TIP The more important or complex the code, the fewer collaborators it should have.

提示 代码越重要或越复杂,它拥有的协作者就应该越少。

Getting rid of the overcomplicated code and unit testing only the domain model and algorithms is the path to a highly valuable, easily maintainable test suite. With this approach, you won’t have 100% test coverage, but you don’t need to—100% coverage shouldn’t ever be your goal. Your goal is a test suite where each test adds significant value to the project. Refactor or get rid of all other tests. Don’t allow them to inflate the size of your test suite.

消除过度复杂代码,并且只对领域模型和算法做单元测试,是通向高价值、易维护测试套件的路径。使用这种方法,你不会拥有 100% 测试覆盖率,但你也不需要——100% 覆盖率永远不应该是你的目标。你的目标应该是一个每个测试都能为项目增加显著价值的测试套件。重构或删除所有其他测试。不要让它们膨胀测试套件的规模。

NOTE Remember that it’s better to not write a test at all than to write a bad test.

注意 记住,不写测试也比写一个糟糕的测试更好。

Of course, getting rid of overcomplicated code is easier said than done. Still, there are techniques that can help you do that. I’ll first explain the theory behind those techniques and then demonstrate them using a close-to-real-world example.

当然,消除过度复杂代码说起来容易做起来难。不过,仍然有一些技术可以帮助你做到这一点。我会先解释这些技术背后的理论,然后用一个接近真实世界的示例演示它们。

7.1.2 Using the Humble Object pattern to split overcomplicated code

7.1.2 使用 Humble Object 模式拆分过度复杂代码

To split overcomplicated code, you need to use the Humble Object design pattern. This pattern was introduced by Gerard Meszaros in his book xUnit Test Patterns: Refactoring Test Code (Addison-Wesley, 2007) as one of the ways to battle code coupling, but it has a much broader application. You’ll see why shortly.

要拆分过度复杂代码,你需要使用 Humble Object 设计模式。Gerard Meszaros 在《xUnit Test Patterns: Refactoring Test Code》(Addison-Wesley,2007)一书中介绍了这一模式,作为对抗代码耦合的一种方式,但它的适用范围要广得多。你很快就会看到原因。

We often find that code is hard to test because it’s coupled to a framework dependency (see figure 7.3). Examples include asynchronous or multi-threaded execution, user interfaces, communication with out-of-process dependencies, and so on.

我们经常发现,代码之所以难以测试,是因为它耦合到了框架依赖(见图 7.3)。例子包括异步或多线程执行、用户界面、与进程外依赖通信,等等。

Figure 7.3

To bring the logic of this code under test, you need to extract a testable part out of it. As a result, the code becomes a thin, humble wrapper around that testable part: it glues the hard-to-test dependency and the newly extracted component together, but itself contains little or no logic and thus doesn’t need to be tested (figure 7.4).

为了测试这段代码中的逻辑,你需要从中提取出可测试部分。结果是,原代码变成围绕该可测试部分的一层薄薄的、谦逊的包装器:它把难以测试的依赖与新提取出的组件粘合在一起,但自身包含很少逻辑或不包含逻辑,因此不需要被测试(图 7.4)。

Figure 7.4

If this approach looks familiar, it’s because you already saw it in this book. In fact, both hexagonal and functional architectures implement this exact pattern. As you may remember from previous chapters, hexagonal architecture advocates for the separation of business logic and communications with out-of-process dependencies. This is what the domain and application services layers are responsible for, respectively.

如果这种方法看起来很熟悉,那是因为你已经在本书中见过它。事实上,六边形架构和函数式架构都实现了这个模式。你可能还记得前几章,六边形架构主张分离业务逻辑与进程外依赖通信。领域层和应用服务层分别负责这两件事。

Functional architecture goes even further and separates business logic from communications with all collaborators, not just out-of-process ones. This is what makes functional architecture so testable: its functional core has no collaborators. All dependencies in a functional core are immutable, which brings it very close to the vertical axis on the types-of-code diagram (figure 7.5).

函数式架构走得更远,它把业务逻辑与所有协作者的通信分离,而不仅仅是与进程外协作者分离。这正是函数式架构如此易于测试的原因:它的函数式核心没有协作者。函数式核心中的所有依赖都是不可变的,这让它在代码类型图上非常接近纵轴(图 7.5)。

Figure 7.5

Another way to view the Humble Object pattern is as a means to adhere to the Single Responsibility principle, which states that each class should have only a single responsibility. One such responsibility is always business logic; the pattern can be applied to segregate that logic from pretty much anything.

也可以把 Humble Object 模式看作遵循单一职责原则的一种手段;该原则指出每个类都应该只有一个职责。其中一种职责始终是业务逻辑;这个模式可以用于把业务逻辑几乎从任何东西中隔离出来。

In our particular situation, we are interested in the separation of business logic and orchestration. You can think of these two responsibilities in terms of code depth versus code width. Your code can be either deep (complex or important) or wide (work with many collaborators), but never both (figure 7.6).

在我们当前的情境中,我们关注的是业务逻辑与编排的分离。你可以用代码深度和代码宽度来理解这两种职责。你的代码可以是深的(复杂或重要),也可以是宽的(与许多协作者协作),但不能二者兼具(图 7.6)。

Figure 7.6

I can’t stress enough how important this separation is. In fact, many well-known principles and patterns can be described as a form of the Humble Object pattern: they are designed specifically to segregate complex code from the code that does orchestration.

我再怎么强调这种分离的重要性都不为过。事实上,许多知名原则和模式都可以描述为 Humble Object 模式的一种形式:它们专门用于把复杂代码与负责编排的代码分离。

You already saw the relationship between this pattern and hexagonal and functional architectures. Other examples include the Model-View-Presenter (MVP) and the Model-View-Controller (MVC) patterns. These two patterns help you decouple business logic (the Model part), UI concerns (the View), and the coordination between them (Presenter or Controller). The Presenter and Controller components are humble objects: they glue the view and the model together.

你已经看到这个模式与六边形架构和函数式架构之间的关系。其他例子包括 Model-View-Presenter(MVP)和 Model-View-Controller(MVC)模式。这两个模式帮助你解耦业务逻辑(Model 部分)、UI 关注点(View)以及二者之间的协调(Presenter 或 Controller)。Presenter 和 Controller 组件就是 humble object:它们把视图和模型粘合在一起。

Another example is the Aggregate pattern from Domain-Driven Design. One of its goals is to reduce connectivity between classes by grouping them into clusters—aggregates. The classes are highly connected inside those clusters, but the clusters themselves are loosely coupled. Such a structure decreases the total number of communications in the code base. The reduced connectivity, in turn, improves testability.

另一个例子是领域驱动设计中的聚合模式。它的目标之一是通过把类分组为簇——聚合——来降低类之间的连接性。在这些簇内部,类高度连接,但簇本身是松耦合的。这种结构会减少代码库中的通信总量。连接性降低反过来会提升可测试性。

Note that improved testability is not the only reason to maintain the separation between business logic and orchestration. Such a separation also helps tackle code complexity, which is crucial for project growth, too, especially in the long run. I personally always find it fascinating how a testable design is not only testable but also easy to maintain.

注意,提升可测试性并不是保持业务逻辑与编排分离的唯一原因。这种分离也有助于处理代码复杂度,而这对项目增长同样关键,尤其从长期看更是如此。我个人一直觉得很有意思:可测试的设计不仅可测试,也容易维护。