什么是领域驱动设计?
领域驱动设计(简称: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原则,同时兼顾开发和部署实践而划分。
领域层
领域层拆分为两个项目:
- IssueTracking.Domain:领域层,该项目包含所有领域层构件,比如:实体、值对象、领域服务、规约、仓储接口等。
- IssueTracking.Domain.Shared:领域共享层,包含属于领域层,但是与其他层共享的类型。举个例子:定义的常量和枚举,既在领域对象中使用,也要在其他层中使用,放在该项目中。
应用层
应用层拆分为两个项目:
- IssueTracking.Application.Contracts:应用契约层,包含应用服务接口和数据传输对象(用于接口),该项目被应用程序客户端引用,比如:WEB项目、API客户端项目。
- IssueTracking.Application:应用层,实现在 Contracts 项目中定义的接口。
展示层
- IssueTracking.Web:可执行程序,调用应用服务或APIs,当前解决方案中是 ASP.NET Core MVC/Razor Pages 应用。
远程服务层
- IssueTracking.HttpApi:远程服务层,该项目用于定义 HTTP APIs,通常包含 MVC Controller 及相关的模型。
- IssueTracking.HttpApi.Client:远程服务代理层,客户端应用程序引用该项目,将直接通过依赖注入使用远程应用服务,该项目基于ABP Framework动态C#客户端API代理系统实现。在C#项目中需要调用HTTP APIs时,会非常有用。
基础层
实现DDD时,可以使用一个基础层项目来实现所有的集成和抽象,当然也可以为不同依赖创建不同项目。
建议折中处理,为核心基础依赖创建单独项目,比如:Entity Framework Core;另外创建一个公共基础项目存放其他基础设施。
启动模板中包含两个项目对 Entity Framework Core 进行集成:
- IssueTracking.EntityFrameworkCore:EF Core核心基础依赖项目,包含:数据上下文、数据库映射、EF Core仓储实现等。
- IssueTracking.EntityFrameworkCore.DbMigrations:数据迁移项目,是一个特殊的工具项目,用于管理 Code First 数据迁移。项目中有独立的数据上下文,用于数据迁移。除了在需要创建新的数据库迁移或添加应用程序模块增加相应的表时,需要创建一个新的数据库迁移之外,通常不会涉及这个项目。
其他项目
还有一个项目,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
项目依赖于 Application
和 EntityFrameworkCore
项目,理论上不应该是这样,但实际上是这样。
这是因为 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会深深地影响你的领域对象设计(比如,实体关系)和应用层代码。假设你当前使用 Entity Framework Core 操作关系型数据库,后期希望切换为 MongoDB,这就决定你不能使用 EF Core 中独有功能,因为在MongoDB中不被支持。
举个例子:
- 不能使用更改跟踪(Change Tacking),因为 MongoDB 不支持。所以,需要显式更改实体。
- 不能在实体中使用导航属性(Navigation Properties) 或集合关联其他聚合,因为可能在文档数据库中不支持。
如果你认为这些功能对你很重要,而且你永远不会弃用 EF Core,我们认为这个原则是可以有弹性的,但是我们仍然建议使用仓储模式来隐藏基础设施的实现细节。
展示技术无关性原则
展示层技术(UI框架)是应用程序中变化最多的部分,将领域层和应用层设计成完全不知道展示层技术或框架是非常重要的。
这一原则相对容易实现,而ABP的启动模板使其更加容易实现,选择不同UI框架自动生成对应的启动模板项目。
在某些场景下,你可能需要在应用层和展示层使用相同的逻辑。举例,你可能需要在两个层中进行验证和授权。在UI层检测是为了提高用户体验,在应用层和领域层是出安全和数据有效性考虑。这是非常正常和必要的。
聚焦状态变化,而不是性能优化
DDD聚焦领域对象如何变化和如何交互;如何创建实体和改变属性,并且保持数据的完整性、有效性;如何创建方法,实现业务规则。
DDD没有考虑报表和大规模查询等需要高性能的业务场景,如果你的应用程序中没有花哨的仪表盘或报表功能,谁会去考虑呢?意思是我们需要自己考虑性能问题。
性能优化或技术选型,只要不影响到业务逻辑,可以自由使用 SQL Server 全部功能,比如:查询优化、索引、存储过程等技术;甚至使用一个其他数据源,如:ElasticSearch,来负责报表功能。
学习帮助
围绕DDD和ABP Framework两个核心技术,后面还会陆续发布核心构件实现、综合案例实现系列文章,敬请关注!
ABP Framework 研习社(QQ群:726299208)
ABP Framework 学习及实施DDD经验分享;示例源码、电子书共享,欢迎加入!