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

7/18/2021 ABPDDD

本系列文章,翻译至Implementing Domain Driven Design (opens new window)

# 示例用例

本节将演示一些示例用例并讨论替代方案。

# 实体创建

从实体/聚合根类创建对象是该实体生命周期的第一步。 聚合 / 聚合根规则和最佳实践部分建议为实体类创建一个主构造函数,以保证创建一个有效的实体。因此,每当我们需要创建该实体的实例时,我们应该始终使用该构造函数

请参阅下面的Issue聚合根类:

public class Issue : AggregateRoot<Guid>
{
    public Guid RepositoryId { get; private set; }
    public string Title { get; private set; }
    public string Text { get; set; }
    public Guid? AssignedUserId { get; internal set; }

    public Issue(
        Guid id,
        Guid repositoryId,
        string title,
        string text = null
        ) : base(id)
    {
        RepositoryId = repositoryId;
        Title = Check.NotNullOrWhiteSpace(title, nameof(title));
        Text = text; //允许为空/null
    }
    private Issue() { } /* ORMs使用该空构造函数 */

    public void SetTitle(string title)
    {
        Title = Check.NotNullOrWhiteSpace(title, nameof(title));
    }

    //...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
  • 此类保证通过其构造函数创建有效实体。
  • 如果您稍后需要更改 Title,则需要使用 SetTitle 方法,该方法继续保持 Title 处于有效状态。
  • 如果要将此问题分配给用户,则需要使用 IssueManager(它在分配之前实现了一些业务规则 - 请参阅上面的领域服务部分)。
  • Text 属性有一个公共的 setter,因为它也接受空值,并且在这个例子中没有任何验证规则。它在构造函数中也是可选的。

让我们看一个用于创建问题的应用程序服务方法:

public class IssueAppService : ApplicationService.IIssueAppService
{
    private readonly IssueManager _issueManager;
    private readonly IRepository<Issue, Guid> _issueRepository;
    private readonly IRepository<AppUser, Guid> _userRepository;

    public IssueAppService(
        IssueManager issueManager,
        IRepository<Issue, Guid> issueRepository,
        IRepository<AppUser, Guid> userRepository
    )
    {
        _issueManager = issueManager;
        _issueRepository = issueRepository;
        _userRepository = userRepository;
    }

