领域逻辑 & 应用逻辑

如前所述,领域驱动设计中的业务逻辑分为两部分(层):领域逻辑和应用逻辑:

  • 领域逻辑由系统的核心领域规则组成,应用逻辑实现应用特定的用例

虽然定义很明确,但实现起来可能并不容易。您可能无法决定哪些代码应该位于应用程序层,哪些代码应该位于领域层。本节试图解释其中的差异

多个应用程序层

当系统比较大时,DDD有助于处理复杂性。特别是,如果在一个领域中开发了多个应用程序,那么领域逻辑与应用程序逻辑的分离就变得重要得多。

假设您正在构建一个具有多个应用程序的系统

  • 一个网站应用程序,用 ASP.NET Core MVC 构建,向用户展示你的产品。这样的网站不需要认证就可以看到产品。用户只有在执行某些操作(比如将产品添加到购物车中)时才会登录到网站。

  • 一个后台管理程序,使用 Angular UI 构建(使用REST APIs)。本应用被公司办公人员使用来管理系统(如编辑产品描述)

  • 一个移动应用程序, 与网站相比,它具有更简单的UI。它可以通过 REST APIs 或其他技术(如TCP套接字)与服务器通信。

每个应用程序都有不同的需求、不同的用例(应用服务方法)、不同的dto、不同的验证和授权规则……等

如果将所有这些逻辑混合到单个应用程序层会使您的服务包含太多的逻辑,使代码更难开发、维护和测试,并导致潜在的bug

如果一个领域有多个应用程序:

  • 为每个应用程序/客户端类型创建单独的应用程序层,并在这些单独的层中实现应用程序特定的业务逻辑。
  • 使用单个领域层共享核心领域逻辑。

这样的设计使得区分领域逻辑和应用程序逻辑变得更加重要。

为了更清楚地了解实现,您可以为每个应用程序类型创建不同的项目(.csproj)。例如:

  • 后台管理应用: IssueTracker.Admin.Application & IssueTracker.Admin.Application.Contracts

  • 公共网站应用: IssueTracker.Public.Application & IssueTracker.Public.Application.Contracts

  • 移动应用: IssueTracker.Mobile.Application & IssueTracker.Mobile.Application.Contracts

案例

本节包含一些应用程序服务和领域服务示例,以讨论如何决定将业务逻辑放置在这些服务中

示例:在领域服务中新建组织

public class OrganizationManager : DomainService
{
//省略了依赖注入
public async Task<Organization> CreateAsync(string name)
{
if(await _organizationRepository.AnyAsync(x => x.Name == name))
{
throw new BusinessException("IssueTracking:DuplicateOrganizationName");
} await _authorizationService.CheckAsync("OrganizationCreationPermissin"); Logger.LogDebug($"Creating organization {name} by {_currentUser.UserName}"); var organization = new Organization(); await _emailSender.SendAsync(
"admin@issuetracking.com",
"New Organization",
"A new organization created with name: " + name
); return organization;
}
}

让我们一步一步地看看 CreateAsync 方法,来讨论代码部分是否应该放在领域服务中

  • 正确: 它首先检查重复的组织名称,并在这种情况下抛出异常。这与核心领域规则有关,我们不允许重复名称

  • 错误: 领域服务不应该执行授权。授权 应该在应用层中完成。

  • 错误: 它记录了包含当前用户的用户名的消息。领域服务不应该依赖于当前用户。即使系统中没有用户,领域服务也应该可用。当前用户(会话)应该是一个与表示/应用层相关的概念。

  • 错误: 它发送了关于这个新组织创建的 电子邮件。我们认为这也是一个特定于用例的业务逻辑。您可能希望在不同的用例中创建不同类型的电子邮件,或者在某些情况下不需要发送电子邮件。

示例:在应用服务中新建组织

public class OrganizationAppService : ApplicationService
{
//省略了依赖注入 [UnitOfWork]
[Authorize("OrganizationCreationPermissin")]
public async Task<Organization> CreateAsync(CreateOrganizationDto input)
{
await _paymentService.ChargeAsync(
CurrentUser.Id,
GetOrganizationPrice()
); var organization = await _organizationManager.CreateAsync(input.Name); await _organizationManager.InsertAsync(organization); await _emailSender.SendAsync(
"admin@issuetracking.com",
"New Organization",
"A new organization created with name: " + input.Name
); return organization; //!!!
} private double GetOrganizationPrice()
{
return 42.0; //或者从其他地方获取
}
}

