wip-6.3

2026-06-05 ⏳4.2分钟(1.7千字)

6.3 Understanding functional architecture

6.3 理解函数式架构

Some groundwork is needed before I can show how to make the transition. In this section, you’ll see what functional programming and functional architecture are and how the latter relates to the hexagonal architecture. Section 6.4 illustrates the transition using an example.

在展示如何进行这种转变之前,需要先打一些基础。本节中,你会看到什么是函数式编程和函数式架构,以及后者与六边形架构之间的关系。6.4 节会用一个示例说明这种转变。

Note that this isn’t a deep dive into the topic of functional programming, but rather an explanation of the basic principles behind it. These basic principles should be enough to understand the connection between functional programming and output-based testing. For a deeper look at functional programming, see Scott Wlaschin’s website and books at https://fsharpforfunandprofit.com/books.

请注意,这并不是对函数式编程主题的深入探讨,而是对其背后基本原则的解释。这些基本原则应该足以帮助你理解函数式编程与基于输出测试之间的联系。若想更深入了解函数式编程,可以参考 Scott Wlaschin 的网站和书籍:https://fsharpforfunandprofit.com/books。

6.3.1 What is functional programming?

6.3.1 什么是函数式编程?

As I mentioned in section 6.1.1, the output-based unit testing style is also known as functional. That’s because it requires the underlying production code to be written in a purely functional way, using functional programming. So, what is functional programming?

正如 6.1.1 节所说,基于输出的单元测试风格也称为函数式。这是因为它要求底层生产代码使用函数式编程,以纯函数方式编写。那么,什么是函数式编程?

Functional programming is programming with mathematical functions. A mathematical function (also known as pure function) is a function (or method) that doesn’t have any hidden inputs or outputs. All inputs and outputs of a mathematical function must be explicitly expressed in its method signature, which consists of the method’s name, arguments, and return type. A mathematical function produces the same output for a given input regardless of how many times it is called.

函数式编程就是用数学函数进行编程。数学函数(也称纯函数)是不包含任何隐藏输入或输出的函数(或方法)。数学函数的所有输入和输出都必须在方法签名中显式表达;方法签名由方法名、参数和返回类型组成。对于给定输入,数学函数无论被调用多少次,都会产生相同输出。

Let’s take the CalculateDiscount() method from listing 6.1 as an example (I’m copying it here for convenience):

让我们以清单 6.1 中的 CalculateDiscount() 方法为例(为方便起见,我把它复制在这里):

public decimal CalculateDiscount(Product[] products)
{
    decimal discount = products.Length * 0.01m;
    return Math.Min(discount, 0.2m);
}

This method has one input (a Product array) and one output (the decimal discount), both of which are explicitly expressed in the method’s signature. There are no hidden inputs or outputs. This makes CalculateDiscount() a mathematical function (figure 6.5).

这个方法有一个输入(Product 数组)和一个输出(decimal 类型的折扣),二者都在方法签名中显式表达。这里没有隐藏输入或输出。因此,CalculateDiscount() 是一个数学函数(图 6.5)。

Figure 6.5

Methods with no hidden inputs and outputs are called mathematical functions because such methods adhere to the definition of a function in mathematics.

没有隐藏输入和输出的方法称为数学函数,因为这类方法符合数学中函数的定义。

DEFINITION In mathematics, a function is a relationship between two sets that for each element in the first set, finds exactly one element in the second set.

定义 在数学中,函数是两个集合之间的一种关系:对于第一个集合中的每个元素,它都能在第二个集合中找到唯一一个元素。

Figure 6.6 shows how for each input number x, function f(x) = x + 1 finds a corresponding number y. Figure 6.7 displays the CalculateDiscount() method using the same notation as in figure 6.6.

图 6.6 展示了对于每个输入数字 x,函数 f(x) = x + 1 如何找到对应的数字 y。图 6.7 使用与图 6.6 相同的记法展示了 CalculateDiscount() 方法。

Figure 6.6
Figure 6.7

Explicit inputs and outputs make mathematical functions extremely testable because the resulting tests are short, simple, and easy to understand and maintain. Mathematical functions are the only type of methods where you can apply output-based testing, which has the best maintainability and the lowest chance of producing a false positive.

显式输入和输出让数学函数极易测试,因为相应测试短小、简单,并且易于理解和维护。数学函数是唯一可以应用基于输出测试的方法类型,而基于输出测试具有最佳可维护性和最低的假阳性概率。

On the other hand, hidden inputs and outputs make the code less testable (and less readable, too). Types of such hidden inputs and outputs include the following:

