wip-7.5

2026-06-05 ⏳2.1分钟(0.8千字)

7.5 Conclusion

7.5 结论

Notice a theme that has been present throughout this chapter: abstracting away the application of side effects to external systems. You achieve such abstraction by keeping those side effects in memory until the very end of the business operation, so that they can be tested with plain unit tests without involving out-of-process dependencies. Domain events are abstractions on top of upcoming messages in the bus. Changes in domain classes are abstractions on top of upcoming modifications in the database.

注意本章贯穿始终的一个主题:把作用于外部系统的副作用抽象出来。实现这种抽象的方式,是把这些副作用保存在内存中,直到业务操作的最后阶段;这样就可以使用普通单元测试来测试它们,而不需要涉及进程外依赖。领域事件是对即将发送到消息总线上的消息的抽象。领域类中的变化是对即将写入数据库的修改的抽象。

NOTE It’s easier to test abstractions than the things they abstract.

注意 测试抽象,比测试抽象所代表的具体事物更容易。

Although we were able to successfully contain all the decision-making in the domain model with the help of domain events and the CanExecute/Execute pattern, you won’t be able to always do that. There are situations where business logic fragmentation is inevitable.

虽然借助领域事件和 CanExecute/Execute 模式,我们成功地把所有决策都限制在领域模型中,但你不可能总是做到这一点。有些情况下,业务逻辑碎片化是不可避免的。

For example, there’s no way to verify email uniqueness outside the controller without introducing out-of-process dependencies in the domain model. Another example is failures in out-of-process dependencies that should alter the course of the business operation. The decision about which way to go can’t reside in the domain layer because it’s not the domain layer that calls those out-of-process dependencies. You will have to put this logic into controllers and then cover it with integration tests. Still, even with the potential fragmentation, there’s a lot of value in separating business logic from orchestration because this separation drastically simplifies the unit testing process.

例如,如果不在领域模型中引入进程外依赖,就无法在控制器之外验证邮箱唯一性。另一个例子是进程外依赖发生故障,并且这种故障应该改变业务操作的流程。关于下一步该怎么走的决策不能放在领域层,因为调用那些进程外依赖的并不是领域层。你必须把这类逻辑放进控制器,然后用集成测试覆盖它。不过,即使可能存在碎片化,把业务逻辑与编排分离仍然很有价值,因为这种分离会极大简化单元测试过程。

Just as you can’t avoid having some business logic in controllers, you will rarely be able to remove all collaborators from domain classes. And that’s fine. One, two, or even three collaborators won’t turn a domain class into overcomplicated code, as long as these collaborators don’t refer to out-of-process dependencies.

正如你无法避免控制器中存在一些业务逻辑一样,你也很少能够从领域类中移除所有协作者。这没关系。只要这些协作者不指向进程外依赖,一个、两个,甚至三个协作者都不会让领域类变成过度复杂代码。

Don’t use mocks to verify interactions with such collaborators, though. These interactions have nothing to do with the domain model’s observable behavior. Only the very first call, which goes from a controller to a domain class, has an immediate connection to that controller’s goal. All the subsequent calls the domain class makes to its neighbor domain classes within the same operation are implementation details.

不过,不要使用 mock 来验证与这类协作者的交互。这些交互与领域模型的可观察行为无关。只有从控制器到领域类的第一通调用,与该控制器的目标有直接联系。领域类在同一个操作中随后对相邻领域类发起的所有调用,都是实现细节。

Figure 7.13 illustrates this idea. It shows the communications between components in the CRM and their relationship to observable behavior. As you may remember from chapter 5, whether a method is part of the class’s observable behavior depends on whom the client is and what the goals of that client are. To be part of the observable behavior, the method must meet one of the following two criteria:

图 7.13 说明了这个想法。它展示了 CRM 中组件之间的通信,以及这些通信与可观察行为之间的关系。你可能还记得第 5 章,一个方法是否属于某个类的可观察行为,取决于客户端是谁,以及该客户端的目标是什么。要成为可观察行为的一部分,该方法必须满足以下两个条件之一:

The controller’s ChangeEmail() method is part of its observable behavior, and so is the call it makes to the message bus. The first method is the entry point for the external client, thereby meeting the first criterion. The call to the bus sends messages to external applications, thereby meeting the second criterion. You should verify both of these method calls (which is the topic of the next chapter). However, the subsequent call from the controller to User doesn’t have an immediate connection to the goals of the external client. That client doesn’t care how the controller decides to implement the change of email as long as the final state of the system is correct and the call to the message bus is in place. Therefore, you shouldn’t verify calls the controller makes to User when testing that controller’s behavior.

控制器的 ChangeEmail() 方法是其可观察行为的一部分,它对消息总线的调用也是如此。第一个方法是外部客户端的入口点,因此满足第一个条件。对总线的调用会向外部应用发送消息,因此满足第二个条件。你应该验证这两个方法调用(这是下一章的主题)。然而,控制器随后对 User 的调用,与外部客户端的目标没有直接联系。只要系统的最终状态正确,并且对消息总线的调用到位,客户端并不关心控制器如何决定实现邮箱修改。因此,在测试控制器行为时,你不应该验证控制器对 User 发起的调用。

Figure 7.13

When you step one level down the call stack, you get a similar situation. Now it’s the controller who is the client, and the ChangeEmail method in User has an immediate connection to that client’s goal of changing the user email and thus should be tested. But the subsequent calls from User to Company are implementation details from the controller’s point of view. Therefore, the test that covers the ChangeEmail method in User shouldn’t verify what methods User calls on Company. The same line of reasoning applies when you step one more level down and test the two methods in Company from User’s point of view.

当你沿调用栈向下一层时,会得到类似情况。现在客户端变成了控制器,而 User 中的 ChangeEmail 方法与该客户端修改用户邮箱的目标有直接联系,因此应该被测试。但从控制器的角度看,User 随后对 Company 的调用是实现细节。因此,覆盖 User 中 ChangeEmail 方法的测试,不应该验证 User 调用了 Company 上的哪些方法。当你再向下一层,从 User 的角度测试 Company 中两个方法时,同样的推理也适用。

Think of the observable behavior and implementation details as onion layers. Test each layer from the outer layer’s point of view, and disregard how that layer talks to the underlying layers. As you peel these layers one by one, you switch perspective: what previously was an implementation detail now becomes an observable behavior, which you then cover with another set of tests.

可以把可观察行为和实现细节想象成洋葱层。从外层的角度测试每一层,并忽略这一层如何与更底层通信。当你一层一层剥开这些层时,你会切换视角:之前是实现细节的东西,现在会变成可观察行为,然后你再用另一组测试覆盖它。