您还未登录! 登录 | 注册 | 帮助  

您的位置: 首页 > 软件开发专栏 > 开发技术 > 正文

浅谈DDD,你学会了吗?

发表于:2023-01-10 作者:小汪哥 来源:小汪哥写代码

DDD 最近几年越来越流行,大家都在聊这个话题,但是每个人对它的理解都不同,小汪哥这里根据之前在系统拆分、需求评估,以及遗留系统改造中的一点点经验,来浅浅的聊下自己对DDD的理解。从认知定义、作用、领域建模方法、实现方法论几个方面来聊聊。

认知定义

DDD 是一种处理高度复杂领域的设计思想,它试图分离技术实现的复杂性,并围绕业务概念构建领域模型来控制业务的复杂性,以解决软件难以理解,难以演进的问题。

DDD 不是架构,而是一种架构设计方法论,它通过边界划分将复杂业务领域简单化,帮我们设计出清晰的领域和应用边界,可以很容易地实现架构演进和微服务的落地。

作用

优势:

1、作为微服务的定义的指导思想。

2、理解业务的一种方法论,可以在接手遗留系统,以及遗留系统改造时快速理解业务。

3、解决领域知识被割裂肢解、代码的业务语义表达能力弱的问题。

4、控制系统复杂度,控制代码量。

劣势:

1、DDD不能解决大部分的性能优化问题,甚至大部分的场景,我们需要为性能优化去做反DDD设计。

2、DDD不能解决开发技术水平的问题。

3、DDD需要我们在领域建模花费很多的时间和精力,而且还可能导致付出和收益不成正比的情况。

领域建模方法

领域建模解决的问题

领域建模的目的是统一大家的业务认知,让业务、开发、测试、产品在同一个频道上交流。其实要做到这一点是很难的,开发喜欢从技术层面去描述问题,产品习惯从业务层面描述问题,两个不在同一个频道怎么能好好沟通。在小团队这种优势表现不出来,在大团队中,沟通成本是很高的。

领域建模,说的很简单,但是做好确实很难,一个复杂的需求不是建几个实体对象就能解决的。从全局看只在脑海中进行的建模实际上并不一定正确和稳定。因此我们需要找到正确的方法帮助对业务领域进行分析,得到建模结构,共享建模成果。值得庆幸的是,前辈及牛人已经总结了一些建模方法。

常用的建模方法有:用例分析法、四色建模法、事件风暴法。这个我就不一一赘述了,网上有很多内容,或者公众号回复【DDD】获取相关资料。

实现方法论

战略设计:

战略设计主要从业务视角出发,建立业务领域模型,划分领域边界,建立通用语言的限界上下文,限界上下文可以作为微服务设计的参考边界。

各种域:

核心域、支撑域和通用域的主要目标是:通过领域划分,区分不同子域在公司内的不同功能属性和重要性,从而公司可对不同子域采取不同的资源投入和建设策略,其关注度也会不一样。

统一语言:

统一语言提供了一种更好的协同方式的可能性。统一语言与其背后的领域模型赋予了研发人员通过重构定义业务的能力,在业务方大多强势的环境中,难能可贵地建立了技术反馈业务的途径,降低了知识消化过程失败的风险。

图片

图片来源:《如何落地业务建模》

限界上下文:

限界上下文是微服务设计和拆分的主要依据。在领域模型中,如果不考虑技术异构、团队沟通等其它外部因素,一个限界上下文理论上就可以设计为一个微服务。

限界上下文的定义就是:用来封装通用语言和领域对象,提供上下文环境,保证在领域之内的一些术语、业务相关对象等(通用语言)有一个确切的含义,没有二义性。

正如电商领域的商品一样,商品在不同的阶段有不同的术语,在销售阶段是商品,而在运输阶段则变成了货物。同样的一个东西,由于业务领域的不同,赋予了这些术语不同的涵义和职责边界,这个边界就可能会成为未来微服务设计的边界。看到这,领域边界就是通过限界上下文来定义的。

战术设计:

战术设计则从技术视角出发,侧重于领域模型的技术实现,完成软件开发和架构落地,包括:聚合根、实体、值对象等代码逻辑及代码分层的设计和实现。主要讨论在一个服务内部,如何划分和组织代码。

实体和值对象:

实体和值对象:从领域模型的基础单元看系统设计实体和值对象是组成领域模型的基础单元。

实体的代码形态

在代码模型中,实体的表现形式是实体类,这个类包含了实体的属性和方法,通过这些方法实现实体自身的业务逻辑。在 DDD 里,这些实体类通常采用充血模型,与这个实体相关的所有业务逻辑都在实体类的方法中实现,跨多个实体的领域逻辑则在领域服务中实现。

