实现领域驱动设计——示例用例

This section will demonstrate some example use cases and discuss alternative scenarios.

本节将演示一些具体的示例用例,并探讨其他可能的替代场景。

Entity Creation    实体创建

Creating an object from an Entity / Aggregate Root class is the first step of the lifecycle of that entity. The Aggregate / Aggregate Root Rules & Best Practices section suggests creating a primary constructor for the Entity class that guarantees to create a valid entity. So, whenever we need to create an instance of that entity, we should always use that constructor.

从实体(Entity)或聚合根(Aggregate Root)类创建对象,是该实体生命周期的第一步。“聚合 / 聚合根规则与最佳实践”章节建议,为实体类创建一个主构造函数,以确保能够创建出一个合法(有效)的实体。因此,每当我们想要创建该实体的一个实例时,都应该始终使用这个构造函数。

See the Issue Aggregate Root class below:

请看下方的 Issue 聚合根类:

image

  • This class guarantees to create a valid entity by its constructor.

    这个类通过其构造函数,确保了能够创建出一个合法(有效)的实体。

  • If you need to change the Title later, you need to use the SetTitle method which continues to keep Title in a valid state.

    如果你后续需要修改标题(Title),你必须使用 SetTitle 方法,该方法会确保 Title 始终保持在合法的状态。

  • If you want to assign this issue to a user, you need to use IssueManager (it implements some business rules before the assignment - see the Domain Services section above to remember).

    如果你想将这个工单(Issue)分配给某个用户,你需要使用 IssueManager(它在分配之前会执行一些业务规则——可以回顾一下上面的“领域服务”章节)。

  • The Text property has a public setter, because it also accepts null values and does not have any validation rules for this example. It is also optional in the constructor.

    Text 属性拥有一个公开的 Setter,因为在这个示例中,它允许接受 null 值,并且没有任何验证规则。同时,它在构造函数中也是可选参数。

Let's see an Application Service method that is used to create an issue:

下面,让我们来看一个用于创建工单的应用服务(Application Service)方法:

image

CreateAsync method:

CreateAsync 方法:

  • Uses the Issue constructor to create a valid issue. It passes the Id using the IGuidGenerator service. It doesn't use auto object mapping here.

    使用 Issue 的构造函数来创建一个合法的工单。它通过 IGuidGenerator 服务来传递(生成)Id。在这里,它并没有使用自动对象映射。

  • If the client wants to assign this issue to a user on object creation, it uses the IssueManager to do it by allowing the IssueManager to perform the necessary checks before this assignment.

    如果客户端希望在创建对象的同时将该工单分配给某个用户,它会使用 IssueManager 来完成此操作,这样 IssueManager 就能在分配之前执行必要的检查。

  • Saves the entity to the database.

    将该实体保存到数据库中。

  • Finally uses the IObjectMapper to return an IssueDto that is automatically created by mapping from the new entity.

    最后,使用 IObjectMapper 返回一个 IssueDto,该 DTO 是通过从新创建的实体自动映射生成的。

Applying Domain Rules on Entity Creation    在实体创建时应用领域规则

The example Issue entity has no business rule on entity creation, except some formal validations in the constructor. However, there may be scenarios where entity creation should check some extra business rules.

示例中的 Issue 实体在创建时并没有复杂的业务规则,除了构造函数中的一些形式上的验证。然而,在某些场景下,实体的创建可能需要检查一些额外的业务规则。

For example, assume that you don't want to allow creating an issue if there is already an issue with exactly the same Title. Where to implement this rule? It is not proper to implement this rule in the Application Service, because it is a core business (domain) rule that should always be checked.

例如,假设你不想允许创建标题(Title)完全相同的工单(即如果已经存在一个标题一模一样的工单,就不允许再创建)。那么,这条规则应该在哪里实现呢?把它放在应用服务(Application Service)里是不合适的,因为它是一条核心的业务(领域)规则,必须时刻被检查。

This rule should be implemented in a Domain Service, IssueManager in this case. So, we need to force the Application Layer always to use the IssueManager to create a new Issue.

这条规则应该在领域服务(Domain Service)中实现,在这个案例中就是 IssueManager。因此,我们需要强制应用层始终通过 IssueManager 来创建一个新的 Issue

First, we can make the Issue constructor internal, instead of public:

首先,我们可以将 Issue 的构造函数改为 internal(内部访问),而不是 public(公开访问):

image

This prevents Application Services from directly using the constructor, so they will use the IssueManager. Then we can add a CreateAsync method to the IssueManager:

这样一来,应用服务(Application Services)就无法直接使用构造函数了,因此它们不得不去调用 IssueManager。接下来,我们可以在 IssueManager 中添加一个 CreateAsync 方法:

image

  • CreateAsync method checks if there is already an issue with the same title and throws a business exception in this case.

    CreateAsync 方法会检查是否已经存在具有相同标题的工单,如果存在,则抛出一个业务异常。

  • If there is no duplication, it creates and returns a new Issue.

    如果没有重复,它就会创建并返回一个新的 Issue 实体。

The IssueAppService is changed as shown below in order to use the IssueManager's CreateAsync method:

IssueAppService 进行了如下修改,以便使用 IssueManagerCreateAsync 方法:

image

Discussion: Why is the Issue not saved to the database in IssueManager?    讨论:为什么 IssueManager 没有把 Issue 保存到数据库?

You may ask "Why didn't IssueManager save the Issue to the database?". We think it is the responsibility of the Application Service.

你可能会问:“为什么 IssueManager 不直接把 Issue 保存到数据库里呢?” 我们认为,这应该是应用服务(Application Service)的职责。

Because the Application Service may require additional changes/operations on the Issue object before saving it. If the Domain Service saves it, then the Save operation is duplicated:

因为应用服务在保存 Issue 对象之前,可能还需要对其进行额外的修改或操作。如果领域服务(Domain Service)把它存了,就会导致保存操作重复:

  • It causes performance loss because of double database round trip.

    由于需要两次数据库往返(round trip),会造成性能损耗。

  • It requires an explicit database transaction that covers both operations.

    这要求有一个显式的数据库事务来同时覆盖这两个操作。

  • If additional actions cancel the entity creation because of a business rule, the transaction should be rolled back in the database.

    如果后续的操作因为触发了某条业务规则而取消了实体的创建,那么数据库中的事务就必须进行回滚。

When you check the IssueAppService, you will see the advantage of not saving the Issue to the database in the IssueManager.CreateAsync. Otherwise, we would need to perform one Insert (in the IssueManager) and one Update (after Assignment).

当你查看 IssueAppService 的代码时,就会发现不在 IssueManager.CreateAsync 中保存 Issue 到数据库的好处。否则的话,我们就需要执行一次插入(Insert,在 IssueManager 中)和一次更新(Update,在分配用户之后)。

Discussion: Why is the duplicate Title check not implemented in the Application Service?    讨论:为什么“标题重复检查”没有放在应用服务(Application Service)里实现?

We could simply say "Because it is a core domain logic and should be implemented in the Domain Layer". However, it brings a new question: "How did you decide that it is a core domain logic, but not an application logic?" (we will discuss the difference later with more details).

我们本来可以简单地回答:“因为这是核心领域逻辑,所以应该在领域层(Domain Layer)实现。” 然而,这又引出了一个新的问题:“你是怎么判断它属于核心领域逻辑,而不是应用逻辑的呢?”(我们稍后会详细讨论这两者的区别)。

For this example, a simple question can help us to make the decision: "If we have another way (use case) of creating an issue, should we still apply the same rule? Is that rule should always be implemented". You may think "Why do we have a second way of creating an issue?". However, in real life, you have;

针对这个例子,一个简单的问题就能帮我们做出决定:“如果我们还有另一种创建工单的方式(用例),我们是否仍然需要应用同一条规则?这条规则是否应该始终被执行?” 你可能会想:“我们为什么会有第二种创建工单的方式呢?” 然而,在现实生活中,你确实会有;

  • End users of the application may create issues in your application's standard UI.

    应用程序的终端用户可能会在你的应用标准用户界面(UI)中创建工单。

  • You may have a second back office application that is used by your own employees and you may want to provide a way of creating issues (probably with different authorization rules in this case).

    你可能还有一个供内部员工使用的后台办公应用程序,并且你可能希望提供一种创建工单的方式(在这种情况下,可能会有不同的授权规则)。

  • You may have an HTTP API that is open to 3rd-party clients and they create issues.

    你可能有一个面向第三方客户端开放的 HTTP API,他们可以通过该接口创建工单。

  • You may have a background worker service that does something and creates issues if it detects some problems. In this way, it will create an issue without any user interaction (and probably without any standard authorization check).

    你可能有一个后台工作服务(Background Worker),它负责执行某些任务,并在检测到问题时创建工单。这样一来,它就会在没有任何用户交互的情况下创建工单(而且可能不需要经过任何标准的授权检查)。

  • You may have a button on the UI that converts something (for example, a discussion) to an issue.

    你的用户界面上可能有一个按钮,可以将某些内容(例如,一次讨论)直接转换为一个工单。

We can give more examples. All of these should be implemented by different Application Service methods (see the Multiple Application Layers section below), but they always follow the rule: Title of the new issue can not be same of any existing issue! That's why this logic is a core domain logic, should be located in the Domain Layer and should not be duplicated in all these application service methods.