另一方面,隐藏输入和输出会让代码更难测试(也更难阅读)。这类隐藏输入和输出包括以下几种:

A good rule of thumb when determining whether a method is a mathematical function is to see if you can replace a call to that method with its return value without changing the program’s behavior. The ability to replace a method call with the corresponding value is known as referential transparency. Look at the following method, for example:

判断一个方法是否是数学函数的经验法则是:看你能否用它的返回值替换对它的调用,而不改变程序行为。用相应值替换方法调用的能力称为引用透明性。来看下面这个方法:

public int Increment(int x)
{
    return x + 1;
}

This method is a mathematical function. These two statements are equivalent to each other:

这个方法是数学函数。下面两条语句彼此等价:

int y = Increment(4);
int y = 5;

On the other hand, the following method is not a mathematical function. You can’t replace it with the return value because that return value doesn’t represent all of the method’s outputs. In this example, the hidden output is the change to field x (a side effect):

另一方面,下面这个方法不是数学函数。你不能用返回值替换它,因为返回值并不代表该方法的所有输出。在这个例子中,隐藏输出是字段 x 的变化(一个副作用):

int x = 0;
public int Increment()
{
    x++;
    return x;
}

Side effects are the most prevalent type of hidden outputs. The following listing shows an AddComment method that looks like a mathematical function on the surface but actually isn’t one. Figure 6.8 shows the method graphically.

副作用是最常见的隐藏输出类型。下面的清单展示了一个 AddComment 方法,它表面上看像数学函数,但实际上并不是。图 6.8 以图形方式展示了该方法。

Listing 6.7
Figure 6.8

6.3.2 What is functional architecture?

6.3.2 什么是函数式架构?

You can’t create an application that doesn’t incur any side effects whatsoever, of course. Such an application would be impractical. After all, side effects are what you create all applications for: updating the user’s information, adding a new order line to the shopping cart, and so on.

当然,你不可能创建一个完全没有任何副作用的应用。这样的应用是不切实际的。毕竟,副作用正是你创建应用的目的:更新用户信息、向购物车添加新订单行,等等。

The goal of functional programming is not to eliminate side effects altogether but rather to introduce a separation between code that handles business logic and code that incurs side effects. These two responsibilities are complex enough on their own; mixing them together multiplies the complexity and hinders code maintainability in the long run. This is where functional architecture comes into play. It separates business logic from side effects by pushing those side effects to the edges of a business operation.

函数式编程的目标并不是彻底消除副作用,而是在处理业务逻辑的代码与产生副作用的代码之间引入分离。这两种职责各自已经足够复杂;把它们混在一起会成倍增加复杂度,并从长期看阻碍代码可维护性。这就是函数式架构发挥作用的地方。它通过把副作用推到业务操作边缘,将业务逻辑与副作用分离。

DEFINITION Functional architecture maximizes the amount of code written in a purely functional (immutable) way, while minimizing code that deals with side effects. Immutable means unchangeable: once an object is created, its state can’t be modified. This is in contrast to a mutable object (changeable object), which can be modified after it is created.

定义 函数式架构会最大化以纯函数式(不可变)方式编写的代码量,同时最小化处理副作用的代码。不可变意味着不可更改:对象一旦创建,其状态就不能被修改。这与可变对象(可更改对象)相反,可变对象创建后仍可被修改。

The separation between business logic and side effects is done by segregating two types of code:

业务逻辑与副作用之间的分离,是通过隔离两类代码完成的:

The code that makes decisions is often referred to as a functional core (also known as an immutable core). The code that acts upon those decisions is a mutable shell (figure 6.9).

做出决策的代码通常称为函数式核心(也称不可变核心)。执行这些决策的代码则是可变壳(图 6.9)。

Figure 6.9

The functional core and the mutable shell cooperate in the following way:

函数式核心与可变壳按以下方式协作:

To maintain a proper separation between these two layers, you need to make sure the classes representing the decisions contain enough information for the mutable shell to act upon them without additional decision-making. In other words, the mutable shell should be as dumb as possible. The goal is to cover the functional core extensively with output-based tests and leave the mutable shell to a much smaller number of integration tests.

为了在这两层之间保持恰当分离,你需要确保表示决策的类包含足够信息,让可变壳无需额外决策即可执行它们。换句话说,可变壳应该尽可能“笨”。目标是用基于输出的测试充分覆盖函数式核心,而把可变壳交给数量少得多的集成测试。

Encapsulation and immutability
Like encapsulation, functional architecture (in general) and immutability (in particular) serve the same goal as unit testing: enabling sustainable growth of your software project. In fact, there’s a deep connection between the concepts of encapsulation and immutability.

