原文链接: https://dev.to/salah856/implementing-domain-driven-design-part-iv-29m2
对象到对象映射
当两个对象具有相同或相似的属性时,自动对象到对象映射是一种将值从一个对象复制到另一个对象的有用方法。
DTO和实体类通常具有相同/相似的属性,你通常需要从实体创建DTO对象。
与手动映射相比,ABP与AutoMapper集成的对象到对象映射系统使这些操作更加容易。
仅对实体使用自动对象映射以输出DTO映射。
不要将自动对象映射用于输入DTO到实体的映射。
不应该使用输入DTO到实体自动映射的原因有一些;
Entity类通常有一个构造函数,它接受参数并确保创建有效的对象。自动对象映射操作通常需要一个空的构造函数。
大多数实体属性将具有私有设置器,你应该使用方法以受控方式更改这些属性。
你通常需要仔细验证和处理用户/客户端输入,而不是盲目地映射到实体属性。
示例用例
本节将演示一些示例用例并讨论替代方案。
实体创建
从实体/聚合根类创建对象是该实体生命周期的第一步。
聚合/聚合根规则和最佳实践部分建议为Entity类创建一个主构造函数,以保证创建一个有效的实体。
因此,每当我们需要创建该实体的实例时,我们应该始终使用该构造函数。
请参阅下面的问题聚合根类:
让我们看一个用于创建问题的应用程序服务方法:
CreateAsync方法;
使用问题构造函数来创建一个有效的问题。它使用IGuidGenerator服务传递Id。它在这里不使用自动对象映射。
如果客户端希望在创建对象时将此问题分配给用户,它会使用IssueManager来执行此操作,方法是允许IssueManager在此分配之前执行必要的检查。
将实体保存到数据库
最后使用IObjectMapper返回一个IssueDto,它是通过从新的Issue实体映射自动创建的。
将领域规则应用于实体创建
示例问题实体没有关于实体创建的业务规则,除了构造函数中的一些正式验证。
但是,在某些情况下,实体创建可能需要检查一些额外的业务规则。
例如,假设如果已经存在具有完全相同标题的问题,则你不想允许创建问题。
在哪里执行这个规则?在Application Service中实现此规则是不合适的,因为它是核心业务(域)规则,应始终检查。
此规则应在域服务中实现,在这种情况下为IssueManager。
所以,我们需要强制应用层总是使用IssueManager来创建一个新的Issue。
首先,我们可以将问题构造函数设置为内部的,而不是公开的:
这可以防止应用程序服务直接使用构造函数,所以他们将使用IssueManager。
然后我们可以在IssueManager中添加一个CreateAsync方法:
CreateAsync方法检查是否已经存在具有相同标题的问题,并在这种情况下引发业务异常。
如果没有重复,它会创建并返回一个新问题。
为了使用IssueManager的CreateAsync方法,如下所示更改了IssueAppService:
更新/操作实体
一旦创建了实体,它就会被用例更新/操作,直到它从系统中删除。
可以有不同类型的用例直接或间接地改变一个实体。
在本节中,我们将讨论更改问题的多个属性的典型更新操作。
这一次,从更新DTO开始:
通过与IssueCreationDto进行比较,你看不到RepositoryId。
因为,我们的系统不允许跨存储库移动问题(想象一下GitHub存储库)。
只有Title是必需的,其他属性是可选的。
让我们看看IssueAppService中的Update实现:
领域逻辑和应用逻辑
领域驱动设计中的业务逻辑分为两部分(层):领域逻辑和应用逻辑:
领域逻辑由系统的核心域规则组成,而应用程序逻辑实现应用程序特定的用例。
虽然定义很清楚,但实现可能不是那么简单。你可能不确定哪个代码应该放在应用层,哪些代码应该在领域层。
多个应用层
当你的系统很大时,DDD有助于处理复杂性。特别是,如果在单个领域中开发多个应用程序,那么领域逻辑与应用程序逻辑的分离就变得更加重要。
假设你正在构建一个具有多个应用程序的系统;
使用ASP.NET Core MVC构建的公共网站应用程序,用于向用户展示你的产品。这样的网站不需要身份验证即可查看产品。用户仅在执行某些操作(如将产品添加到购物篮)时才登录网站。
使用Angular UI(使用 REST API)构建的后台应用程序。该应用程序由公司的办公室工作人员用来管理系统(如编辑产品描述)。
与公共网站相比,具有更简单UI的移动应用程序。它可以通过REST API或其他技术(如TCP套接字)与服务器通信。
每个应用程序都会有不同的需求、不同的用例(应用程序服务方法)、不同的DTO、不同的验证和授权规则……等等。
将所有这些逻辑混合到单个应用程序层中,会使你的服务包含太多具有复杂业务逻辑的条件,会使你的代码更难开发、维护和测试,并导致潜在的错误。
如果你有多个具有单个域的应用程序;
为每个应用程序/客户端类型创建单独的应用程序层,并在这些单独的层中实现应用程序特定的业务逻辑。
使用单个领域层来共享核心域逻辑。
这样的设计使得区分领域逻辑和应用逻辑变得更加重要。
为了更清楚地了解实现,你可以为每种应用程序类型创建不同的项目 (.csproj)。例如:
IssueTracker.Admin.Application & IssueTracker.Admin.Application.Contracts,后台(管理员)应用程序的项目。
IssueTracker.Public.Application & IssueTracker.Public.Application.Contracts,公共 Web 应用程序的项目。
IssueTracker.Mobile.Application & IssueTracker.Mobile.Application.Contracts,移动应用项目
示例:在领域服务中创建新组织
让我们看一下CreateAsync方法,讨论代码部分是否应该在领域服务中:
正确:它首先检查重复的组织名称,并在这种情况下抛出异常。这与核心域规则有关,我们绝不允许重复名称。
错误:领域服务不应执行授权。授权应该在应用层完成。
错误:它记录一条包含当前用户的用户名的消息。领域服务不应依赖于当前用户。即使系统中没有用户,领域服务也应该可用。当前用户(会话)应该是一个表示/应用层相关的概念。
错误:它发送一封关于这个新组织创建的电子邮件。我们认为这也是特定于用例的业务逻辑。你可能希望在不同的用例中创建不同类型的电子邮件,或者在某些情况下不需要发送电子邮件。
示例:在应用程序服务中创建新组织
让我们一步步看一下 CreateAsync 方法,讨论代码部分是否应该在应用服务中:
正确:应用程序服务方法应该是工作单元(事务性)。ABP的工作单元系统使这一过程自动化(即使不需要为应用程序服务添加[UnitOfWork]属性)。
正确:授权应该在应用层完成。在这里,它是通过使用[Authorize]属性来完成的。
正确:调用付款(基础设施服务)来为此操作收费(创建组织是我们业务中的付费服务)。
正确:应用程序服务方法负责将更改保存到数据库。
正确:我们可以向系统管理员发送电子邮件作为通知。
错误:不要从应用程序服务返回实体。改为返回 DTO。
示例:CRUD 操作
此应用程序服务本身不做任何事情,并将所有工作委托给领域服务。它甚至将DTO传递给 IssueManager。
不要仅为没有任何领域逻辑的简单CRUD操作创建领域服务方法。
切勿将DTO传递给域服务或从域服务返回DTO。
应用服务可以直接使用仓储来查询、创建、更新或删除数据,除非在这些操作期间需要执行一些领域逻辑。
在这种情况下,创建领域服务方法,但仅用于那些真正需要的方法。
如果你觉得这篇文章对你有所启发,请关注我的个人公众号”My IO“