让我们一步一步地看看 CreateAsync 方法,来讨论代码部分是否应该放在应用程序服务中

  • 正确: 应用程序服务方法应该是工作单元(事务)。ABP的 工作单元 系统使这个自动完成(甚至不需要为应用服务添加 [UnitOfWork] 属性)。

  • 正确: 授权 应该在应用层完成。这里,它是通过使用 [Authorize] 属性来完成的

  • 正确: 调用支付(基础设施服务)来为该操作收费(创建组织在我们的业务中是一种付费服务)

  • 正确: 应用服务方法负责将更改保存到数据库。

  • 正确: 我们可以发送 电子邮件 通知系统管理员

  • 错误: 不要从应用程序服务返回实体。而是返回一个DTO。

讨论:为什么我们不将支付逻辑转移到领域服务中?

您可能想知道为什么支付代码不在 OrganizationManager 中。这是一件很重要的事情,我们不想错过付款

然而,仅仅重要还不足以将代码视为核心业务逻辑。 我们可能还有其他的用例,在这些用例中创建一个新的 Organization 是不需要收费的。比如:

  • 管理员用户可以使用后台应用程序创建新的组织,而无需支付任何费用

  • 后台工作的数据导入/集成/同步系统也可能需要创建没有任何支付操作的组织。

如您所见,支付不是创建有效组织的必要操作。它是特定于用例的应用程序逻辑。

示例: 增删改查 操作

public class IssueAppService
{
private readonly IssueManager _issueManager;
public IssueAppService(IssueManager issueManager)
{
_issueManager = issueManager;
} public async Task<IssueDto> GetAsync(Guid id)
{
return _issueManager.GetAsync(id);
} public async Task CreateAsync(IssueCreationDto input)
{
return _issueManager.CreateAsync(input);
} public async Task UpdateAsync(UpdateIssueDto input)
{
return _issueManager.UpdateAsync(input);
} public async Task DeleteAsync(Guid id)
{
return _issueManager.DeleteAsync(id);
}
}

这个应用程序服务本身不做任何事情,而是将所有工作委托给领域服务。它甚至将 dto 传递给 IssueManager

  • 不要仅仅为没有任何领域逻辑的简单CRUD操作创建领域服务。
  • 永远不要向领域服务传递dto或从领域服务返回dto。

应用程序服务可以直接使用存储库来查询、创建、更新或删除数据,除非在这些操作期间需要执行一些领域逻辑。在这种情况下,创建 Domain Service 方法,但只针对那些真正需要的方法

如果你对领域驱动设计和构建大型企业系统更感兴趣,推荐以下书籍作为参考书:

  • "Domain Driven Design" by Eric Evans
  • "Implementing Domain Driven Design" by Vaughn

    Vernon
  • "Clean Architecture" by Robert C. Martin

完结