我们可以举出更多的例子。所有这些场景都应该由不同的应用服务(Application Service)方法来实现(请参阅下方的“多应用层”章节),但它们都必须始终遵循同一条规则:新工单的标题不能与任何现有工单的标题相同! 正因如此,这条逻辑属于核心领域逻辑,它应该被放在领域层(Domain Layer),而不应该在这些应用服务方法中被重复编写。

Updating/Manipulating An Entity    更新/操作实体

Once an entity is created, it is updated/manipulated by the use cases until it is deleted from the system. There can be different types of the use cases directly or indirectly changes an entity.

实体一旦被创建,就会在各种用例(Use Cases)中被不断地更新或操作,直到它最终从系统中被删除。存在多种不同类型的用例,它们会直接或间接地改变一个实体。

In this section, we will discuss a typical update operation that changes multiple properties of an Issue.

在本节中,我们将探讨一个典型的更新操作,即修改一个工单(Issue)的多个属性。

This time, beginning from the Update DTO:

这一次,我们从更新专用的数据传输对象(Update DTO)开始讲起:

image

By comparing to IssueCreationDto, you see no RepositoryId. Because, our system doesn't allow moving issues across repositories (think as GitHub repositories). Only Title is required and the other properties are optional.

IssueCreationDto(创建工单的数据传输对象)相比,你会发现这里没有 RepositoryId(仓库ID)。这是因为,我们的系统不允许将工单在不同的仓库之间移动(你可以把它想象成 GitHub 的仓库)。只有 Title(标题)是必填项,其他属性都是可选的。

Let's see the Update implementation in the IssueAppService:

接下来,让我们看看 IssueAppService 中的更新操作具体是如何实现的:

image

  • The UpdateAsync method gets the id as a separate parameter. It is not included in the UpdateIssueDto. This is a design decision that helps ABP to properly define HTTP routes when you auto-expose this service as an HTTP API endpoint. So, that's not related to DDD.

    UpdateAsync 方法将 id 作为一个独立的参数来获取,它并没有被包含在 UpdateIssueDto 中。这是一个设计决策,目的是当你将此服务自动暴露为 HTTP API 端点时,帮助 ABP 框架能够正确地定义 HTTP 路由。因此,这与 DDD(领域驱动设计)本身无关。

  • It starts by getting the Issue entity from the database.

    首先,它从数据库中获取 Issue 实体。

  • Uses IssueManager's ChangeTitleAsync instead of directly calling Issue.SetTitle(...). Because we need to implement the duplicate Title check as just done in the Entity Creation. This requires some changes in the Issue and IssueManager classes (will be explained below).

    它使用 IssueManagerChangeTitleAsync 方法,而不是直接调用 Issue.SetTitle(...)。这是因为我们需要像创建实体时那样,实现标题重复的检查。这需要对 IssueIssueManager 类进行一些修改(将在下文解释)。

  • Uses IssueManager's AssignToAsync method if the assigned user is being changed with this request.

    如果请求中要更改被分配的用户,则使用 IssueManagerAssignToAsync 方法。

  • Directly sets the Issue.Text since there is no business rule for that. If we need later, we can always refactor.

    直接设置 Issue.Text,因为针对这个字段并没有任何业务规则。如果我们以后有需要,随时可以进行重构。

  • Saves changes to the database. Again, saving changed entities is a responsibility of the Application Service method that coordinates the business objects and the transaction. If the IssueManager had saved internally in ChangeTitleAsync and AssignToAsync methods, there would be double database operations (see the Discussion: Why is the Issue not saved to the database in IssueManager? above).

    将更改保存到数据库。同样,保存实体的变更是应用服务(Application Service)方法的职责,它负责协调业务对象和事务。如果 IssueManagerChangeTitleAsyncAssignToAsync 方法内部进行了保存操作,那么就会产生两次数据库操作(请参见上文讨论:为什么 Issue 不在 IssueManager 中保存到数据库?)。

  • Finally, uses the IObjectMapper to return an IssueDto that is automatically created by mapping from the updated Issue entity.

    最后,使用 IObjectMapper 返回一个 IssueDto,该 DTO 是通过从更新后的 Issue 实体自动映射生成的。

As said, we need some changes in the Issue and IssueManager classes.

如前所述,我们需要对 IssueIssueManager 类进行一些修改。

First, made SetTitle internal in the Issue class:

首先,将 Issue 类中的 SetTitle 方法改为 internal(内部访问):

image

Then added a new method to the IssueManager to change the Title:

接着,在 IssueManager 中添加了一个新方法来修改标题:

image

posted @ 2026-05-17 15:32  菜鸟吊思  阅读(9)  评论(0)    收藏  举报