三、Abp vNext 基础篇丨分层架构
介绍
本章节对 ABP 框架进行一个简单的介绍,摘自ABP官方,后面会在使用过程中对各个知识点进行细致的讲解。
领域驱动设计
领域驱动设计(简称:DDD)是一种针对复杂需求的软件开发方法。将软件实现与不断发展的模型联系起来,专注于核心领域逻辑,而不是基础设施细节。DDD适用于复杂领域和大规模应用,而不是简单的CRUD应用。它有助于建立一个灵活、模块化和可维护的代码库。
一个基于领域驱动的解决方案有四个基本层:
领域层:实现领域(或系统)中的用例独立的核心业务逻辑。
应用层:基于领域的应用程序用例,应用程序用例可以看作是用户界面上的用户交互。
展示层:包含应用程序UI元素(页面、组件等)。
基础层:支持层,通过对第三方类库的调用或系统的抽象和集成来实现对其他层的支持。
核心构件
DDD主要关注领域层和应用层,展示层和基础层被看作是细节,业务层不应该依赖于它们,但这并不意味着展示层和基础层不重要,它们也非常重要。展示层中的UI框架和基础层中的数据提供程序有他们自己的实现规则和最佳实践,需要了解和应用。然而,这些并不在DDD的主题中,我们重点来看领域层和应用层的基本构件。
领域层构件
实体(Entity):一个实体是一个对象,该对象包含自己的属性和方法,属性用于存储数据和描述状态;方法结合属性实现业务逻辑。一个实体使用唯一标识(ID)来表示,两个实体对象ID不同则是为不同的实体。
值对象(Value Object):值对象是另一种类型的领域对象,该对象由其属性而不是唯一ID来标识。意思是说,只有全部属性相同才会被认为是同一个对象。值对象通常被实现为不可变的,而且大多比实体简单得多。
聚合和聚合根:聚合根是一个特定类型的实体,具有额外的职责。聚合是以聚合根为中心绑定在一起的一组对象,对象包括实体和值对象。
仓储(接口):仓储是一个类似集合的接口,被领域层和应用层用来访问数据持久化系统(数据库)。它将数据库的复杂性从业务代码中隐藏起来。领域层包含仓储接口。
领域服务:领域服务是无状态服务,实现核心领域业务规则。用于实现依赖于多个聚合(实体)或外部服务的领域逻辑。
规约:用于为实体和其他业务对象定义可命名的、可重用的和可组合的过滤器。
领域事件:领域事件是一种低耦合的通知方式,当一个特定的领域事件发生时,会通知其他服务。
应用层构件
应用服务:应用服务是无状态服务,实现应用程序用例。一个应用服务通常获取和返回数据传输对象(DTOs),用于展示层。调用领域对象来实现用例。一个用例通常被认为是一个工作单元。
数据传输对象(DTO):DTO是简单对象,不包含任何业务逻辑,只用于在应用层和展示层传递数据。
工作单元:一个工作单元是一个原子工作。在工作单元中的所有操作统一提交,要么全部成功,失败则全部回滚。
ABP项目分层解析
领域层
领域层拆分为两个项目:
Bcvp.Blog.Core.Domain:领域层,该项目包含所有领域层构件,比如:实体、值对象、领域服务、规约、仓储接口等。
Bcvp.Blog.Core.Domain.Shared:领域共享层,包含属于领域层,但是与其他层共享的类型。举个例子:定义的常量和枚举,既在领域对象中使用,也要在其他层中使用,放在该项目中。
应用层
应用层拆分为两个项目:
Bcvp.Blog.Core.Application.Contracts:应用契约层,包含应用服务接口和数据传输对象(用于接口),该项目被应用程序客户端引用,比如:WEB项目、API客户端项目。
Bcvp.Blog.Core.Application:应用层,实现在 Contracts 项目中定义的接口。
展示层
Bcvp.Blog.Core.HttpApi.Host 项目作为一个独立的端点提供 HTTP API 服务,供客户端调用。
远程服务层
Bcvp.Blog.Core.HttpApi:远程服务层,该项目用于定义 HTTP APIs,通常包含 MVC Controller 及相关的模型。
Bcvp.Blog.Core.HttpApi.Client:远程服务代理层,客户端应用程序引用该项目,将直接通过依赖注入使用远程应用服务,该项目基于ABP Framework动态C#客户端API代理系统实现。在C#项目中需要调用HTTP APIs时,会非常有用。
基础层
实现DDD时,可以使用一个基础层项目来实现所有的集成和抽象,当然也可以为不同依赖创建不同项目。
建议折中处理,为核心基础依赖创建单独项目,比如:Entity Framework Core;另外创建一个公共基础项目存放其他基础设施。
启动模板中包含两个项目对 Entity Framework Core 进行集成:
Bcvp.Blog.Core.EntityFrameworkCore:EF Core核心基础依赖项目,包含:数据上下文、数据库映射、EF Core仓储实现等。
其他项目
还有一个项目 Bcvp.Blog.Core.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 项⽬,可以在⻚⾯/组件中使⽤应⽤服务
DDD通用原则
在正式开始之前我们在梳理一下DDD的通用原则。
数据库(Database Provider / ORM)独⽴性原则
领域层和应⽤层不知道项⽬中使⽤的 ORM 和 Database Provider。只依赖于仓储接⼝,并且仓储接⼝ 不适合使⽤⽤任何 ORM 特殊对象
这⼀原则的主要原因是:
-
使领域层和应⽤层与基础层独⽴,因为基础层将来可能更改,或者你可能需要⽀持其他类型数据库。
-
使领域和应⽤聚焦在业务代码上,通过将基础设施实现细节隐藏于仓储之后,使您的领域和应⽤服 务专注于业务代码。
-
易于⾃动化测试,因为可以通过仓储接⼝模拟仓储数据。
关于数据库独⽴性原则的讨论
假设你当前使⽤ 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 全部功能。
结语
本节知识点:
- 1.讲解Abp和DDD的分层架构介绍
- 2.很重要的知识点DDD聚焦状态变化而非性能优化
联系作者:加群:867095512 @MrChuJiu