Chapter 5, Introducing Refactoring Principles
第 5 章 , 重构原则介绍
The pillars of safe change
安全变革的支柱
It is now time to implement some solutions for the problems exposed in the previous paragraphs. As discussed, modularizing a monolithic system is an essential first step toward achieving a more flexible and maintainable architecture. Refactoring within this context requires a careful approach that aligns with the principles of modularity and incremental change. Our goal is to decouple tightly interwoven components so that they can evolve independently, reducing the risk of cascading changes and enhancing the system’s overall stability.
现在是时候为前几段中暴露的问题实施一些解决方案了。如前所述,模块化单片系统是实现更灵活、更可维护的架构的重要第一步。在这种情况下进行重构需要采取符合模块化和增量变更原则的谨慎方法。我们的目标是将紧密交织的组件解耦,以便它们能够独立演变,从而降低级联变化的风险并增强系统的整体稳定性。
Exploring the solution structure
探索解决方案结构
Let’s start by exploring how our project was initially developed by looking at the solution structure inside Visual Studio in Figure 5.3.
让我们首先通过查看图 5.3 中的 Visual Studio 中的解决方案结构来探索我们的项目最初是如何开发的。

Figure 5.3 – The initial solution structure
图 5.3 – 初始解决方案结构
As you can see, there is a little bit of DDD in it. As you should have already spotted, there is lots of coupling because everything is inside single folders of specific common projects. For example, BrewUp.DomainModel contains a folder named Services with both sales and warehouse services.
如您所见,其中有一点 DDD。正如您应该已经发现的那样,有很多耦合,因为所有内容都在特定常见项目的单个文件夹中。例如,BrewUp.DomainModel 包含一个名为 Services 的文件夹,其中包含销售和仓库服务。
We know that we could do so much better with a bit of moving around, but before we dive deep into it and discover too late that we broke something, let’s try to apply the design and architectural principles introduced in previous chapters at the code level.
我们知道,只要稍微移动一下,我们就可以做得更好,但在我们深入研究并发现我们破坏某些东西为时已晚之前,让我们尝试在代码级别应用前几章中介绍的设计和架构原则。
You can apply these principles by examining SalesService in BrewUp.Rest.Services. You can immediately see that SalesOrderService is tightly coupled with WarehouseService:
您可以通过检查 BrewUp.Rest.Services 中的 SalesService 来应用这些原则。您可以立即看到 SalesOrderService 与 WarehouseService 紧密耦合:
public sealed class SalesOrderService(
[FromKeyedServices("sale")] IRepository saleRepository,
[FromKeyedServices("warehouse")] IRepository warehouseRepository) : ISalesOrderService
{
public async Task CreateSalesOrderAsync(SalesOrderId salesOrderId, SalesOrderNumber salesOrderNumber, OrderDate orderDate,
CustomerId customerId, CustomerName customerName, IEnumerable<SalesOrderRowJson> rows, CancellationToken cancellationToken)
{
List<SalesOrderRowJson> beersAvailable = new();
foreach (var row in rows)
{
var availability = await warehouseRepository.GetByIdAsync<Entities.Warehouses.Availability>(row.BeerId.ToString(), cancellationToken);
if (availability != null)
beersAvailable.Add(row);
}
var aggregate = SalesOrder.CreateSalesOrder(salesOrderId, salesOrderNumber, orderDate, customerId, customerName, beersAvailable);
await saleRepository.InsertAsync(aggregate.MapToReadModel(), cancellationToken);
}
}
Here, SalesOrderService is not only aware of the existence of WarehouseService but is also directly responsible for managing interactions with it. This tight coupling creates a situation where any changes to WarehouseService—even changes as minor as altering the method signature—could have ripple effects throughout SalesOrderService, thus violating the principles of modularity (remember the impact of changes as shown in Figure 5.1).
在这里,SalesOrderService 不仅知道 WarehouseService 的存在,还直接负责管理与它的交互。这种紧密耦合创造了一种情况,即对 WarehouseService 的任何更改(即使是像更改方法签名这样微小的更改)都可能在整个 SalesOrderService 中产生连锁反应,从而违反模块化原则(请记住更改的影响,如图 5.1 所示)。
These services are bound together, making it difficult to modify one without affecting the other. To move toward a modular architecture, we need to break this dependency, enabling each service to operate independently and making it easier to adapt the system incrementally.
这些服务绑定在一起,因此很难在不影响另一个服务的情况下修改一个服务。为了走向模块化架构,我们需要打破这种依赖关系,使每个服务能够独立运行,并更容易逐步调整系统。
However, before tackling the refactoring process head-on, it’s crucial to understand the role that tests play in ensuring a smooth and safe process. Refactoring, by its very nature, involves altering the internal structure of your code without changing its external behavior. However, this can be risky if not done with due care. This is where the concept of a safety net of tests comes into play.
然而,在正面解决重构过程之前,了解测试在确保过程顺利和安全方面所发挥的作用至关重要。重构,就其本质而言,涉及在不改变其外部行为的情况下更改代码的内部结构。但是,如果不小心谨慎,这可能会有风险。这就是测试安全网的概念发挥作用的地方。
Establishing comprehensive tests ensures that our changes do not inadvertently break existing functionality.
建立全面的测试可确保我们的更改不会无意中破坏现有功能。
Understanding tests and their role in refactoring
了解测试及其在重构中的作用
At its core, a test is a piece of code that checks whether another piece of code (the system under test) behaves as expected. When you refactor an application, you inevitably change the code base, sometimes significantly. Without a robust set of tests, you have no reliable way to confirm that your changes haven’t introduced new bugs or altered the system’s behavior unintentionally.
从本质上讲,测试是一段代码,用于检查另一段代码(被测系统)是否按预期运行。重构应用程序时,不可避免地会更改代码库,有时甚至会发生重大变化。如果没有一组强大的测试,您就无法可靠地确认您的更改没有引入新的错误或无意中改变了系统的行为。
Tests act as your first line of defense. They give you the confidence to make changes because you know that if something goes wrong, the tests will catch it. This is particularly important in a DDD context, where the alignment between the business domain and the code is critical. Any misalignment introduced during refactoring could have significant consequences on the overall design and functionality of the application.
测试是您的第一道防线。它们让您有信心做出改变,因为您知道如果出现问题,测试会发现它。这在 DDD 上下文中尤为重要,因为业务域和代码之间的一致性至关重要。重构过程中引入的任何错位都可能对应用程序的整体设计和功能产生重大影响。
Before reviewing the specific types of tests, it’s essential to grasp the variety available and how each serves a unique purpose in maintaining your application’s health.
在查看特定类型的测试之前,必须了解可用的测试种类以及每种测试如何在维护应用程序健康方面发挥独特作用。
In Figure 5.4, you can see the famous pyramid of tests, a concept that visualizes the optimal distribution of different types of tests in your code base.
在图 5.4 中,您可以看到著名的测试金字塔 ,这个概念可视化了代码库中不同类型测试的最佳分布。