封装与不可变性 与封装一样,函数式架构(总体上)和不可变性(特别是)服务于与单元测试相同的目标:让软件项目能够可持续增长。事实上,封装和不可变性这两个概念之间有很深的联系。

As you may remember from chapter 5, encapsulation is the act of protecting your code against inconsistencies. Encapsulation safeguards the class’s internals from corruption by

你可能还记得第 5 章,封装是保护代码免受不一致影响的行为。封装通过以下方式保护类的内部不被破坏:

Immutability tackles this issue of preserving invariants from another angle. With immutable classes, you don’t need to worry about state corruption because it’s impossible to corrupt something that cannot be changed in the first place. As a consequence, there’s no need for encapsulation in functional programming. You only need to validate the class’s state once, when you create an instance of it. After that, you can freely pass this instance around. When all your data is immutable, the whole set of issues related to the lack of encapsulation simply vanishes.

不可变性从另一个角度处理保持不变量的问题。使用不可变类时,你无需担心状态损坏,因为无法改变的东西一开始就不可能被损坏。因此,在函数式编程中不需要封装。你只需要在创建类实例时验证一次状态。之后,就可以自由传递这个实例。当所有数据都是不可变的,与缺乏封装相关的一整套问题就会直接消失。

There’s a great quote from Michael Feathers in that regard:

Michael Feathers 在这方面有一句很棒的话:

Object-oriented programming makes code understandable by encapsulating moving parts. Functional programming makes code understandable by minimizing moving parts.

面向对象编程通过封装运动部件让代码易于理解。函数式编程通过最小化运动部件让代码易于理解。

6.3.3 Comparing functional and hexagonal architectures

6.3.3 比较函数式架构与六边形架构

There are a lot of similarities between functional and hexagonal architectures. Both of them are built around the idea of separation of concerns. The details of that separation vary, though.

函数式架构与六边形架构之间有很多相似之处。二者都围绕关注点分离这一理念构建。不过,分离的细节有所不同。

As you may remember from chapter 5, the hexagonal architecture differentiates the domain layer and the application services layer (figure 6.10). The domain layer is accountable for business logic while the application services layer, for communication with external applications such as a database or an SMTP service. This is very similar to functional architecture, where you introduce the separation of decisions and actions.

你可能还记得第 5 章,六边形架构区分领域层和应用服务层(图 6.10)。领域层负责业务逻辑,而应用服务层负责与数据库或 SMTP 服务等外部应用通信。这与函数式架构非常相似;在函数式架构中,你引入的是决策与动作的分离。

Figure 6.10

Another similarity is the one-way flow of dependencies. In the hexagonal architecture, classes inside the domain layer should only depend on each other; they should not depend on classes from the application services layer. Likewise, the immutable core in functional architecture doesn’t depend on the mutable shell. It’s self-sufficient and can work in isolation from the outer layers. This is what makes functional architecture so testable: you can strip the immutable core from the mutable shell entirely and simulate the inputs that the shell provides using simple values.

另一个相似点是依赖的单向流动。在六边形架构中,领域层内部的类应该只彼此依赖;它们不应该依赖应用服务层的类。同样,函数式架构中的不可变核心也不依赖可变壳。它是自给自足的,可以脱离外层独立工作。这正是函数式架构如此易于测试的原因:你可以把不可变核心完全从可变壳中剥离出来,并用简单值模拟壳所提供的输入。

The difference between the two is in their treatment of side effects. Functional architecture pushes all side effects out of the immutable core to the edges of a business operation. These edges are handled by the mutable shell. On the other hand, the hexagonal architecture is fine with side effects made by the domain layer, as long as they are limited to that domain layer only. All modifications in hexagonal architecture should be contained within the domain layer and not cross that layer’s boundary. For example, a domain class instance can’t persist something to the database directly, but it can change its own state. An application service will then pick up this change and apply it to the database.

二者的区别在于对副作用的处理方式。函数式架构会把所有副作用从不可变核心推出去,推到业务操作的边缘。这些边缘由可变壳处理。另一方面,六边形架构允许领域层产生副作用,只要这些副作用仅限于领域层内部。六边形架构中的所有修改都应该被限制在领域层内,而不能跨越该层边界。例如,领域类实例不能直接把某些内容持久化到数据库,但它可以改变自身状态。随后,应用服务会获取这一变化并将其应用到数据库。

NOTE Functional architecture is a subset of the hexagonal architecture. You can view functional architecture as the hexagonal architecture taken to an extreme.

注意 函数式架构是六边形架构的一个子集。你可以把函数式架构看作是走向极致的六边形架构。