什么是领域驱动设计?

领域驱动设计(简称:DDD)是一种针对复杂需求的软件开发方法。将软件实现与不断发展的模型联系起来,专注于核心领域逻辑,而不是基础设施细节。DDD适用于复杂领域大规模应用,而不是简单的CRUD应用。它有助于建立一个灵活、模块化和可维护的代码库。

OOP 和 SOLID

DDD实现高度依赖面向对象编程思想(OOP)SOLID原则。实际上,实现并扩展了这些原则。因此,在真正实施DDD时,对OOP和SOLID的良好理解将对您有很大帮助。

DDD 和 Clean Architecture

一个基于领域驱动的解决方案有四个基本层:

业务逻辑分布在两个层中:领域层(Domain Layer)和 应用层(Application Layer),分别包含不同类型的业务逻辑:

  • 领域层:实现领域(或系统)中的用例独立的核心业务逻辑。
  • 应用层:基于领域的应用程序用例,应用程序用例可以看作是用户界面上的用户交互。
  • 展示层:包含应用程序UI元素(页面、组件等)。
  • 基础层:支持层,通过对第三方类库的调用或系统的抽象和集成来实现对其他层的支持。

简洁架构(Clean Architecture) 是与之相同的分层架构,又称为洋葱架构(Onion Architecture)。

从架构图可以看出,每一层只直接依赖于它内部的层,最独立的层是领域层,显示在最内圈中。

核心构件

DDD主要关注领域层和应用层,展示层和基础层被看作是细节,业务层不应该依赖于它们,但这并不意味着展示层和基础层不重要,它们也非常重要。展示层中的UI框架和基础层中的数据提供程序有他们自己的实现规则和最佳实践,需要了解和应用。然而,这些并不在DDD的主题中,我们重点来看领域层和应用层的基本构件。

领域层构件

  • 实体(Entity):一个实体是一个对象,该对象包含自己的属性和方法,属性用于存储数据和描述状态;方法结合属性实现业务逻辑。一个实体使用唯一标识(ID)来表示,两个实体对象ID不同则是为不同的实体。
  • 值对象(Value Object):值对象是另一种类型的领域对象,该对象由其属性而不是唯一ID来标识。意思是说,只有全部属性相同才会被认为是同一个对象。值对象通常被实现为不可变的,而且大多比实体简单得多。
  • 聚合和聚合根:聚合根是一个特定类型的实体,具有额外的职责。聚合是以聚合根为中心绑定在一起的一组对象,对象包括实体和值对象。
  • 仓储(接口):仓储是一个类似集合的接口,被领域层和应用层用来访问数据持久化系统(数据库)。它将数据库的复杂性从业务代码中隐藏起来。领域层包含仓储接口。
  • 领域服务:领域服务是无状态服务,实现核心领域业务规则。用于实现依赖于多个聚合(实体)或外部服务的领域逻辑。
  • 规约:用于为实体和其他业务对象定义可命名的、可重用的和可组合的过滤器。
  • 领域事件:领域事件是一种低耦合的通知方式,当一个特定的领域事件发生时,会通知其他服务。

应用层构件

  • 应用服务:应用服务是无状态服务,实现应用程序用例。一个应用服务通常获取和返回数据传输对象(DTOs),用于展示层。调用领域对象来实现用例。一个用例通常被认为是一个工作单元。
  • 数据传输对象(DTO):DTO是简单对象,不包含任何业务逻辑,只用于在应用层和展示层传递数据。
  • 工作单元:一个工作单元是一个原子工作。在工作单元中的所有操作统一提交,要么全部成功,失败则全部回滚。

实现:全景图

项目分层

下图是在 .Net解决方案(Visual Studio),基于 ABP 应用程序启动模板创建的解决方案结构:

解决方案名称为:IssueTracking 。解决方案的项目分层考虑到DDD原则,同时兼顾开发和部署实践而划分。

示例项目业务场景参考 GitHub 问题追踪,这个场景比较通用,使用过Git的开发人员都了解。

领域层

领域层拆分为两个项目:

  • IssueTracking.Domain:领域层,该项目包含所有领域层构件,比如:实体、值对象、领域服务、规约、仓储接口等。
  • IssueTracking.Domain.Shared:领域共享层,包含属于领域层,但是与其他层共享的类型。举个例子:定义的常量和枚举,既在领域对象中使用,也要在其他层中使用,放在该项目中。

应用层

应用层拆分为两个项目:

  • IssueTracking.Application.Contracts:应用契约层,包含应用服务接口和数据传输对象(用于接口),该项目被应用程序客户端引用,比如:WEB项目、API客户端项目。
  • IssueTracking.Application:应用层,实现在 Contracts 项目中定义的接口。

