实现领域驱动设计 - 使用ABP框架 - 创建实体

打印 上一主题 下一主题

主题 548|帖子 548|积分 1644

用例演示 - 创建实体

本节将演示一些示例用例并讨论可选场景。
创建实体

从实体/聚合根类创建对象是实体生命周期的第一步。聚合/聚合根规则和最佳实践部分 建议为Entity类创建一个主构造函数,以保证创建一个有效的实体。因此,无论何时我们需要创建实体的实例,我们都应该使用那个构造函数
参见下面的问题聚合根类:
  1. public class Issue : AggregateRoot<Guid>
  2. {
  3.     public Guid RepositoryId { get; private set; }
  4.     public string Title { get; private set; }
  5.     public string Text { get; set; }
  6.     public Guid? AssignedUserId { get; private set; }
  7.    
  8.     public Issue(
  9.         Guid id,
  10.         Guid repositoryId,
  11.         string title,
  12.         string text = null
  13.     ) : base(id)
  14.     {
  15.         RepositoryId = repositoryId;
  16.         Title = Check.NotNullOrWhiteSpace(title, nameof(title));
  17.         Text = text; // 允许空值
  18.     }
  19.     private Issue() { //为ORM保留的空构造函数 }
  20.     public void SetTitle(string title)
  21.     {
  22.         Title = Check.NotNullOrWhiteSpace(title, nameof(title));
  23.     }
  24. }
复制代码

  • 该类保证通过其构造函数创建有效的实体
  • 如果你需要更改标题,你需要使用 SetTitle 方法保证标题在一个有效状态
  • 如果您想将这个问题分配给用户,您需要使用 IssueManager (它在分配之前实现了一些业务规则, 请参阅我之前关于 领域服务 的文章)。
  • Text 属性有一个公共setter,因为它也接受null值,并且这个示例没有任何验证规则。它在构造函数中也是可选的
让我们看看用于创建问题的Application Service方法:
  1. public class IssueAppService : ApplicationService, IIssueAppService
  2. {
  3.     //省略了Repository和DomainService的依赖注入
  4.     [Authorize]
  5.     public async Task<IssueDto> CreateAsync(IssueCreationDto input)
  6.     {
  7.         //创建一个有效的问题实体
  8.         var issue = new Issue(
  9.             GuidGenerator.Create(),
  10.             input.RepositoryId,
  11.             input.Title,
  12.             input.Text
  13.         );
  14.         //如果传入了被分配人,则把该问题法分配给这个用户
  15.         if(input.AssignedUserId.HasValue)
  16.         {
  17.             var user = await _userRepository.GetAsync(input.AssignedUserId.Value);
  18.             await _issueManager.AssignToAsync(issue, user);
  19.         }
  20.         // 把问题实体保存到数据库
  21.         await _issueRepository.InsertAsync(issue);
  22.         //返回表示这个新的问题的DTO
  23.         return ObjectMapper.Map<Issue, IssueDto>(issue);
  24.     }
  25. }
复制代码
CreateAsync 方法:

  • 使用 Issue 构造函数创建有效的问题。它使用 IGuidGenerator 服务传递Id。这里不使用自动对象映射
  • 如果客户端希望在对象创建时将这个问题分配给用户,它会使用IssueManager 来完成,允许 IssueManager 在分配之前执行必要的检查。
  • 保存实体到数据库
  • 最后使用 IObjectMapper 返回一个 IssueDto ,该 IssueDto 是通过映射从新的 Issue 实体自动创建的
使用领域规则创建实体

上述示例, Issue 没有关于实体创建的业务规则,除了在构造函数中进行一些形式的验证。但是,在某些情况下,实体创建应该检查一些额外的业务规则
例如,假设您不希望在完全相同的标题已经存在问题的情况下创建问题。在哪里实现这个规则? 在 Application Service 中实现此规则是不合适的,因为它是一个应该始终检查的 核心业务(领域)规则
该规则应该在 领域服务 (在本例中是 IssueManager )中实现。因此,我们需要强制应用层总是使用 IssueManager 来创建一个新的 Issue
首先,我们可以将 Issue 构造函数设置为 internal ,而不是 public:
  1. public class Issue : AggregateRoot<Guid>
  2. {
  3.     internal Issue(
  4.         Guid id,
  5.         Guid repositoryId,
  6.         string title,
  7.         string text = null
  8.     ) : base(id)
  9.     {
  10.         //...
  11.     }
  12. }
复制代码
这阻止了应用服务直接使用构造函数,所以它们将使用 IssueManager 。然后我们可以在 IssueManager 中添加一个 CreateAsync 方法:
  1. public class IssueManager : DomainService
  2. {
  3.     //省略了依赖注入
  4.     public async Task<IssueDto> CreateAsync(
  5.         Guid repositoryId,
  6.         string title,
  7.         string text = null
  8.     )
  9.     {
  10.         //如果存在相同标题的问题,直接抛错
  11.         if(await _issueRepository.AnyAsync(i => i.Title == title))
  12.         {
  13.             throw new BusinessException("IssueTracking:IssueWithSameTitleExists");
  14.         }
  15.         //创建一个有效的问题实体
  16.         return new Issue(
  17.             GuidGenerator.Create(),
  18.             repositoryId,
  19.             title,
  20.             text
  21.         );
  22.     }
  23. }
复制代码

  • CreateAsync 方法检查相同标题是否已经存在问题,并在这种情况下抛出业务异常
  • 如果没有重复,则创建并返回一个新的Issue
为了使用上述方法,IssueAppService 被修改如下:
  1. public class IssueAppService : ApplicationService, IIssueAppService
  2. {
  3.     //省略了依赖注入
  4.     public async Task<IssueDto> CreateAsync(IssueCreationDto input)
  5.     {
  6.         //★修改为通过领域服务创建有效的问题实体, 而不是直接new
  7.         var issue = await _issueManager.CreateAsync(
  8.             GuidGenerator.Create(),
  9.             input.RepositoryId,
  10.             input.Title,
  11.             input.Text
  12.         );
  13.         //如果传入了被分配人,则把该问题法分配给这个用户
  14.         if(input.AssignedUserId.HasValue)
  15.         {
  16.             var user = await _userRepository.GetAsync(input.AssignedUserId.Value);
  17.             await _issueManager.AssignToAsync(issue, user);
  18.         }
  19.         // 把问题实体保存到数据库
  20.         await _issueRepository.InsertAsync(issue);
  21.         //返回表示这个新的问题的DTO
  22.         return ObjectMapper.Map<Issue, IssueDto>(issue);
  23.     }
  24. }
复制代码
讨论:为什么问题没有在 IssueManager 中保存到数据库?

你可能会问 “为什么 IssueManager 不把问题保存到数据库中?” 我们认为这是应用服务的责任
因为,在保存问题对象之前,应用程序服务可能需要对其进行额外的更改/操作。如果领域服务保存它,则保存操作将重复

  • 两次数据库往返会导致性能损失
  • 需要显式的数据库事务来包含这两个操作
  • 如果由于业务规则的原因,其他操作取消了实体创建,则应该在数据库中回滚事务
当你检查 IssueAppService 时,你会看到在 IssueManager.CreateAsync 中不保存 Issue 到数据库的好处。否则,我们将需要执行一次插入(在 IssueManager 中)和一次更新(在分配问题之后)
讨论:为什么不在应用程序服务中实现重复标题检查?

我们可以简单地说 “因为它是一个核心领域逻辑,应该在领域层中实现”。然而,这带来了一个新的问题: “您如何判断它是核心领域逻辑,而不是应用程序逻辑?” (稍后我们将详细讨论其中的差异)
对于这个例子,一个简单的问题可以帮助我们做出决定: “如果我们有另一种方法(用例)来创建一个问题,我们是否仍然应用相同的规则?” 你可能会想 “为什么我们有第二种制造问题的方式?” 然而,在现实生活中,你有:


  • 应用程序的最终用户可能会在应用程序的标准UI中创建问题(比如在github的网页端创建问题)
  • 您可能有第二个后台应用程序,由您自己的员工使用,您可能希望提供一种创建问题的方法(在本例中可能使用不同的授权规则)
  • 您可能有一个对第三方客户端开放的HTTP API,他们会创建问题。
  • 您可能有一个 background worker service,如果它检测到一些故障,它会做一些事情并创建问题。这样,它将在没有任何用户交互的情况下(可能没有任何标准的授权检查)创建问题。
  • 您甚至可以在UI上设置一个按钮,将某些内容 (例如,讨论) 转换为问题
综上所述,不同的应用程序始终遵循这样的规则:新问题的标题不能与任何现有问题的标题相同!他们与应用层无关! 这就是为什么该逻辑是核心领域逻辑,应该位于领域层中,而不应该在应用程序服务中实现为重复的代码。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

嚴華

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表