掘金 后端 ( ) • 2024-04-28 17:42

走上DDD

还记得之前提到的代码大改版吗?说的就是从mvc架构改版成DDD架构。

代码都写完了,是什么原因让我们再次走上了重构之路呢?

一般来说,在项目初期应该选好架构,然后再进入到开发阶段,到了项目后期,如果有改动会有delay的风险,让我们再次开启重构之路的原因是

一切都缘于我们追求极致的简洁,也许我们的老毛病又犯了(白眼)。在使用mvc架构的过程中我们一直在反复的思考,主要有以下几点

  1. 如何让产品经理看得懂我们的代码,无论是从包的命名、还是类的命名,乃至函数的命名都能准确无误的表达想要做的事情。
  2. 核心业务如何摆脱对springboot框架的依赖,我们认为好的代码不应该轻易依赖第三方框架。举例,我们的service能不能不用@service标记,依然可以工作。
  3. 怎么样才能把代码写的更加的清晰、简洁、饱满

在这个过程中,我们重读了 《重构:改善现有代码的设计》,虽然目前代码写的应该还可以,但我们追求的是把代码写的更加的规范。

于是我们走上了领域驱动之路,在过去的经历中,我对领域驱动的落地总是望而生畏,因为确实没有优秀的项目来指导如何落地,包括Eric evans他老人家都不知道,他的回答是 By experience。

为了能够搞清楚领域驱动,我读了一些文章,另外还读了两本书

《领域驱动设计 软件核心复杂性应对之道》Eric evans

《解构领域驱动设计》张逸

DDD工程结构

接下来直接给你看一下我们整体的包结构,从包结构你能看出来领域驱动的味道

│  ├─src
│  │  ├─main
│  │  │  ├─java
│  │  │  │  └─com
│  │  │  │      └─viens
│  │  │  │          └─award
│  │  │  │              ├─application 
│  │  │  │              │  ├─controller
│  │  │  │              │  ├─convert
│  │  │  │              │  ├─request
│  │  │  │              │  └─response
│  │  │  │              │  └─mq
│  │  │  │              │  └─task
│  │  │  │              ├─domain
│  │  │  │              │  ├─model
│  │  │  │              │  ├─port
│  │  │  │              │  ├─service
│  │  │  │              │  └─setter
│  │  │  │              └─infrastructure
│  │  │  │                  ├─configuration
│  │  │  │                  │  ├─exception
│  │  │  │                  │  ├─filter
│  │  │  │                  │  ├─limiter
│  │  │  │                  │  ├─message
│  │  │  │                  │  │  ├─canal
│  │  │  │                  │  │  └─kafka
│  │  │  │                  │  ├─monitor
│  │  │  │                  │  ├─scenes
│  │  │  │                  │  └─springbean
│  │  │  │                  └─repository
│  │  │  │                      └─convert

application

该层的主要职责是协议转换和数据转换。协议转换有commandLine、mq消息协议、http协议等;数据转换主要指的是把请求实体转换成核心领域的entity。

我们在这一层做了对canal消息的监听,提供Restful API 接口。

├─application 
│  ├─controller //控制器,跟mvc的控制器使用方式是一样的
│  ├─convert //对象转换
│  ├─request // 请求实体
│  └─response //响应实体
│  └─mq //消息的监听,消息转换器
│  └─task //系统执行任务,比如定时任务

domain

领域核心层,主要核心业务。比如我们奖券的分配规则等,接下来详细解释一下domain层的各个包命名,该层不依赖包括springboot的框架,那你可能有疑问,我如何把service注入到spring beans 容器中,后边我们说。


├─domain
│  ├─model // entity和值对象,怎么来区分entity和value呢,看看有没有唯一标识,如果有大概率是entity,比如person,有身份id,是实体。住址是值对象,比如住址。
│  ├─port //该包的主要作用是给核心业务提供访问第三方入口,具体是service->port
│  ├─service //具体业务实现
│  └─setter

infrastructure

基础设施层,该层主要是具体的实现方案,比如数据库操作,kafka操作,springboot框架加载等。

└─infrastructure 
 ├─configuration 
  ├─exception //全局异常
  ├─filter //过滤器
  ├─limiter //限流
  ├─message 
    ├─canal //canal消息
    └─kafka //kafka消息
  ├─monitor //监控
  ├─scenes //
  └─springbean //springbean操作
└─repository //资源库
 └─convert 

DDD核心概念

统一语言:

在表述同一件事情的时候用相同的词汇,主要为了解决语义歧义,减少沟通成本,相关的词汇可以直接反映到代码层面,比如大家在定义商品的时候,有人说goods,有人说commodity,这样就不容易沟通。

限界上下文:

领域所处环境和边界,所处环境是指事情发生的当时情景。比如商品,在销售阶段叫商品,在运输阶段叫货品,商品就是内容,销售阶段是当时的环境

聚合根:

把不同实体以及行为聚合在一起,如果要访问聚合内的实体行为,只能通过聚合根

实体:

有唯一标识的就是实体,比如person,通过身份证来识别。当然很多时候实体会发生转变成值对象

值对象:

不能够通过唯一标识来识别,比如住址。

领域、核心域

最核心的业务是什么

支撑域

用于支撑业务的一些基础设施,可以理解成infrastructure层

通用域

通用的技术工具,比如各种utils

DDD最佳实践

eric evans提到的六边形架构

六边形架构

image.png

该图主要是domain层的一些介绍,最核心的是model,最外边是我们的port,刚才从我们的代码结构中也看到了。

整洁架构

image.png

https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html

洋葱架构

image.png

https://medium.com/expedia-group-tech/onion-architecture-deed8a554423

这三种架构都是DDD实践的推荐架构,整体上都差不多,其他层都依赖于核心层,核心层不允许依赖其他层,这是核心理念。

这样做有什么好处呢?

  1. 可以很好的进行单元测试,核心层的自洽正好给单元测试提供了土壤
  2. 可以更好的保持稳定,基础层和应用层的改变,不会影响到核心层的变动。

MVC和DDD该如何选择

MVC的分层架构已经成为经典,也是我们最常用的架构模式,如此经典的架构很显然有它的用武之地。 mvc在架构层次上有简单,清晰的特点,也比较容易上手,对待一些简单的业务场景,开发效率非常好,但对于复杂的业务场景会很容易堆砌代码,形成大函数的风格,所以不是很复杂的业务场景建议用mvc。

DDD的充血模型为复杂的业务提供了良好的土壤,让复杂的操作更内聚,封装性更好,易于扩展,但对开发人员来说上手成本比较高,而且不是很容易学习。

总结

任何架构都有优点和缺点,没有任何一个是完美的,在技术选型上重要的是不追究奢华,不过分追求时尚,不过分追求华丽,而是按照自己的业务场景选择最适合自己的技术,无论是从学习成本、开发效率、质量稳定性上适合自己就好。