Figure 5.4 – The pyramid of tests
图 5.4 – 测试金字塔
As you can see, the pyramid categorizes tests into three different layers: Unit tests, Integration tests, and E2E tests. Let’s discuss each of these and acceptance tests next:
如您所见,金字塔将测试分为三个不同的层:单元测试、集成测试和 E2E 测试。接下来让我们讨论其中的每一个和验收测试:
-
Unit tests: Unit tests lie at the base of the pyramid and focus on individual units of code, typically functions or methods, verifying that they perform as expected in isolation. Unit tests are the fastest to run and are usually abundant in well-tested code bases. Their speed and granularity make them ideal for covering the smallest units of your application. They provide the foundation of your safety net and allow you to refactor with the confidence that the fundamental building blocks of your application will continue to function correctly.
单元测试 :单元测试位于金字塔的底部,侧重于单个代码单元,通常是函数或方法,验证它们是否按预期独立运行。单元测试运行速度最快,通常在经过充分测试的代码库中大量存在。它们的速度和粒度使其成为覆盖应用中最小单元的理想选择。它们为您的安全网奠定了基础,并允许您确信应用程序的基本构建块将继续正常运行而重构。
-
Integration tests: In the middle layer, you have integration tests. They check how different modules or services work together. Unlike unit tests, they don’t isolate individual pieces of code but instead focus on the interactions between them. They are fewer in number and slower than unit tests but crucial for ensuring that the various parts of your system interact correctly. When refactoring, integration tests ensure that changes in one module don’t break its interactions with others. They provide a broader safety net, catching issues that unit tests might miss.
集成测试 :在中间层,您有集成测试。他们检查不同的模块或服务如何协同工作。与单元测试不同,它们不隔离单个代码片段,而是关注它们之间的交互。它们比单元测试数量更少且速度更慢,但对于确保系统的各个部分正确交互至关重要。重构时,集成测试可确保一个模块中的更改不会破坏其与其他模块的交互。它们提供了更广泛的安全网,捕获单元测试可能遗漏的问题。
-
E2E tests: At the top layer lie E2E tests that simulate real user scenarios, testing the entire application from the user interface down to the database. These are the least numerous but the most comprehensive, testing the entire system as a user would interact with. While they are slower and more expensive to maintain, their ability to validate complete user flows makes them indispensable. When refactoring, E2E tests assure you that the application’s core functionalities remain intact.
E2E 测试: 顶层是模拟真实用户场景的 E2E 测试,测试从用户界面到数据库的整个应用程序。这些是数量最少但最全面的,在用户交互时测试整个系统。虽然它们速度较慢且维护成本更高,但它们验证完整用户流的能力使它们不可或缺。重构时,E2E 测试可确保应用程序的核心功能保持不变。
-
Acceptance tests: There is also a fourth layer not represented in the original pyramid that tends to overlap with the E2E tests. That layer is made up of acceptance tests. These tests are often written from the perspective of the end user or the business. They validate that the system meets the specified requirements. As already stated, they overlap with E2E tests but are usually more focused on business outcomes than on technical correctness.
验收测试 :还有第四层在原始金字塔中没有表示,往往与 E2E 测试重叠。该层由验收测试组成。这些测试通常是从最终用户或业务的角度编写的。它们验证系统是否满足指定的要求。如前所述,它们与 E2E 测试重叠,但通常更关注业务成果而不是技术正确性。
Applying the Testing Pyramid: A complete example
应用测试金字塔:一个完整的示例
Each layer of the testing pyramid has its strengths and weaknesses. Unit tests, for example, are fast and easy to write, but they only cover individual components. If you refactor a significant portion of your code, unit tests alone might not be enough to guarantee that everything still works as intended.
测试金字塔的每一层都有其优点和缺点。例如,单元测试编写快速且易于编写,但它们仅涵盖单个组件。如果重构了很大一部分代码,则仅靠单元测试可能不足以保证一切仍按预期工作。
Integration tests, while slower and more complex, give you confidence that different parts of your system interact correctly. However, they can be more challenging to maintain, especially if the integration points change during refactoring.
集成测试虽然速度较慢且更复杂,但让您确信系统的不同部分可以正确交互。但是,它们的维护可能更具挑战性,尤其是在重构期间集成点发生变化时。
E2E tests provide the highest level of assurance but at the cost of speed and maintenance overhead. They are often brittle, meaning small changes in the code base can cause them to fail, even if the underlying functionality hasn’t been affected.
E2E 测试提供最高级别的保证,但代价是速度和维护开销。它们通常很脆弱,这意味着代码库中的微小更改可能会导致它们失败,即使底层功能没有受到影响。
In conclusion, the simplicity and essence of the pyramid provide a couple of rules of thumb for choosing how many and which tests to write:
总之,金字塔的简单性和本质为选择编写的测试数量和测试提供了一些经验法则:
-
Write tests with different granularity
编写不同粒度的测试
-
High-level tests should be significantly fewer in proportion to tests at lower levels
与较低级别的测试成比例,高水平测试应该明显减少
Let’s go back to the scenario explained in the Exploring the solution structure subsection at the beginning of this section. We could write unit tests that validate the interaction between SalesOrderService and WarehouseService, such as the following:
让我们回到本节开头的探索解决方案结构小节中介绍的方案。我们可以编写单元测试来验证 SalesOrderService 和 WarehouseService 之间的交互,例如:
public class SalesOrderServiceTests
{
[Fact]
public void ProcessOrder_ShouldReserveInventory()
{
// Arrange
var warehouseServiceMock = new Mock<IWarehouseService>();
var salesOrderService = new SalesOrderService(warehouseServiceMock.Object);
var order = new Order { BeerId = 1, Quantity = 10 };
// Act
salesOrderService.ProcessOrder(order);
// Assert
warehouseServiceMock.Verify(ws => ws.ReserveInventory(order.BeerId, order.Quantity), Times.Once);
}
}
Writing many unit tests provides a foundation that allows you to refactor with confidence, knowing that any regression will be caught early.
编写许多单元测试提供了一个基础,使您可以放心地重构,因为您知道任何回归都会被及早发现。
Mock library 模拟库
A mock library is a tool used in unit testing to simulate the behavior of real objects, allowing you to isolate and test specific parts of your code without relying on external systems. It helps ensure your tests are reliable, consistent, and fast by controlling the behavior of dependencies, making it easier to verify how your code interacts with other components. For this book, we choose to use Moq (https://github.com/devlooped/moq), a popular mock library for C# that makes it easy to create and configure mock objects for your tests.
模拟库是单元测试中使用的一种工具,用于模拟真实对象的行为,允许您在不依赖外部系统的情况下隔离和测试代码的特定部分。它通过控制依赖项的行为来帮助确保您的测试可靠、一致和快速,从而更轻松地验证您的代码如何与其他组件交互。在本书中,我们选择使用 Moq (https://github.com/devlooped/moq),这是一个流行的 C# 模拟库,可以轻松地为测试创建和配置模拟对象。
Another set of tests you should consider writing before starting to apply any change to the code base are those on the presentation layer. To reference our pyramid, we should also write some integration tests. The following code should give you an idea:
在开始对代码库应用任何更改之前,您应该考虑编写的另一组测试是表示层上的测试。为了引用我们的金字塔,我们还应该编写一些集成测试。以下代码应该能给你一个想法:
[Fact]
public async Task Can_Create_SalesOrder()
{
DateTime now = DateTime.UtcNow;
SalesOrderJson body = new(Guid.NewGuid().ToString(),
$"{now.Year:0000}{now.Month:00}{now.Day:00}-{now.Hour:00}{now.Minute:00}",
Guid.NewGuid(), "Customer",
now, new List<SalesOrderRowJson>
{
new()
{
BeerId = Guid.NewGuid(),
BeerName = "BrewUp IPA",
Quantity = new(10, "Lt"),
Price = new(5, "EUR")
}
});
var stringJson = JsonSerializer.Serialize(body);
var httpContent = new StringContent(stringJson, Encoding.UTF8, "application/json");
var postResult = await integrationFixture.Client.PostAsync("/v1/sales", httpContent);
Assert.Equal(HttpStatusCode.Created, postResult.StatusCode);
}
In this test, we are making sure that the payload that arrives at our service is handled as it should be without bothering the internal working. What matters is that the result does not change. For this very reason, we did not call the specific method mapped to the /v1/sales/ endpoint as you would normally do with unit tests and a mock library. Instead, we emulated an HTTP call to be as abstract as possible.
在此测试中,我们确保到达我们服务的有效负载得到应有的处理,而不会打扰内部工作。重要的是结果不会改变。正是出于这个原因,我们没有像通常使用单元测试和模拟库那样调用映射到 /v1/sales/ 端点的特定方法。相反,我们模拟了 HTTP 调用,使其尽可能抽象。
By writing these tests in both the domain and application layers, we end up with the safety net we were talking about at the beginning of this section. We can finally move on to reorganizing our code base so that it relates to the bounded contexts already explained in Chapter 3, Strategic Patterns.
通过在域层和应用程序层中编写这些测试,我们最终获得了本节开头讨论的安全网。我们终于可以继续重组我们的代码库,使其与第 3 章 “ 战略模式 ”中已经解释过的有界上下文相关。
1330

被折叠的 条评论
为什么被折叠?