展示层

  • IssueTracking.Web:可执行程序,调用应用服务或APIs,当前解决方案中是 ASP.NET Core MVC/Razor Pages 应用。

ABP框架提供不同类型的UI框架,比如:Angular和Blazor。如果采用这种UI框架,解决方案为前后端分离架构,解决方案中不包含 IssueTracking.Web 项目,而是通过 IssueTracking.HttpApi.Host 项目作为一个独立的端点提供 HTTP API 服务,供客户端调用。

远程服务层

  • IssueTracking.HttpApi:远程服务层,该项目用于定义 HTTP APIs,通常包含 MVC Controller 及相关的模型。

大多数时候,API Controller 只是应用服务的包装器,以便将它们公开给远程客户端。因为ABP框架提供根据应用服务接口自动生成API Controller,实现自动配置并将你的应用服务公开为API控制器,所以通常不会在这个项目中创建控制器。

  • IssueTracking.HttpApi.Client:远程服务代理层,客户端应用程序引用该项目,将直接通过依赖注入使用远程应用服务,该项目基于ABP Framework动态C#客户端API代理系统实现。在C#项目中需要调用HTTP APIs时,会非常有用。

在解决方案的 test 文件夹中有一个控制台应用程序,名为IssueTracking.HttpApi.Client.ConsoleTestApp。它只是使用IssueTracking.HttpApi.Client项目来消费应用程序所暴露的API。它只是一个演示应用程序,可以安全地删除它。如果认为不需要,甚至可以删除IssueTracking.HttpApi.Client项目。

基础层

实现DDD时,可以使用一个基础层项目来实现所有的集成和抽象,当然也可以为不同依赖创建不同项目。

建议折中处理,为核心基础依赖创建单独项目,比如:Entity Framework Core;另外创建一个公共基础项目存放其他基础设施。

启动模板中包含两个项目对 Entity Framework Core 进行集成:

  • IssueTracking.EntityFrameworkCore:EF Core核心基础依赖项目,包含:数据上下文、数据库映射、EF Core仓储实现等。
  • IssueTracking.EntityFrameworkCore.DbMigrations:数据迁移项目,是一个特殊的工具项目,用于管理 Code First 数据迁移。项目中有独立的数据上下文,用于数据迁移。除了在需要创建新的数据库迁移或添加应用程序模块增加相应的表时,需要创建一个新的数据库迁移之外,通常不会涉及这个项目。

可能你会疑惑为什么集成EF Core创建了两个项目,因为模块化的需要。每一个模块有其独立的 DbContext,应用程序也有一个 DbContextDbMigration项目包含用于跟踪和应用单个迁移模块的联合。虽然大多数时候您不需要了解它,但您可以查看 EF Core迁移文档,以获得更多信息。

其他项目

还有一个项目,IssueTracking.DbMigrator,一个简单的控制台应用程序,当你执行它时,会迁移数据库结构并初始化种子数据。这是一个有用的实用程序,可以在开发和生产环境中使用它。

项目依赖关系

下图是解决方案中项目引用(依赖)关系

前面我们讲解了各个项目的作用,接下来梳理项目之前的关系:

  • Domain.Shared 其他项目直接或间接引用,项目中定义的类型在所有项目中共享。
  • Domain 只引用 Domain.Shared,比如:在 Domain.Shared 中定义的 IssuType 枚举类型需要在 Domain 项目中 Issue 实体中用到。
  • Application.Contracts 依赖 Domain.Shared,这样我们可以在 DTOs 中使用这些共享类型。比如:CreateIssueDto中可以直接使用 IssueType 枚举。
  • Application 依赖 Application.Contracts ,因为 Application 实现 Application.Contracts 中定义的服务接口和使用 DTO 对象。同时,引用 Domain 项目,在应用服务中使用仓储接口领域对象
  • EntiryFrameworkCore 依赖 Domain ,映射 Domain 对象(实体和值类型)到数据库表(ORM)并实现在 Domain 中定义的仓储接口。
  • HttpApi 依赖 Application.Contract,在控制器在内部对 应用服务接口 进行依赖注入。
  • HttpApi.Client 依赖 Application.Contract 消费应用服务
  • Web 依赖 HttpApi ,发布里面定义的HTTP APIs。另外,通过这种方式,它间接地依赖于 Application.Contracts 项目,可以在页面/组件中使用应用服务。

虚拟依赖

当你仔细查看解决方案依赖关系图时,会看到还有两个依赖关系,在上图中用虚线表示。Web项目依赖于 ApplicationEntityFrameworkCore 项目,理论上不应该是这样,但实际上是这样。

这是因为 Web 是运行和托管应用程序的最终项目,应用程序在运行时需要应用服务和仓储的实现

