用例演示 - 创建实体

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

创建实体

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

参见下面的问题聚合根类:

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; private set; } public Issue(
Guid id,
Guid repositoryId,
string title,
string text = null
) : base(id)
{
RepositoryId = repositoryId;
Title = Check.NotNullOrWhiteSpace(title, nameof(title));
Text = text; // 允许空值
} private Issue() { //为ORM保留的空构造函数 } public void SetTitle(string title)
{
Title = Check.NotNullOrWhiteSpace(title, nameof(title));
}
}
  • 该类保证通过其构造函数创建有效的实体

  • 如果你需要更改标题,你需要使用 SetTitle 方法保证标题在一个有效状态

  • 如果您想将这个问题分配给用户,您需要使用 IssueManager (它在分配之前实现了一些业务规则, 请参阅我之前关于 领域服务 的文章)。

  • Text 属性有一个公共setter,因为它也接受null值,并且这个示例没有任何验证规则。它在构造函数中也是可选的

让我们看看用于创建问题的Application Service方法:

public class IssueAppService : ApplicationService, IIssueAppService
{
//省略了Repository和DomainService的依赖注入 [Authorize]
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.AssignToAsync(issue, user);
} // 把问题实体保存到数据库
await _issueRepository.InsertAsync(issue); //返回表示这个新的问题的DTO
return ObjectMapper.Map<Issue, IssueDto>(issue);
}
}

CreateAsync 方法:

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

使用领域规则创建实体

上述示例, Issue 没有关于实体创建的业务规则,除了在构造函数中进行一些形式的验证。但是,在某些情况下,实体创建应该检查一些额外的业务规则

例如,假设您不希望在完全相同的标题已经存在问题的情况下创建问题。在哪里实现这个规则? 在 Application Service 中实现此规则是不合适的,因为它是一个应该始终检查的 核心业务(领域)规则

该规则应该在 领域服务 (在本例中是 IssueManager )中实现。因此,我们需要强制应用层总是使用 IssueManager 来创建一个新的 Issue

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

public class Issue : AggregateRoot<Guid>
{
internal Issue(
Guid id,
Guid repositoryId,
string title,
string text = null
) : base(id)
{
//...
}
}

这阻止了应用服务直接使用构造函数,所以它们将使用 IssueManager 。然后我们可以在 IssueManager 中添加一个 CreateAsync 方法:

public class IssueManager : DomainService
{
//省略了依赖注入 public async Task<IssueDto> 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
);
}
}
  • CreateAsync 方法检查相同标题是否已经存在问题,并在这种情况下抛出业务异常
  • 如果没有重复,则创建并返回一个新的Issue

为了使用上述方法,IssueAppService 被修改如下:

public class IssueAppService : ApplicationService, IIssueAppService
{
//省略了依赖注入 public async Task<IssueDto> CreateAsync(IssueCreationDto input)
{
//*修改为通过领域服务创建有效的问题实体, 而不是直接new
var issue = await _issueManager.CreateAsync(
GuidGenerator.Create(),
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);
}
}

讨论:为什么问题没有在 IssueManager 中保存到数据库?

你可能会问 “为什么 IssueManager 不把问题保存到数据库中?” 我们认为这是应用服务的责任

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

  • 两次数据库往返会导致性能损失
  • 需要显式的数据库事务来包含这两个操作
  • 如果由于业务规则的原因,其他操作取消了实体创建,则应该在数据库中回滚事务

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

讨论:为什么不在应用程序服务中实现重复标题检查?

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

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

  • 应用程序的最终用户可能会在应用程序的标准UI中创建问题(比如在github的网页端创建问题)
  • 您可能有第二个后台应用程序,由您自己的员工使用,您可能希望提供一种创建问题的方法(在本例中可能使用不同的授权规则)
  • 您可能有一个对第三方客户端开放的HTTP API,他们会创建问题。
  • 您可能有一个 background worker service,如果它检测到一些故障,它会做一些事情并创建问题。这样,它将在没有任何用户交互的情况下(可能没有任何标准的授权检查)创建问题。
  • 您甚至可以在UI上设置一个按钮,将某些内容 (例如,讨论) 转换为问题

综上所述,不同的应用程序始终遵循这样的规则:新问题的标题不能与任何现有问题的标题相同!他们与应用层无关! 这就是为什么该逻辑是核心领域逻辑,应该位于领域层中,而不应该在应用程序服务中实现为重复的代码。