实体以 DO(领域对象)的形式存在,每个实体对象都有唯一的 ID。我们可以对一个实体对象进行多次修改,修改后的数据和原来的数据可能会大不相同。但是,由于它们拥有相同的 ID,它们依然是同一个实体。

实体的数据库形态​

在领域模型映射到数据模型时,一个实体可能对应 0 个、1 个或者多个数据库持久化对象。大多数情况下实体与持久化对象是一对一。在某些场景中,有些实体只是暂驻静态内存的一个运行态实体,它不需要持久化。

值对象​

值对象相对实体来说,会更加抽象一些。简单来说,值对象本质上就是一个集合。

值对象的代码形态​

值对象在代码中有这样两种形态。如果值对象是单一属性,则直接定义为实体类的属性;如果值对象是属性集合,则把它设计为 Class 类,Class 将具有整体概念的多个属性归集到属性集合,这样的值对象没有 ID,会被实体整体引用。

图片

图片来源:《DDD 实战课》

例如上图:

人员实体原本包括:姓名、年龄、性别以及人员所在的省、市、县和街道等属性。这样显示地址相关的属性就很零碎了对不对?现在,我们可以将“省、市、县和街道等属性”拿出来构成一个“地址属性集合”,这个集合就是值对象了。

聚合和聚合根:

领域模型内的实体和值对象就好比个体,而能让实体和值对象协同工作的组织就是聚合,它用来确保这些领域对象在实现共同的业务逻辑时,能保证数据的一致性。聚合就是由业务和逻辑紧密关联的实体和值对象组合而成的,聚合是数据修改和持久化的基本单元,每一个聚合对应一个仓储,实现数据的持久化。聚合有一个聚合根和上下文边界(一个聚合包含了多个实体对象和值对象,其中有一个实体对象做为聚合根。这些对象聚集在一起形成了一个比较完整独立的业务边界,称为上下文边界。),这个边界根据业务单一职责和高内聚原则,定义了聚合内部应该包含哪些实体和值对象,而聚合之间的边界是松耦合的。按照这种方式设计出来的微服务很自然就是“高内聚、低耦合”的。

我们以保险的投保业务场景为例,看一下聚合的构建过程主要都包括哪些步骤:

图片

图片来源:《DDD 实战课》

聚合根​

聚合根 leave 中有属性、值对象、关联实体和自身的业务行为。Leave 实体采用充血模型 ,有自己的业务行为,具体就是聚合根实体类的方法,如代码中的 getDuration 和 addHistoryApprovalInfo 等方法。

聚合根引用实体和值对象,它可以组合聚合内的多个实体,在聚合根实体类方法中完成复杂的业务行为,这种复杂的业务行为也可以在聚合领域服务里实现。但为了职责和边界清晰,我建议聚合要根据自身的业务行为在实体类方法中实现,而涉及多个实体组合才能实现的业务能力由领域服务完成。下面是聚合根 leave 的实体类方法,它包含属性、对实体和值对象的引用以及自己的业务行为和方法。

public class Leave {    String id;    Applicant applicant;    Approver approver;    LeaveType type;    Status status;    Date startTime;    Date endTime;    long duration;    int leaderMaxLevel;//审批领导的最高级别    ApprovalInfo currentApprovalInfo;    List<ApprovalInfo> historyApprovalInfos; 
public long getDuration()        return endTime.getTime()- startTime.getTime();    }
public Leave addHistoryApprovalInfo(ApprovalInfo approvalInfo)        if (null== historyApprovalInfos)            historyApprovalInfos= new ArrayList<>();        this.historyApprovalInfos.add(approvalInfo);        return this;    } 
public Leavecreate(){        this.setStatus(Status.APPROVING);        this.setStartTime(new Date());        return this;}
//其它方法}

DDD分层架构

最后就是如何组织代码的问题,这个时候就需要要到DDD的分层架构。

那么从之前的MVC三层架构如何演变成DDD的分层架构呢? 

DDD分层架构与MVC架构的映射关系:

图片

在《领域驱动设计——软件核心复杂性应对之道》书中也描述了各层的关系:

图片

不过小汪哥觉得,代码的组织方式可以根据团队的情况来调整,只要能符合领域驱动的思想即可。

各个层级的作用可以参考之前的文章:领域驱动落地实战​,这里就不在一一赘述了。

小结

本文主要从DDD是什么,能干什么,不能干什么,怎么干(领域建模方法、实现方法论)几个方面来聊了一下领域驱动,当然,一千个人有一千种对领域驱动的理解。​