DDD的分层架构设计
Posted Mirana_77
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了DDD的分层架构设计相关的知识,希望对你有一定的参考价值。
DDD的分层架构设计
几种微服务架构模型对比分析
整洁架构
整洁架构又名“洋葱架构”。为什么叫它洋葱架构?因为整洁架构的层就像洋葱片一样,它体现了分层的设计思想。
在整洁架构里,同心圆代表应用软件的不同部分,从里到外依次是领域模型、领域服务、应用服务和最外围的容易变化的内容,比如用户界面和基础设施。 整洁架构最主要的原则是依赖原则,它定义了各层的依赖关系,越往里依赖越低,代码级别越高,越是核心能力。外圆代码依赖只能指向内圆,内圆不需要知道外圆的任何情况。
在洋葱架构中,各层的职能是这样划分的:
- 领域模型实现领域内核心业务逻辑,它封装了企业级的业务规则。领域模型的主体是实 体,一个实体可以是一个带方法的对象,也可以是一个数据结构和方法集合。
- 领域服务实现涉及多个实体的复杂业务逻辑。
- 应用服务实现与用户操作相关的服务组合与编排,它包含了应用特有的业务流程规则, 封装和实现了系统所有用例。
- 最外层主要提供适配的能力,适配能力分为主动适配和被动适配。主动适配主要实现外 部用户、网页、批处理和自动化测试等对内层业务逻辑访问适配。被动适配主要是实现核心业务逻辑对基础资源访问的适配,比如数据库、缓存、文件系统和消息中间件等。
红圈内的领域模型、领域服务和应用服务一起组成软件核心业务能力。
六边形架构
六边形架构又名“端口适配器架构”。六边形架构的核心理念是:应用是通过端口与外部进行交互的。
也就是说,在下图的六边形架构中,红圈内的核心业务逻辑(应用程序和领域模型)与外部资源(包括 APP、Web 应用以及数据库资源等)完全隔离,仅通过适配器进行交互。它解决了业务逻辑与用户界面的代码交错问题,很好地实现了前后端分离。六边形架构各层的依 赖关系与整洁架构一样,都是由外向内依赖。
六边形架构将系统分为内六边形和外六边形两层,这两层的职能划分如下:
- 红圈内的六边形实现应用的核心业务逻辑;
- 外六边形完成外部应用、驱动和基础资源等的交互和访问,对前端应用以 API 主动适配 的方式提供服务,对基础资源以依赖倒置被动适配的方式实现资源访问。
六边形架构的一个端口可能对应多个外部系统,不同的外部系统也可能会使用不同的适配器,由适配器负责协议转换。这就使得应用程序能够以一致的方式被用户、程序、自动化测 试和批处理脚本使用。
DDD 分层架构
按照职责,将架构分为了四层,用户接口层、应用层、领域层和基础层,架构图如下所示:
用户接口层
用户接口层负责向用户显示信息和解释用户指令。这里的用户可能是:用户、程序、自动化 测试和批处理脚本等等。
应用层
应用层是很薄的一层,理论上不应该有业务规则或逻辑,主要面向用例和流程相关的操作。 但应用层又位于领域层之上,因为领域层包含多个聚合,所以它可以协调多个聚合的服务和 领域对象完成服务编排和组合,协作完成业务操作。此外,应用层也是微服务之间交互的通道,它可以调用其它微服务的应用服务,完成微服务 之间的服务组合和编排。
另外,应用服务是在应用层的,它负责服务的组合、编排和转发,负责处理业务用例的执行 顺序以及结果的拼装,以粗粒度的服务通过 API 网关向前端发布。还有,应用服务还可以进行安全认证、权限校验、事务控制、发送或订阅领域事件等。
领域层
领域层的作用是实现企业核心业务逻辑,通过各种校验手段保证业务的正确性。领域层主要 体现领域模型的业务能力,它用来表达业务概念、业务状态和业务规则。
领域层包含聚合根、实体、值对象、领域服务等领域模型中的领域对象。那么这几个对象有什么关系呢?
- 首先,领域模型的业务逻辑主要是由实体和领域服务来实现的,其中实体会采用充血模型来实现所有与之相关的业务功能。
- 其次,实体和领域对象在实现业务逻辑上不是同级的,当领域中的某些功能,单一实体(或者值对象)不能实现时,领域服务就会出马,它可以组合聚合内的多个实体(或者值对象),实现复杂的业务逻辑。
基础层
基础层是贯穿所有层的,它的作用就是为其它各层提供通用的技术和基础服务,包括第三方工具、驱动、消息中间件、网关、文件、缓存以及数据库等。比较常见的功能还是提供数据库持久化。
基础层包含基础服务,它采用依赖倒置设计,封装基础资源服务,实现应用层、领域层与基础层的解耦,降低外部资源变化对应用的影响。
分层架构的原则
在《实现领域驱动设计》一书中,DDD 分层架构有一个重要的原则:每层只能与位于其下方的层发生耦合。
而架构根据耦合的紧密程度又可以分为两种:严格分层架构和松散分层架构。
- 在严格分层架构中,领域服务只能被应用服务调用,而应用服务只能被用户接口层调用,服务是逐层对外封装或组合的,依赖关系清晰。
- 而在松散分层架构中,领域服务可以同时被应用层或用户接口层调用,服务的依赖关系比较复杂且难管理,甚至容易使核心业务逻辑外泄。
DDD分层架构与三层架构
DDD 分层架构中的要素其实和三层架构类似,只是在 DDD 分层架构中,这些要素被重新 归类,重新划分了层,确定了层与层之间的交互规则和职责边界。
从图上可以看出,主要差异其实在业务逻辑层与数据访问层。
DDD 分层架构对三层架构的业务逻辑层进行了更清晰的划分,改善了三层架构核心业务逻 辑混乱,代码改动相互影响大的情况。DDD 分层架构将业务逻辑层的服务拆分到了应用层 和领域层。应用层快速响应前端的变化,领域层实现领域模型的能力。
另外一个重要的变化发生在数据访问层和基础层之间。三层架构数据访问采用 DAO 方式; DDD 分层架构的数据库等基础资源访问,采用了仓储(Repository)设计模式,通过依赖 倒置实现各层对基础资源的解耦。
仓储又分为两部分:仓储接口和仓储实现。仓储接口放在领域层中,仓储实现放在基础层。 原来三层架构通用的第三方工具包、驱动、Common、Utility、Config 等通用的公共的资 源类统一放到了基础层。
仓储模式是一种设计模式。它是在应用业务逻辑和数据层之间增加的一个抽象层,应用逻辑通过调用仓储接口的方式与数据层交互,与数据相关的实现都在仓储实现中实现,这样就可以避免在应用逻辑中混入数据相关的实现逻辑。从而就解耦了应用逻辑和数据逻辑。在基础资源 变化时不会对应用逻辑有太大的影响。
对比分析
DDD 分层架构、整洁架构、六边形架构都是以领域模型为核心,实行分层架构,内部核心业务逻辑与外部应用、资源隔离并解耦。
它们几个其实是一点一点继承和发展过来的,在大的分层上基本上没什么太大的差异, 思路基本是一致的,都是以领域模型为中心,加上用于编排的应用层逻辑。但是在分层的内部有 一些小的差异。包括外部的适配方式也有差异。
红色框内部主要实现核心业务逻辑,但核心业务逻辑也是有差异的,有的业务逻辑属于领域模型的能力,有的则属于面向用户的用例和流程编排能力。按照这种功能的差异,我们在这三种架构中划分了应用层和领域层,来承担不同的业务逻辑。
领域层实现面向领域模型,实现领域模型的核心业务逻辑,属于原子模型,它需要保持领域模型和业务逻辑的稳定,对外提供稳定的细粒度的领域服务,所以它处于架构的核心位置。
应用层实现面向用户操作相关的用例和流程,对外提供粗粒度的 API 服务。它就像一个齿轮一样进行前台应用和领域层的适配,接收前台需求,随时做出响应和调整,尽量避免将前台需求传导到领域层。应用层作为配速齿轮则位于前台应用和领域层之间。
架构模型通过分层的方式来控制需求变化从外到里对系统的影响,从外向里受需求影响逐步减小。面向用户的前端可以快速响应外部需求进行调整和发布,灵活多变,应用层通过服务组合和编排来实现业务流程的快速适配上线,减少传导到领域层的需求,使领域层保持长期 稳定。
COLA 架构
这里插个题外话,COLA是阿里巴巴自研的应用架构,我们公司自研的DDD架构(当然,现在回顾起来我们自研的架构跟人家比差的不是一点半点……后续慢慢把我个人理解的架构实践整理下)以及上面提到的DDD分层架构与COLA有很多相似之处。今天整理笔记的时候仔细看了下COLA4.0的架构图,感觉有一处很棒的设计:防腐层。COLA的架构图如下:
防腐层:简单的说,应用不要直接依赖外域的信息,要把外域的信息转换成自己领域上下文(Context)的实体再去使用,从而实现本域和外部依赖的解耦。主要是在Domain层定义Gateway接口,然后在Infrastructure提供Gateway接口的实现。
再多的细节就不说了,可以查看大佬的博客:https://blog.csdn.net/significantfrank/article/details/110934799
部分内容摘自极客《DDD实战》
DDD「领域驱动设计」分层架构初探
前言
基于 DDD 传统分层架构实现。 项目 github地址:https://github.com/WuMortal/DDDSample
这个分层架构是工作中项目正在使用的分层架构,使用了一段时间发现受益匪浅,所以整理好我对该分层架构的一些理解分享给大家,我对于该分层架构还处于学习阶段理解有误的地方请指出。本次会以一个案例来说明各个分层的作用以及他们之间的调用关系,还有本次的重点不在于DDD
,因为这个我还未能完全理解,当然避免不了中间会涉及DDD
的一些概念。
DDD 简单介绍
DDD
什么?为什么使用 DDD
?
关于这个问题有兴趣的可以自行百度,我相信网络上已经有大量的文章来说明这几个问题。我目前的理解是“业务”,是为了应对现在复杂和多变的业务,是一种开发理念。
这里我就以一个小故事描述吧,有一天你接到任务要实现一个修改用户的功能,非常简单。使用传统三层架构我们会怎么写?
-
先在
DAL
层添加UserDAL
然后实现一个Update(UserEntity user)
方法 -
接着在
BLL
中添加一个UserBLL
在实现一个Update(string email,string pwd ...)
方法。 -
UI
层在调用,OK 完成任务下班回家。
接着你接到一个新的需求就是:需要增加用户修改信息的记录。
你立马在 BLL
的 Update
的方法里增加的用户修改信息的操作记录,完成需求。
过了一段时间又来了一个需求:用户改了信息需要通知到管理员,并且用户每天只能修改 3 次信息。
好了之后又经历了几波需求,你的代码也在不断的增加和变化,有一天你接收新的项目或者离开了,那么接收你项目的人完全不清楚这里的业务情况。因为 Update
方法并没有直接的反应出里的业务情况,代码目的不明确。代码变得难以维护。
那么在 DDD
里这些应该怎么做呢?
-
首先在方法的命名上做出更改既然业务是修改信息那么命名应该是
Modify(string email,string pwd ...)
-
将用户修改信息的记录代码放在
DomainService
(领域服务) 中,当然这里的类、方法命名要直接的反应出业务情况,如:RecordUserModifyDomainService
。 -
对应的通知管理员的代码也应该放入
DomainService
中,DomainService
应该尽量简单一般只做一件事情。
分层架构图
下面是关于 DDD 分层的一些描述,摘抄至之前看过的一片文章。
Presentation 为表示层,负责向用户显示信息和解释用户命令。这里指的用户可以是另一个计算机系统,不一定是使用用户界面的人。
Application 为应用层,定义软件要完成的任务,并且指挥表达领域概念的对象来解决问题。这一层所负责的工作对业务来说意义重大,也是与其它系统的应用层进行交互的必要渠道。应用层要尽量简单,不包含业务规则或者知识,而只为下一层中的领域对象协调任务,分配工作,使它们互相协作。它没有反映业务情况的状态,但是却可以具有另外一种状态,为用户或程序显示某个任务的进度。
Domain 为领域层(或模型层),负责表达业务概念,业务状态信息以及业务规则。尽管保存业务状态的技术细节是由基础设施层实现的,但是反映业务情况的状态是由本层控制并且使用的。领域层是业务软件的核心,领域模型位于这一层。
Infrastructure 层为基础实施层,向其他层提供通用的技术能力:为应用层传递消息,为领域层提供持久化机制,为用户界面层绘制屏幕组件,等等。基础设施层还能够通过架构框架来支持四个层次间的交互模式。
说明
如上图每个层中其实对应着具体的项,下面将对每个项进行说明。
-
Domain
层分为:Domain
、DomainService
和IDomainService
。- 首先
Domain
中包含有Entity
和IRepository
,Entity
是你的实体一般对于数据库表但是在某些情况下你也可以冗余一些字段。IRepository
仓储的方法的定义,该层不会有具体的实现。 DomainService
和IDomainService
,IDomainService
只是负责表达业务的概念,DomainService
里才是具体业务逻辑代码。在这一层的代码命名上需要注意,我们的命名一般要能直接描述出该代码业务的功能。这里可以参考DDD
的几个概念:通用语言、领域。
- 首先
-
Infrastructure
层分为:Repository
和CrossCutting
:Repository
里面就是Domain
里IRepository
的具体实现。项目中 RepositoryExtensions.cs 是一个扩展类,将所有的仓储注入容器中,方便我们在项目中使用DI
(依赖注入)。CrossCutting
主要是提供一些各个层通用的东西,如一些枚举、扩展方法、工具类等等。
-
Application
层分为:Application
和ApplicationContract
。ApplicationContract
里主要包含DTO
、ViewModel
、IXXXService
。DTO
是数据传输对象,主要负责给展现层提供展示数据,DTO
里应该只有值类型存在,当然根据具体情况也可存在其他的DTO
。ViewModel
用于展现层传入的模型,简单的说DTO
输出,ViewModel
输入。IXXXService
就是应用层的方法定义。Application
里面主要是用于 实现ApplicationContract
里的IXXXService
,还有Entity
和DTO
的映射也属于该层的工作。ApplicationExtensions.cs 扩展方法是用于实现DI
。
-
Presentation
层里目前只有一个 WebAPI。展现层的代码一般有:对传入模型的校验。
案例
本次以一个用户注册的流程为案例,来简单说明如何使用该分层架构进行项目开发。
- 首先在
Domain
中建一个 UserEntity,有 Id、Mobile、Name、Age、RegisterDateTime 属性。接着建立 IUserRepository,编写需要定义的方法,这里我定义了一个 GetByMobile(string mobile) 方法。
1 [Table(Name = "User")] 2 public class UserEntity 3 { 4 [Column(IsIdentity = true)] public Guid Id { get; set; } 5 6 public string Mobile { get; set; } 7 8 public string Name { get; set; } 9 10 public int Age { get; set; } 11 12 public DateTime RegisterDateTime { get; set; } = DateTime.Now; 13 } 14 15 public interface IUserRepository : IBasicRepository<UserEntity, Guid> { Task GetByMobileAsync(string mobile); }
IBasicRepository 是使用了 FreeSql,你们可以自己实现。
- 然后在 Repository 中建 UserRepository 类,该类继承 IUserRepository 并且实现该接口的所有方法。
public class UserRepository : GuidRepository, IUserRepository { public UserRepository(IFreeSql freeSql) : base(freeSql) { } #region Implementation of IUserRepository public async Task<UserEntity> GetByMobileAsync(string mobile) { return await this.Where(u => u.Mobile == mobile).FirstAsync(); } #endregion }
- 仓储基本好了后就是
Application
,首先需要在 ApplicationContract 中建 UsesDTO,根据业务情况你也可以建 UserSimpleDTO 、UserDetailDTO。DTO
里包含你需要返回的数据,我这里有 Id、Name、Mobile、Age、ProfilePhotoSrc(头像地址根据 Id 拼接,这里我用 imgage/Id.png 的格式)。
public class UserDTO { public Guid Id { get; set; } public string Name { get; set; } public string Mobile { get; set; } public int Age { get; set; } public string ProfilePhotoSrc { get; set; } }
- 添加好 UserDTO 后,然后添加 IUserService.cs 接口,接着在 Application 的 Service 中添加对应的 UserService,并且 UserService 继承 IUserService。
public interface IUserService { /// /// 用户注册 /// ///用户名 ///手机号 ///年龄 /// Task Register(string userName, string mobile, int age); List<UserDTO> GetList(); } public class UserService : IUserService { readonly IUserRepository _userRepository; public UserService(IUserRepository userRepository) { _userRepository = userRepository; } #region Implementation of IUserService /// <summary> /// 用户注册 /// </summary> /// <param name="userName">用户名</param> /// <param name="mobile">手机</param> /// <param name="age">年龄</param> /// <returns></returns> public async Task<bool> Register(string userName, string mobile, int age) { var userEnity = await _userRepository.GetByMobileAsync(mobile); if (userEnity != null) { return false; } var addUserEntity = new UserEntity { Id = Guid.NewGuid(), Age = age, Name = userName, Mobile = mobile }; return await _userRepository.InsertAsync(addUserEntity) != null; } public List<UserDTO> GetList() { return _userRepository.Select .ToList().ToDTOList(); } #endregion }
-
UserServcie 是对应展现层的控制器 UserController ---> IUserService。
-
最后展现层的 WebAPI 只需要注入 IUserService,就可以开心的使用了。
[HttpPost] public async Task Post() { var second = DateTime.Now.Second.ToString("00"); bool isSuccess = await _userService.Register("Wigor", $"188888888{second}", 22); return Ok(isSuccess); }
就这样这个简单的案例就完成了,你可以参考着上面 说明 对比着去看看,当然这里有一些东西并没有体现,如 DomainServie,如果按照 DDD 来说还有 值对象、聚合、通用语言……,对于「通用语言」的话其实上面的小故事就体现出了一点。
结语
就 DDD 而言我这里还有很多东西都没有交代,今后有时间的话会慢慢的写出来。还有我也是在学习 DDD 所以有错的地方请指出,望多多包涵。
在使用这套分层架构的时候碰到了许多问题,这里还要感谢老大的指导,为我解答疑问。
最后附上《实现领域驱动设计》中的一句话:
我认为不管使用什么技术,我们的目的都是提供业务价值。
以上是关于DDD的分层架构设计的主要内容,如果未能解决你的问题,请参考以下文章