实现领域驱动设计 - 使用ABP框架 - 创建实体的更多相关文章

  1. .net core +codefirst(.net core 基础入门,适合这方面的小白阅读) 【我们一起写框架】领域驱动设计的CodeFirst框架(一)—序篇

    .net core +codefirst(.net core 基础入门,适合这方面的小白阅读)   前言 .net core mvc和 .net mvc开发很相似,比如 视图-模型-控制器结构.所以. ...

  2. 【我们一起写框架】领域驱动设计的CodeFirst框架(一)—序篇

    前言 领域驱动设计,其实已经是一个很古老的概念了,但它的复杂度依旧让学习的人头疼不已. 互联网关于领域驱动的文章有很多,每一篇写的都很好,理解领域驱动设计的人都看的懂. 不过,这些文章对于那些初学者而 ...

  3. DDD 领域驱动设计-“臆想”中的实体和值对象

    其他博文: DDD 领域驱动设计-三个问题思考实体和值对象 DDD 领域驱动设计-三个问题思考实体和值对象(续) 以下内容属于博主"臆想",如有不当,请别当真. 扯淡开始: 诺兰的 ...

  4. (转)EntityFramework之领域驱动设计实践

    EntityFramework之领域驱动设计实践 - 前言 EntityFramework之领域驱动设计实践 (一):从DataTable到EntityObject EntityFramework之领 ...

  5. EntityFramework之领域驱动设计实践

    EntityFramework之领域驱动设计实践 - 前言 EntityFramework之领域驱动设计实践 (一):从DataTable到EntityObject EntityFramework之领 ...

  6. DDD 领域驱动设计-三个问题思考实体和值对象(续)

    上一篇:DDD 领域驱动设计-三个问题思考实体和值对象 说实话,整理现在这一篇博文的想法,在上一篇发布出来的时候就有了,但到现在才动起笔来,而且写之前又反复读了上一篇博文的内容及评论,然后去收集资料, ...

  7. DDD 领域驱动设计-三个问题思考实体和值对象

    消息场景:用户 A 发送一个消息给用户 B,用户 B 回复一个消息给用户 A... 现有设计:消息设计为实体并为聚合根,发件人.收件人设计为值对象. 三个问题: 实体最重要的特性是什么? Messag ...

  8. 【DDD】使用领域驱动设计思想实现业务系统

    最近新接了一个业务系统——社区服务系统,为了快速熟悉和梳理老系统的业务逻辑和代码,同时对老系统代码做一些优化,于是打算花上一个月时间不间断地对老系统服务进行重构.同时,考虑到社区业务的复杂性,想起了之 ...

  9. 从0开发3D引擎(补充):介绍领域驱动设计

    我们使用领域驱动设计(英文缩写为DDD)的方法来设计引擎,在引擎开发的过程中,领域模型会不断地演化. 本文介绍本系列使用的领域驱动设计思想的相关概念和知识点,给出了相关的资料. 上一篇博文 从0开发3 ...

  10. 基于ABP落地领域驱动设计-05.实体创建和更新最佳实践

    目录 系列文章 数据传输对象 输入DTO最佳实践 不要在输入DTO中定义不使用的属性 不要重用输入DTO 输入DTO中验证逻辑 输出DTO最佳实践 对象映射 学习帮助 系列文章 基于ABP落地领域驱动 ...

随机推荐

  1. maven 环境搭建

    1.maven环境搭建 1)下载maven,http://maven.apache.org/download.cgi,到本地解压,然后配置环境变量 MAVEN_HOME:D:\software\apa ...

  2. C字符串和指针问题汇总

    空指针和传参问题 1) 段错误.形参改为二级指针即可 void GetMemory( char *p ){ p = ( ); } void Test( void ){ char *str = NULL ...

  3. JS实现颜色值的转换

    从网上看了个案例,是实现颜色值转换的,就想着自己也写个.网上的案例链接找不到了,这里也就不贴了. JavaScript颜色转换的核心就是进制间的转换. rgba(0,0,0,.4)转换成#000000 ...

  4. commons-logging日志系统

    日志的重要性是随着系统的膨胀而显现的,在一个庞大的系统中查错没有各种日志信息    是寸步难行的.所以在系统加入日志是必须的. 最原始的日志方式,就是在程序的适当地方添加System.out.prin ...

  5. js模拟快捷键操作表单

    <html> <head> </head> <body> <script> //键盘快捷键提交表单ctrl+s document.onkey ...

  6. ORACLE触发器具体解释

    ORACLE PL/SQL编程之八: 把触发器说透 本篇主要内容例如以下: 8.1 触发器类型 8.1.1 DML触发器 8.1.2 替代触发器 8.1.3 系统触发器 8.2 创建触发器 8.2.1 ...

  7. cmd 窗口的复制粘贴

    如下几种方法1.点击鼠标右键,选择标志,再点击左键拖动选择要复制的内容,然后回车即可复制被 选择的内容 2.点击鼠标右键,选择标志,再点击左键拖动选择要复制的内容,然后点击鼠标右键, 此时就把选择的内 ...

  8. Linux 添加epel源

    1.epel-release yum install epel-release 这样有些没办法通过yum 安装  可以这样安装(例如redis)

  9. Telephone Lines USACO 月赛

    以前做过这套题目 这个题又重新写了:http://www.cnblogs.com/jh818012/archive/2013/05/05/3182681.html 还是以前的思路 一直错在一个地方:决 ...

  10. .net 图片压缩

    压缩图片方法: /// <summary> /// 生成缩略图 /// </summary> /// <param name="originalImagePat ...