这个设计决定有可能让你在展示层中使用实体EF Core 对象,但这应该是严格避免的。然而,我们发现替代设计过于复杂。在这里,如果你想消除这种依赖性,有两个备选方案:

  • 将 Web 项目转换为 Razor类库类型,然后创建新项目,比如:Web.Host,引用 Web 项目、Application 和 EntityFrameworkCore 项目。在新项目中,不需要编写任何UI代码,只用来做承载项目。
  • 从 Web 项目中移除 Application 和 EntityFrameworkCore 项目引用,作为 ABP 插件模块在应用初始化时加载程序集。

DDD应用程序的执行流程

下图显示基于DDD模式开发的Web应用请求的基本流程:

  • 通过UI用户交互(可以看做是一个用例)发起HTTP请求到服务器
  • 在展示层 MVC Controller(HTTP API) 或 Razor Page Handler(Razor Pages)接收并处理请求,在此阶段执行横切关注点,如:授权、输入验证、异常处理、审计日志、缓存等。Controller或Page在构造函数中注入应用服务接口,调用方法发送和接收DTO对象。
  • 应用服务使用领域对象(实体、仓储接口、领域服务等)实现用例。在此阶段,应用层执行横切关注点,如:授权、验证、审计日志、工作单元等。一个应用服务方法是一个工作单元,具有原子性。

大多数横切关注点在ABP框架中自动实现或按照约定实现,无需额外编写代码。

通用原则

在进入DDD之前,让我们梳理下DDD通用原则。

数据库(Database Provider / ORM)独立性原则

领域层和应用层不知道项目中使用的 ORM 和 Database Provider。只依赖于仓储接口,并且仓储接口不适合使用用任何 ORM 特殊对象

这一原则的主要原因是:

  1. 使领域层和应用层与基础层独立,因为基础层将来可能更改,或者你可能需要支持其他类型数据库。
  2. 使领域和应用聚焦在业务代码上,通过将基础设施实现细节隐藏于仓储之后,使您的领域和应用服务专注于业务代码。
  3. 易于自动化测试,因为可以通过仓储接口模拟仓储数据。

根据这一原则,除启动应用程序外,解决方案中的任何项目都没有引用 EntityFrameworkCore 项目。

关于数据库独立性原则的讨论

尤其是原因1会深深地影响你的领域对象设计(比如,实体关系)和应用层代码。假设你当前使用 Entity Framework Core 操作关系型数据库,后期希望切换为 MongoDB,这就决定你不能使用 EF Core 中独有功能,因为在MongoDB中不被支持。

举个例子:

  • 不能使用更改跟踪(Change Tacking),因为 MongoDB 不支持。所以,需要显式更改实体。
  • 不能在实体中使用导航属性(Navigation Properties) 或集合关联其他聚合,因为可能在文档数据库中不支持。

那么如何解决实体关联的问题?记住规则:仅通过Id引用其他聚合。

如果你认为这些功能对你很重要,而且你永远不会弃用 EF Core,我们认为这个原则是可以有弹性的,但是我们仍然建议使用仓储模式来隐藏基础设施的实现细节。

ABP Framework 为仓储接口 IRepository 提供获取 IQueryable 对象的扩展方法 GetQueryableAsync(),使我们在使用仓储时可以直接使用标准LINQ扩展方法

展示技术无关性原则

展示层技术(UI框架)是应用程序中变化最多的部分,将领域层和应用层设计成完全不知道展示层技术或框架是非常重要的。

这一原则相对容易实现,而ABP的启动模板使其更加容易实现,选择不同UI框架自动生成对应的启动模板项目。

在某些场景下,你可能需要在应用层和展示层使用相同的逻辑。举例,你可能需要在两个层中进行验证和授权。在UI层检测是为了提高用户体验,在应用层和领域层是出安全和数据有效性考虑。这是非常正常和必要的。

聚焦状态变化,而不是性能优化

DDD聚焦领域对象如何变化和如何交互;如何创建实体和改变属性,并且保持数据的完整性、有效性;如何创建方法,实现业务规则。

DDD没有考虑报表大规模查询等需要高性能的业务场景,如果你的应用程序中没有花哨的仪表盘或报表功能,谁会去考虑呢?意思是我们需要自己考虑性能问题。

性能优化或技术选型,只要不影响到业务逻辑,可以自由使用 SQL Server 全部功能,比如:查询优化、索引、存储过程等技术;甚至使用一个其他数据源,如:ElasticSearch,来负责报表功能。

学习帮助

围绕DDDABP Framework两个核心技术,后面还会陆续发布核心构件实现综合案例实现系列文章,敬请关注!

ABP Framework 研习社(QQ群:726299208)

ABP Framework 学习及实施DDD经验分享;示例源码、电子书共享,欢迎加入!

