漫谈领域驱动设计[读书笔记]
前言
- 领域驱动设计,其实是一种概念模型。它的英文缩写是DDD,其在刚刚出生的时候,没有很大的应用,但是当马丁弗勒提出微服务思想的时候,其大显身手。
- 在聊DDD之前,我们需要说一下几个代码实体设计模型
贫血模型
- 我们都是Java开发,大家都知道Java是一门面向对象的语言。
- 但是很遗憾的是,在我们自学的过程中,和网上的教程,Java面向对象的功能,只有在学习JavaSE基础的时候用到了,其大部分的功能【Java对象】,都已经转接到所谓的Service层,而Java的实体,或者是VO、DTO、BO等对象的功能,仅仅只是数据的载体,其仅仅对数据进行少量的操作——数据格式转换、数据格式校验。
- 但是我们仍然可以记得起来,我们当时学习面向对象的时候,如一个User。它有自己的名字、密码、id、住址、手机号等等其他的信息。当一个User对象需要修改信息的时候,只需要调用其内部编写的方法就可完成对象的数据修改。
- 但是在我们学习的过程中,我们发现,很多的时候,User对象【或者是其他类型的对象】只是数据的载体。绝大部分的对实体的操作,都被放在了Service进行操作。实体只是和数据库表进行映射、数据传输、数据转换。而这就把面向对象的特性,玩成了面向过程。何为面向过程——就是典型的三层架构、四层架构:Controller调用Service、Service调用DAO,DAO查询数据库,返回数据对象,然后向上传递。关于对象的操作,其没有自己的实现。也就是说,对象本身没有对它自己操作的权限,其把权力交给Service了,这就是所谓的贫血模型。实体对象的功能被无限缩小。
- Java面向对象的语言,最后却像面向过程一样,用户自己不能操作自己,而让被人操作自己,这样的耦合度就很高。别人对自己做了什么,自己完全没有知情权。这是不是有些恐怖?
充血模型
- 而贫血模型,其对应的是充血模型。
- 充血模型,是一种和贫血模型对应的。充血模型,每个对象都可以有自己的字段,也有自己的方法,也就是面向对象中的自己操作自己。在这种模型下,对象有对自己的知情权,其可以对自己进行操作,这就不需要别人去操作对象他自己。
- 充血模型是把对象和其业务属性封装到一起,这个时候其不再是对象的数据载体,也是对象的业务载体。充血模型满足面向对象的封装特性,而不似贫血模型这样面向过程一样的。
贫血模型和充血模型的比较
- 贫血模型,类面向过程,在三层架构和四层架构中,其贫血对象[Entity、BO、PO]只是数据的载体,只是包含部分简单的格式化、数据转换操作。其核心的功能由外部对象Service完成,也就是其本身的业务功能交由别人去做。固定的思维,使用Controller调用Service,Service调用DAO。破坏了封装,不是太符合单一职责原则。
- 充血模型,面向对象。在贫血模型中的对象,他们不仅仅是数据的载体,他们也是自己数据的操作者,一个充血对象可以对自己进行增删改查的操作,而非Service去调用其功能实现,在这种情况下,Service的”中央集权“被削弱,部分业务功能转移到充血对象。而Service的功能被拆分,完成了转移。
- 优点和缺点:贫血模型相对比较简单,其实体不需要关心任何的操作,他们只需要承载数据即可,Service层手握大权,全部的功能都是通过Service来完成的。充血模型相对贫血模型来说,设计的难度和复杂度会更高,因为需要考虑一个模型对象,到底需要由哪些行为方法来操作对象,因为Service的业务部分转移,所以如果充血模型对象设计的不合理,那么要么修改充血模型对象[这就可能会导致修改上层代码]、要么就在Service层增加方法,而这个时候,就会变成非贫血模型和贫血模型混杂的情况,整个系统的代码逻辑就会很拉跨,也就是四不像的代码,维护起来很复杂。
何为DDD
- DDD英文全名是:Domain-Driven Design 。领域驱动设计
- 那么我们为什么需要选择DDD呢?先复习一下大学知识。
- 计算机专业的童鞋都应该学过这样的一门学科《软件工程导论》,其中包含多种软件设计方法,比如瀑布流、迭代、螺旋、敏捷,在其中,我印象最深刻的就是,无论是那种开发模型,一个稳定的系统、厉害的系统,其的设计都是高内聚、低耦合的。也就是说,我一个内聚,就可以完成我自己的很多的功能,我不需要依赖别人[耦合]来实现。这样的话,整个系统之间耦合度不高,可以进行合适的拆分和复用。
- 而DDD中的聚合根,即使我们上面说的一个内聚。可以做这样的一个比喻。
- 而在聚合根内由很多实体,实体中有其自己的方法和值对象。那么权力下移,实体操作自己,完成数据的持久化操作等。
- 和传统的开发模式相对比,传统模式开发,是数据库驱动设计[SQL-Driven],也就是一个系统,是由数据库先行设计,然后根据数据库设计完成对应的数据操作、代码编写。在这个情况下,很多的业务逻辑包含在SQL里面,而一条SQL能做的事情很少,这样的话,Service对象要求做一些操作的时候,就会比较尴尬,缺少SQL就要去DAO里面去写,这样就会造成SQL的冗余,很多SQL语句差别只是某个字段。
- 而在DDD中,通过事件风暴、领域建模、产品愿景等,给出所有的需要的业务场景,定义领域内锁包含的属性和方法 。领域模型相当于可复用的业务中间层。新功能需求的开发,都基于之前定义好的这些领域模型来完成。
- 在DDD设计中,没有把Service去掉,只是其功能被削弱,这是为什么?
- Service是不可能去掉的,对于我现在的理解来说,因为每个领域模型,其都是针对自己的操作,而Service类负责与Repository交流,xxxService负责和xxxRepository打交道,调用xxxRepository的方法,获取数据库中的数据,转换为领域模型,然后由领域模型对象完成业务逻辑,最后调用xxxRepository完成数据的持久化。
- Service类负责跨服务调用,实现领域模型的聚合功能,DDD是和微服务进行协作的,也就是需要调用其他服务的领域模型对象的方法,跨服务的通信就在Service中完成
- Service负责一些非功能性和第三方系统交互的工作。 比如幂等、事务、发邮件、发消息、记录日志、调用其他系统的 RPC 接口等,都可以放到 Service 类中。
- 基于充血模型的DDD开发模式,Service被改造了充血模型,但是Controller层和Repository还是贫血模型,是否需要充血领域建模?
- 不需要,因为Service的领域建模,就已经完成了大部分的功能了。而且,Entity,其生命周期很短,一般传递到Service就结束了,而VO、DTO,也只是数据的载体,不需要对数据进行很多的复杂操作。从功能上来讲,其不包含业务逻辑、只包含数据。
DDD的构建过程
建立领域知识
- 说白话,就是理解在这个领域中(如保险领域、电商领域等等)能够提炼出的领域对象,也就是这个对象能够有哪些字段和操作
通用语言
- 因为不同的行业的注重点不一样,像是我们程序员去对接客户,我们在想代码实现,但是客户在想功能。这样就会导致思维不对口,这个时候,就应该使用通用语言——就是双方都能理解,都能充分表达和被表达。
模型驱动设计
- 通过事件风暴、通用语言分析、产品远景,进行下一步的模型驱动设计,也就是设计出这个模型的作用范围,以及其功能
模型关系图
- 当我们设计好多个模型的时候,模型与模型之间可能会有耦合,需要把模型之间的耦合线关联起来,这样就可以知道哪些地方需要使用什么
建立层架构
- 此处层结构,就是代码上的逻辑分层和物理分层。已经是DDD的逻辑了
- 而且,一般而言,一个模型即可成为一个微服务,但是要具体问题具体分析
实体对象和值对象
实体
与面向对象中的概念类似,在这里再次提出是因为它是领域模型的基本元素。在领域模型中,实体应该具有唯一的标识符,从设计的一开始就应该考虑实体,决定是否建立一个实体也是十分重要的。值对象
和我们说的编程中数值类型的变量是不同的,它仅仅是没有唯一标识符的实体,比如有两个收获地址的信息完全一样,那它就是值对象,并不是实体。值对象在领域模型中是可以被共享的,他们应该是“不可变的”(只读的),当有其他地方需要用到值对象时,可以将它的副本作为参数传递。
服务
- 并非所有的点都可以通过模型实现,那么这个时候就需要使用服务来进一步的加深和完善功能。
- 服务的存在就是为领域模型提供简单的方法
- 特点
- 服务中体现的行为一定是不属于任何实体和值对象的,但它属于领域模型的范围内
- 服务的行为一定涉及其他多个对象
- 服务的操作是无状态的
模块
- 为了组织统一的模型概念来达到减少复杂性的目的。
- 模块应该有一致的对外接口被其他模块调用, 比如有三个对象在模块a中,那么模块b不应该直接操作这三个对象,而是操作暴露的接口。模块的命名也很有讲究,最好能够深层次反映领域模型。
聚合和聚合根
- 聚合被看作是多个模型单元间的组合,它定义了模型的关系和边界。每个聚合都有一个根,根是一个实体,并且是唯一可被外访问的。正是如此,聚合可以保证多个模型单元的不变性,因为其他模型都参考聚合的根。所以要想改变其他对象,只能通过聚合的根去操作。根如果没有了,那么聚合中的其他对象也将不存在。
工厂
- 在复杂的应用中,实体和聚合通常十分复杂,那么通过构造器去构造对象,那很复杂。工厂把创建对象的细节封装起来,实现依赖反转。
- 当然对聚合也适用(当建立了聚合根时,其他对象可以自动创建)。工厂最早被大家熟知可能还是在设计模式中,的确,在这里提到的工厂也是这个概念。
- 注意,以下情况不适宜使用工厂:
- 构造器很简单
- 构造对象不依赖其他对象的创建
- 使用策略模式就可解决
仓库
仓库
封装了获取对象的逻辑,领域对象无须和底层数据库交互,它只需要从仓库中获取对象即可。仓库可以存储对象的引用,当一个对象被创建后,它可能会被存储到仓库中,那么下次就可以从仓库取。如果用户请求的数据没在仓库中,则会从数据库里取,这就减少了底层交互的次数
统一非空验证
- NonEmptyValidationConfig
- 何为统一非空验证?
- 为什么需要统一非空验证?
疑问
- 领域模型自己操作自己,如果发现某些SQL没有实现[应该会存在],这个时候也要去实现Repository的方法。[必然的]
- 缺乏实践,对其功能执行还不是很熟悉。
参考资料
- 浅析DDD(领域驱动设计):https://www.jianshu.com/p/b6ec06d6b594
- 命令和查询责任分离 (CQRS) 模式:https://docs.microsoft.com/zh-cn/azure/architecture/patterns/cqrs
- 《实现领域驱动设计》 作者:Vaughn Vernon