实现领域驱动设计 - 使用ABP框架 - 领域逻辑 & 应用逻辑的更多相关文章

  1. 实现领域驱动设计 - 使用ABP框架 - 什么是领域驱动设计?

    前言: 最近看到ABP官网的一本电子书,感觉写的很好,翻译出来,一起学习下 (Implementing Domain Driven Design) https://abp.io/books DDD简介 ...

  2. 实现领域驱动设计 - 使用ABP框架 - 通用准则

    在进入细节之前,让我们看看一些总体的 DDD 原则 数据库提供者 / ORM 无关性 领域和应用程序层应该与 ORM / 数据库提供程序 无关.它们应该只依赖于 Repository 接口,而 Rep ...

  3. 实现领域驱动设计 - 使用ABP框架 - 解决方案概览

    .NET解决方案的分层 下图显示了使用ABP的 应用启动模板 创建的Visual Studio解决方案: 解决方案名称为问题跟踪,它由多个项目组成.通过考虑DDD原则以及开发和部署实践,该解决方案是分 ...

  4. 实现领域驱动设计 - 使用ABP框架 - 存储库

    存储库 Repository 是一个类似于集合的接口,领域层和应用程序层使用它来访问数据持久性系统(数据库),以读写业务对象(通常是聚合) 常见的存储库原则是: 在领域层定义一个存储库接口(因为它被用 ...

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

    用例演示 - 创建实体 本节将演示一些示例用例并讨论可选场景. 创建实体 从实体/聚合根类创建对象是实体生命周期的第一步.聚合/聚合根规则和最佳实践部分建议为Entity类创建一个主构造函数,以保证创 ...

  6. 基于ABP落地领域驱动设计-06.正确区分领域逻辑和应用逻辑

    目录 系列文章 领域逻辑和应用逻辑 多应用层 示例:正确区分应用逻辑和领域逻辑 学习帮助 系列文章 基于ABP落地领域驱动设计-00.目录和前言 基于ABP落地领域驱动设计-01.全景图 基于ABP落 ...

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

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

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

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

  9. 基于ABP落地领域驱动设计-00.目录和小结

    <实现领域驱动设计> -- 基于 ABP Framework 实现领域驱动设计实用指南 翻译缘由 自 ABP vNext 1.0 开始学习和使用该框架,被其优雅的设计和实现吸引,适逢 AB ...

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

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

随机推荐

  1. 阿里IM技术分享(八):深度解密钉钉即时消息服务DTIM的技术设计

    本文引用自InfoQ社区"5亿用户如何高效沟通?钉钉首次对外揭秘即时消息服务DTIM"一文,作者陈万红等.策划褚杏娟,有修订和改动. 一.引言 本文是国内企业IM的事实王者钉钉首次 ...

  2. PaperAssistant:使用Microsoft.Extensions.AI实现

    前言 上篇文章介绍了使用Semantic Kernel Chat Completion Agent实现的版本. 使用C#构建一个论文总结AI Agent 今天来介绍一下使用Microsoft.Exte ...

  3. Windows安全加固(三)

    五.网络安全配置 协议安全 1.SYN攻击保护 指定触发SYN洪水攻击保护所必须超过的TCP连接请求数阈值为5:指定处于SYN_RCVD状态的TCP连接数的阈值为500,指定处于至少已发送一次重传的S ...

  4. Dynamic CRM插件程序集中引入第三方dll合并打包

    有时候 在插件程序集不可避免的需要使用第三方的dlll 但crm插件平台注册时 只能注册一个dll 即项目自身的dll 第三方的dll无法正常在注册后使用 查找官方资料 找到如下方法 合并打包成一个d ...

  5. Huawei LiteOS基于Cortex-M4 GD32F4平台移植

    1.Huawei LiteOS简介 Huawei LiteOS源码获取: https://github.com/LiteOS/LiteOS, https://gitee.com/LiteOS/Lite ...

  6. Brainfly: 用 C# 类型系统构建 Brainfuck 编译器

    Brainfuck 简介 Brainfuck 是由 Urban Müller 在 1993 年创造的一门非常精简的图灵完备的编程语言. 正所谓大道至简,这门编程语言简单到语法只有 8 个字符,每一个字 ...

  7. Idea报错 【cannot access com.xxx】的【解决办法】

    正常操作代码,一个类突然标红,提示 cannot access com.xxx . 执行下面操作,执行完后项目变为正常 但是如果是你代码存在异常,可不是清缓存就能解决了,就要靠自己了哈哈 第一步Fil ...

  8. PHP常量与数据类型

    PHP常量与数据类型 PHP常量 在PHP中,常量是值在脚本执行期间不会改变的量.常量使用define()函数或const关键字来定义. 使用define()函数: define("GREE ...

  9. pytest自动化测试 - 我对测试用例超时处理的一点看法

    1 pytest自动化测试 - 我对测试用例超时处理的一点看法 1.1 背景   用例在执行过程中,可能由于网络等待,或者等待一些特殊的文件,而又由于一些异常,导致这些条件一直不能满足,用例卡死,这种 ...

  10. 还在手动更改SpringBoot的环境yml配置文件?老鸟带你可视化配置

    问题说明: 在SpringBoot开发时.SpringBoot的特性:'约定大于配置',我们只需要在**application.yml **配置当前的环境变量属与那个文件 比如测试环境 'applic ...