当前位置 博文首页 > iEricLee:基于ABP落地领域驱动设计-01.全景图
领域驱动设计(简称:DDD)是一种针对复杂需求的软件开发方法。将软件实现与不断发展的模型联系起来,专注于核心领域逻辑,而不是基础设施细节。DDD适用于复杂领域和大规模应用,而不是简单的CRUD应用。它有助于建立一个灵活、模块化和可维护的代码库。
DDD实现高度依赖面向对象编程思想(OOP)和SOLID原则。实际上,实现并扩展了这些原则。因此,在真正实施DDD时,对OOP和SOLID的良好理解将对您有很大帮助。
一个基于领域驱动的解决方案有四个基本层:
业务逻辑分布在两个层中:领域层(Domain Layer)和 应用层(Application Layer),分别包含不同类型的业务逻辑:
简洁架构(Clean Architecture) 是与之相同的分层架构,又称为洋葱架构(Onion Architecture)。
从架构图可以看出,每一层只直接依赖于它内部的层,最独立的层是领域层,显示在最内圈中。
DDD主要关注领域层和应用层,展示层和基础层被看作是细节,业务层不应该依赖于它们,但这并不意味着展示层和基础层不重要,它们也非常重要。展示层中的UI框架和基础层中的数据提供程序有他们自己的实现规则和最佳实践,需要了解和应用。然而,这些并不在DDD的主题中,我们重点来看领域层和应用层的基本构件。
下图是在 .Net解决方案(Visual Studio),基于 ABP 应用程序启动模板创建的解决方案结构:
解决方案名称为:IssueTracking
。解决方案的项目分层考虑到DDD原则,同时兼顾开发和部署实践而划分。
示例项目业务场景参考 GitHub 问题追踪,这个场景比较通用,使用过Git的开发人员都了解。
领域层
领域层拆分为两个项目:
应用层
应用层拆分为两个项目:
展示层
ABP框架提供不同类型的UI框架,比如:Angular和Blazor。如果采用这种UI框架,解决方案为前后端分离架构,解决方案中不包含 IssueTracking.Web 项目,而是通过 IssueTracking.HttpApi.Host 项目作为一个独立的端点提供 HTTP API 服务,供客户端调用。
远程服务层
大多数时候,API Controller 只是应用服务的包装器,以便将它们公开给远程客户端。因为ABP框架提供根据应用服务接口自动生成API Controller,实现自动配置并将你的应用服务公开为API控制器,所以通常不会在这个项目中创建控制器。
在解决方案的
test
文件夹中有一个控制台应用程序,名为IssueTracking.HttpApi.Client.ConsoleTestApp
。它只是使用IssueTracking.HttpApi.Client
项目来消费应用程序所暴露的API。它只是一个演示应用程序,可以安全地删除它。如果认为不需要,甚至可以删除IssueTracking.HttpApi.Client
项目。
基础层
实现DDD时,可以使用一个基础层项目来实现所有的集成和抽象,当然也可以为不同依赖创建不同项目。
建议折中处理,为核心基础依赖创建单独项目,比如:Entity Framework Core;另外创建一个公共基础项目存放其他基础设施。
启动模板中包含两个项目对 Entity Framework Core 进行集成:
可能你会疑惑为什么集成EF Core创建了两个项目,因为模块化的需要。每一个模块有其独立的
DbContext
,应用程序也有一个DbContext
。DbMigration
项目包含用于跟踪和应用单个迁移模块的联合。虽然大多数时候您不需要了解它,但您可以查看 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
项目依赖于 Application
和 EntityFrameworkCore
项目,理论上不应该是这样,但实际上是这样。
这是因为 Web
是运行和托管应用程序的最终项目,应用程序在运行时需要应用服务和仓储的实现。
这个设计决定有可能让你在展示层中使用实体和EF Core 对象,但这应该是严格避免的。然而,我们发现替代设计过于复杂。在这里,如果你想消除这种依赖性,有两个备选方案:
下图显示基于DDD模式开发的Web应用请求的基本流程:
大多数横切关注点在ABP框架中自动实现或按照约定实现,无需额外编写代码。
在进入DDD之前,让我们梳理下DDD通用原则。
数据库(Database Provider / ORM)独立性原则
领域层和应用层不知道项目中使用的 ORM 和 Database Provider。只依赖于仓储接口,并且仓储接口不适合使用用任何 ORM 特殊对象。
这一原则的主要原因是:
根据这一原则,除启动应用程序外,解决方案中的任何项目都没有引用
EntityFrameworkCore
项目。
关于数据库独立性原则的讨论
尤其是原因1会深深地影响你的领域对象设计(比如,实体关系)和应用层代码。假设你当前使用 Entity Framework Core 操作关系型数据库,后期希望切换为 MongoDB,这就决定你不能使用 EF Core 中独有功能,因为在MongoDB中不被支持。
举个例子:
那么如何解决实体关联的问题?记住规则:仅通过Id引用其他聚合。
如果你认为这些功能对你很重要,而且你永远不会弃用 EF Core,我们认为这个原则是可以有弹性的,但是我们仍然建议使用仓储模式来隐藏基础设施的实现细节。
ABP Framework 为仓储接口
IRepository
提供获取IQueryable
对象的扩展方法GetQueryableAsync()
,使我们在使用仓储时可以直接使用标准LINQ扩展方法。
展示技术无关性原则
展示层技术(UI框架)是应用程序中变化最多的部分,将领域层和应用层设计成完全不知道展示层技术或框架是非常重要的。
这一原则相对容易实现,而ABP的启动模板使其更加容易实现,选择不同UI框架自动生成对应的启动模板项目。
在某些场景下,你可能需要在应用层和展示层使用相同的逻辑。举例,你可能需要在两个层中进行验证和授权。在UI层检测是为了提高用户体验,在应用层和领域层是出安全和数据有效性考虑。这是非常正常和必要的。
聚焦状态变化,而不是性能优化
DDD聚焦领域对象如何变化和如何交互;如何创建实体和改变属性,并且保持数据的完整性、有效性;如何创建方法,实现业务规则。
DDD没有考虑报表和大规模查询等需要高性能的业务场景,如果你的应用程序中没有花哨的仪表盘或报表功能,谁会去考虑呢?意思是我们需要自己考虑性能问题。
性能优化或技术选型,只要不影响到业务逻辑,可以自由使用 SQL Server 全部功能,比如:查询优化、索引、存储过程等技术;甚至使用一个其他数据源,如:ElasticSearch,来负责报表功能。
围绕DDD和ABP Framework两个核心技术,后面还会陆续发布核心构件实现、综合案例实现系列文章,敬请关注!
ABP Framework 研习社(QQ群:726299208)
ABP Framework 学习及实施DDD经验分享;示例源码、电子书共享,欢迎加入!