    public async Task<IssueDto> CreateAsync(IssueCreationDto input)
    {
        //创建有效实体
        var issue = new Issue(
            GuidGenerator.Create(),
            input.RepositoryId,
            input.Title,
            input.Text
        );

        //应用领域其他操作
        if (input.AssignedUserId.HasValue)
        {
            var user = await _userRepository.GetAsync(input.AssignedUserId.Value);
            await _issueManager.AssignToAsynce(issue, user);
        }

        //保存
        await _issueRepository.InsertAsync(issue);

        //返回代表新问题的DTO
        return ObjectMapper.Map<Issue, IssueDto>(issue);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

CreateAsync 方法;

  • 使用Issue构造函数来创建一个有效的问题。它使用 IGuidGenerator (opens new window) 服务传递 Id。此处不使用自动对象映射。
  • 如果客户端想要在创建对象时将此问题分配给用户,它会使用 IssueManager 来执行此操作,方法是允许 IssueManager 在此分配之前执行必要的检查。
  • 将实体保存到数据库。
  • 最后使用 IObjectMapper 返回一个 IssueDto,它是通过从新的 Issue 实体映射自动创建的。

# 在实体创建中应用领域规则

示例Issue实体没有关于实体创建的业务规则,除了构造函数中的一些简单验证。但是,可能存在实体创建应检查一些额外业务规则的情况。

例如,假设您不希望在已经存在具有完全相同Title的问题的情况下创建问题。在哪里执行这个规则?在应用服务中实现这个规则是不合适的,因为它是一个核心业务(领域)规则,应该总是被检查。

此规则应在领域服务中实现,在本例中为 IssueManager。因此,我们需要强制应用层始终使用 IssueManager 来创建新Issue

首先,我们可以将 Issue 构造函数设为internal,而不是public

public class Issue : AggregateRoot<Guid>
{
    //...
    internal Issue(
        Guid id,
        Guid repositoryId,
        string title,
        string text = null
        ) : base(id)
    {
        RepositoryId = repositoryId;
        Title = Check.NotNullOrWhiteSpace(title, nameof(title));
        Text = text;//允许为空/null
    }
    //...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

这会阻止应用程序服务直接使用构造函数,因此它们将使用 IssueManager。然后我们可以向 IssueManager 添加一个 CreateAsync 方法:

using System;
using System.Threading.Tasks;
using Volo.Abp;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Domain.Services;

namespace IssueTracking.Issues
{
    public class IssueManager : DomainService
    {
        private readonly IRepository<Issue, Guid> _issueRepository;

        public IssueManager(IRepository<Issue, Guid> issueRepository)
        {
            _issueRepository = issueRepository;
        }

        public async Task<Issue> CreateAsync(
            Guid repositoryId,
            string title,
            string text = null)
        {
            if (await _issueRepository.AnyAsync(i => i.Title == title))
            {
                throw new BusinessException("IssueTracking:IssueWithSameTitleExists");
            }

            return new Issue(
                GuidGenerator.Create(),
                repositoryId,
                title,
                text
            );
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
  • CreateAsync 方法检查是否已经存在具有相同标题的问题,并在这种情况下引发业务异常。
  • 如果没有重复,它会创建并返回一个新Issue

为了使用 IssueManagerCreateAsync 方法,将 IssueAppService 更改为如下所示:

public class IssueAppService : ApplicationService, IIssueAppService
{
    private readonly IssueManager _issueManager;
    private readonly IRepository<Issue, Guid> _issueRepository;
    private readonly IRepository<AppUser, Guid> _userRepository;

    public IssueAppService(
        IssueManager issueManager,
        IRepository<Issue, Guid> issueRepository,
        IRepository<AppUser, Guid> userRepository)
    {
        _issueManager = issueManager;
        _issueRepository = issueRepository;
        _userRepository = userRepository;
    }

    public async Task<IssueDto> CreateAsync(IssueCreationDto input)
    {
        // 使用IssueManager创建一个有效实体
        var issue = await _issueManager.CreateAsync(
            input.RepositoryId,
            input.Title,
            input.Text
        );

        // 应用领域其他操作
        if (input.AssignedUserId.HasValue)
        {
            var user = await _userRepository.GetAsync(input.AssignedUserId.Value);
            await _issueManager.AssignToAsync(issue, user);
        }

        // 保存
        await _issueRepository.InsertAsync(issue);

        // 返回代表新问题的DTO
        return ObjectMapper.Map<Issue, IssueDto>(issue);
    }
}

// *** IssueCreationDto class ***
public class IssueCreationDto
{
    public Guid RepositoryId { get; set; }
    [Required]
    public string Title { get; set; }
    public Guid? AssignedUserId { get; set; }
    public string Text { get; set; }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

# 讨论:为什么在IssueManagerIssue没有保存到数据库中?

您可能会问“为什么 IssueManager 没有将Issue保存到数据库中?”。我们认为这是应用服务的责任。

因为,应用服务在保存它之前可能需要对问题对象进行额外的更改/操作。如果领域服务保存它,那么保存操作将重复;

  • 由于两次数据库交互,这会导致性能下降。
  • 它需要涵盖这两种操作的显式数据库事务。
  • 如果其他操作因业务规则而取消实体创建,则应在数据库中回滚事务。

当您检查 IssueAppService 时,您将看到IssueManager.CreateAsync 方法中不将 Issue 保存到的数据库的优点。否则,我们将需要执行一次插入(在 IssueManager 中)和一次更新(在分配之后)。

# 讨论:为什么应用服务中没有实现重复标题检查?

我们可以简单地说“因为它是核心领域逻辑,应该在领域层中实现”。但是,它带来了一个新问题“您如何确定它是核心领域逻辑,而不是应用程序逻辑?” (稍后我们将详细讨论差异)。

对于这个例子,一个简单的问题可以帮助我们做出决定:“如果我们有另一种方式(用例)来创建问题,我们是否仍应应用相同的规则?是否应始终实施该规则”。您可能会想“为什么我们有第二种方式来创建问题?”。然而,在现实生活中,你有;

  • 应用程序的最终用户可能会在应用程序的标准 UI 中创建问题。
  • 您可能有自己的员工使用的第二个后台应用程序,并且您可能希望提供一种创建问题的方法(在这种情况下可能具有不同的授权规则)。
  • 您可能有一个对 3rd 方客户端开放的 HTTP API,并且它们会创建问题。
  • 您可能有一个后台工作服务,它会执行某些操作并在检测到某些问题时创建问题。通过这种方式,它将在没有任何用户交互的情况下创建问题(并且可能没有任何标准的授权检查)。
  • 您可能在 UI 上有一个按钮,可以将某些内容(例如,讨论)转换为问题。

我们可以举出更多的例子。

所有这些都应该通过不同的应用服务方法来实现(参见下面的多应用层部分),但它们始终遵循规则:新问题的标题不能与任何现有问题相同!这就是为什么这个逻辑是核心领域逻辑,应该位于领域层,而不应该在所有这些应用服务方法中重复

# 更新/操作实体

创建实体后,用例会对其进行更新/操作,直到将其从系统中删除。可以有不同类型的用例直接或间接地改变一个实体。

在本节中,我们将讨论更改问题的多个属性的典型更新操作。

这一次,从更新 DTO 开始:

public class UpdateIssueDto
{
    [Required]
    public string Title {get;set;}
    public string Text{get;set;}
    public Guid? AssignedUserId{get;set;}
}
1
2
3
4
5
6
7

通过与 IssueCreationDto 进行比较,您看不到 RepositoryId。因为,我们的系统不允许跨存储库移动问题(假设为 GitHub 存储库)。只有 Title 是必需的,其他属性是可选的。

让我们看看 IssueAppService 中的更新实现:

public class IssueAppService : ApplicationService.IIssueAppService
{
    private readonly IssueManager _issueManager;
    private readonly IRepository<Issue, Guid> _issueRepository;
    private readonly IRepository<AppUser, Guid> _userRepository;

    public IssueAppService(
        IssueManager issueManager,
        IRepository<Issue, Guid> issueRepository,
        IRepository<AppUser, Guid> userRepository
    )
    {
        _issueManager = issueManager;
        _issueRepository = issueRepository;
        _userRepository = userRepository;
    }

    public async Task<IssueDto> UpdateAsync(Guid id, UpdateIssueDto input)
    {
        //从数据库中获取实体
        var issue = await _issueRepository.GetAsync(id);

        //修改标题
        await _issueManager.ChangeTitleAsync(issue, input.Title);

        //修改所属用户
        if (input.AssignedUserId.HasValue)
        {
            var user = await _userRepository.GetAsync(input.AssignedUserId.Value);
            await _issueManager.AssignToAsync(issue, user);
        }

        //修改文本(没有业务规则,可接受所有值)
        issue.Text = input.Text;

        //更新数据库实体
        await _issueRepository.UpdateAsync(issue);

        //返回代表新问题的DTO
        return ObjectMapper.Map<Issue, IssueDto>(issue);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
  • UpdateAsync 方法获取 id 作为单独的参数。它不包含在 UpdateIssueDto 中。这是一个设计决策,可帮助 ABP 在您将此服务自动公开 (opens new window)为 HTTP API 端点时正确定义 HTTP 路由。所以,这与 DDD 无关。
  • 它首先从数据库中获取Issue实体。
  • 使用 IssueManagerChangeTitleAsync 而不是直接调用 Issue.SetTitle(...)。因为我们需要像在实体创建中那样实现重复的标题检查。这需要对 IssueIssueManager 类进行一些更改(将在下面解释)。
  • 如果此请求正在更改分配的用户,则使用 IssueManagerAssignToAsync 方法。
  • 直接设置 Issue.Text 因为没有业务规则。如果我们以后需要,我们可以随时重构。
  • 保存对数据库的更改。同样,保存更改的实体是协调业务对象和事务的应用程序服务方法的责任。如果 IssueManager 已经在 ChangeTitleAsyncAssignToAsync 方法内部保存,则会出现两次数据库操作(请参阅上面的讨论:为什么在IssueManagerIssue没有保存到数据库中?上面)。
  • 最后使用 IObjectMapper 返回一个 IssueDto,它是通过从更新的 Issue 实体映射自动创建的。

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

首先,在 Issue 类中将 SetTitle 方法设置为 internal

internal void SetTitle(string title)
{
	Title=Check.NotNullOrWhiteSpace(title,nameof(title));
}
1
2
3
4

然后向 IssueManager 添加了一个新方法来更改标题:

public async Task ChangeTitleAsync(Issue issue, string title)
{
    if (issue.Title == title)
    {
        return;
    }

    if (await _issueRepository.AnyAsync(i => i.Title == title))
    {
        throw new BusinessException("IssueTracking:IssueWithSameTitleExists");
    }

    issue.SetTitle(title);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

作者:Gerry Ge

出处:实现领域驱动设计-示例用例 (opens new window)

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际 (opens new window)」许可协议进行许可。

转载请注明出处

Last Updated: 2/24/2022, 3:11:39 PM