基于ABP落地领域驱动设计-01.全景图的更多相关文章

  1. 基于ABP落地领域驱动设计-02.聚合和聚合根的最佳实践和原则

    目录 前言 聚合 聚合和聚合根原则 包含业务原则 单个单元原则 事务边界原则 可序列化原则 聚合和聚合根最佳实践 只通过ID引用其他聚合 用于 EF Core 和 关系型数据库 保持聚合根足够小 聚合 ...

  2. 基于ABP落地领域驱动设计-03.仓储和规约最佳实践和原则

    目录 系列文章 仓储 仓储的通用原则 仓储中不包含领域逻辑 规约 在实体中使用规约 在仓储中使用规约 组合规约 学习帮助 围绕DDD和ABP Framework两个核心技术,后面还会陆续发布核心构件实 ...

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

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

  4. 基于ABP落地领域驱动设计-04.领域服务和应用服务的最佳实践和原则

    目录 系列文章 领域服务 应用服务 学习帮助 系列文章 基于ABP落地领域驱动设计-00.目录和前言 基于ABP落地领域驱动设计-01.全景图 基于ABP落地领域驱动设计-02.聚合和聚合根的最佳实践 ...

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

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

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

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

  7. Java开发架构篇《初识领域驱动设计DDD落地》

    作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 DDD(Domain-Driven Design 领域驱动设计)是由Eric Eva ...

  8. 基于领域驱动设计(DDD)超轻量级快速开发架构

    smartadmin.core.urf 这个项目是基于asp.net core 3.1(最新)基础上参照领域驱动设计(DDD)的理念,并参考目前最为了流行的abp架构开发的一套轻量级的快速开发web ...

  9. 基于领域驱动设计(DDD)超轻量级快速开发架构(二)动态linq查询的实现方式

    -之动态查询,查询逻辑封装复用 基于领域驱动设计(DDD)超轻量级快速开发架构详细介绍请看 https://www.cnblogs.com/neozhu/p/13174234.html 需求 配合Ea ...

随机推荐

  1. linux 查看运行java所在目录

    通过ps及top命令查看进程信息时,只能查到相对路径,查不到的进程的详细信息 需要查看pos_service.jar的绝对路径(在哪里目录下)  使用:ll /proc/PID Linux在启动一个进 ...

  2. Linux系统运行netstat命令时的过三关斩一将

    1.简介 这篇文章主要是记录在日常工作中遇到的一些问题,将其都总结整合到一起,方便查看,提高工作效率.小伙伴们看到标题可能觉得奇怪,不是过五关斩六将而是过三关斩一将.慢慢地往后看发现其中奥秘. 2.过 ...

  3. Python数模笔记-Sklearn(4)线性回归

    1.什么是线性回归? 回归分析(Regression analysis)是一种统计分析方法,研究自变量和因变量之间的定量关系.回归分析不仅包括建立数学模型并估计模型参数,检验数学模型的可信度,也包括利 ...

  4. KeUserModeCallback函数

    内核调用用户 正常的系统调用过程为Ring3->Ring0->Ring3,而KeUserModeCallback提供了一种Ring0->Ring3->Ring0的方式,即从内核 ...

  5. 【开源技术分享】无需流媒体服务,让浏览器直接播放rtsp/rtmp的神器:EasyMedia

    不同于市面上其他需要各种转发到流媒体服务的中间件来说,EasyMedia不需要依赖任何nginx-rtmp,srs,zlmediakit等等第三方流媒体服务,只需要你有rtsp或者rtmp等等协议的视 ...

  6. 在 Apache 上使用网络安全服务(NSS)实现 HTTPS--RHCE 系列(八)

        在 Apache 上使用网络安全服务(NSS)实现 HTTPS--RHCE 系列(八) 发布:linux培训 来源:Linux认证 时间:2015-12-21 15:26 分享到: 达内lin ...

  7. 二、Python流程控制练习题

    一.分支结构-if等 练习题: 练习1:英制单位与公制单位互换 练习2:掷骰子决定做什么 练习3:百分制成绩转等级制 练习4:输入三条边长如果能构成三角形就计算周长和面积 练习5:个人所得税计算器 练 ...

  8. Linux中级之负载均衡(lvs,nginx,haproxy)、中间件

    一.负载均衡的概念 1.系统的扩展方式: scale up:向上扩展 scale out:向外扩展 2.集群类型:  LB(Load Balancing).HA(high availability) ...

  9. kotlin知识点

    主构造函数里的参数,如果不声明为var或者val,则这个参数一般是用来初始化父类.它不算是这个类的字段,它的作用域只在主构造函数当中. val 的对象不仅数据不能变, 引用也不能变. //自定义的类似 ...

  10. Navicat Premium 15 linux 安装与激活 ArchLinux 2021

    查看了很多教程花了半小时才弄好可真不容易 参考https://github.com/orginly/navicat-keygen 下载地址 http://www.navicat.com.cn/down ...