写在前面
目前在公司看到过的大部分代码中都有两种类型:服务类(Service Object)和数据类(Data Object);所有的数据对象,被开放了所有的Getter和Setter方法,再有lombok等语法糖的加入,对象的封装变得更加困难了。而所有的业务逻辑都被堆在了各种Service中,好的团队会对这些Service类做很好的分层设计,这种开发方式能够在程序员中被广泛认可,其优势不言而喻,它能够让一个只要掌握编程语言的新手,快速的承接需求并交付,无需在代码设计和怎么写的问题上花费更多的精力和学习成本。
这种方式看作是一种通过确定【输入】和【输出】来控制软件开发确定性的方式,输入即程序对外提供的可以执行程序的入口,我们常见的像HTTP接口、消息监听、定时任务等;输出是程序对外部环境或者状态的影响,可以是数据库的写入、消息的广播推送、外部系统的调用等等。
相比于使用领域驱动设计的思维进行开发,面向过程的这种开发方式更简单直接,对人和团队的要求更低,在人员变动频繁的现状中,它能带来更快速的交付。但随着系统逐渐的演进,业务的核心复杂性变高,在交付的代码会发现;
上千行的方法
上百个属性的类
循环依赖的Service
无法控制的数据一致性
越来越多的分支逻辑等等
应对软件复杂度的方式有很多,不妨尝试一下DDD领域驱动设计。
什么是领域驱动设计
领域驱动设计是一种思想,通过事件风暴使用通用语言对业务进行领域建模,通过限界上下文进行合理的领域拆分,可以使得领域模型转向微服务的设计和落地,从而解决复杂软件难以理解,难以演进,也可以解决微服务业务界限难以界定的问题。
在进行领域驱动设计落地的过程中,个人认为最大的一个困难点是面向对象思维的转变,领域驱动设计实际上是基于面向对象设计的一种更高维度的设计模式。
领域驱动设计之领域模型
我们现在的开发现状是通过输入和输出来进行设计,领域驱动设计则是在其基础上增加了一层:领域模型。即所有的输入都要转换为领域模型,所有的输出也都要通过领域模型去完成。领域驱动设计的所有模块、模式、方法都是基于领域对象,为领域对象服务的。领域模型本身作为对现实世界中我们所解决问题空间的抽象,之所以使用面向对象来作为领域模型的承载,主要原因还是面向对象更加符合当下人们对现实世界的认知,理解和使用都更加简单;领域模型只反映业务,和任何技术实现无关;领域模型不仅能反映领域中的一些实体概念,如货物,书本,应聘记录,地址等;还能反映领域中的一些过程概念,如资金转账等; DDD 的核心知识体系包括:领域、子域、核心域、通用域、支撑域、限界上下文、实体、值对象、聚合和聚合根等概念。
本文关注如何将领域模型在代码中进行落地:
领域对象
实体(Entities)
领域模型中最核心的是领域对象,而领域对象中最核心的是实体,实体是拥有唯一标识和状态,且具有生命周期的业务对象。
我们在实际应用的过程中,实体往往是需要持久化到数据库的,因此大部分情况下,我们都以数据库中的主键作为实体的唯一标识,虽然这种方式并不完全符合领域对象应独立于数据库存在,但在实际使用的过程中,并不会产生太大的影响。以一个订单实体Order的创建为例,用数据库主键作为实体唯一标识,使用这种策略,实体只有经过持久化以后,才能产生唯一标识
关联(Association)
一个实体往往会关联另外一个实体,这种关联关系主要包含一对一、一对多、多对多这三种类型,这个相信大家在数据库设计的过程中已经很熟悉了。在领域模型里,一对多,多对多的关联,往往会让代码复杂度急剧上升,在设计时注意消除不必要管联。
领域对象的持久化(Persistence)
现在大部分应用使用的ORM框架,基本上都是用Mybatis,因此我们往往都需要有一个对象来映射数据库表结构,这里我将它命名为数据库对象,我们在代码中一般会通过DO、BO等后缀来进行区分。很多时候都会直接将数据库模型作为代码设计的目标,代码逻辑也是为了操作数据库对象来写,导致代码中缺失真实业务场景的还原。DDD时一定要将领域模型和数据库模型分离开,这样我们的业务代码仅需要关注领域模型,到需要持久化的时候再去关心如何将领域模型转换为数据库模型,转化为数据库对象时需要注意:
-
不要将数据库关注的属性,无脑添加到领域对象中去,比如id、gmt_created、gmt_modified等。
-
实体间的关联,在数据库中经常会通过关系表来表达,但在领域对象中,完全可以通过类的引用关系来表示
值对象(Value Object)
当一个实体内的部分属性,我们发现它们具有较强的相关性,这些属性单独抽象成一个对象可以更好的描述事物,且这个对象并不具备唯一性,我们就将它归类为值对象,值对象具备以下特征:
- 不需要唯一标识来代表其唯一性
- 一些有关系的属性的聚合
- 有自己的特征
- 对模型有重要的意义
- 是用来描述事物的对象
通过值对象的提取,可以简化实体,突出实体核心属性,开发者只需要把注意力放在实体本身关键的属性上;使实体在持续演进的过程中,不会逐渐膨胀。
人员实体原本包括:姓名、年龄、性别以及人员所在的省、市、县和街道等属性。这样显示地址相关的属性就很零碎了对不对,现在,我们可以将 “省、市、县和街道等属性” 拿出来构成一个“地址属性集合”,这个集合就是值对象了。
聚合(Aggregate)
实体关联的极简设计能够帮助我们描述现实世界事物之间的关系,当多个实体之间在某些场景下需要保持更改的一致性时,除了使用对象关联外,还可以建立一个对象组,将有着紧密关系的实体和值对象封装在一起,这个对象组就是领域模型中的聚合。
聚合是一种更大范围的封装,把一组有相同生命周期、在业务上不可分隔的实体和值对象放在一起考虑,只有根实体可以对外暴露引用,这个根实体就是聚合根,聚合也是一种内聚性的表现。
工厂(Factories)
不同于设计模式中的工厂模式,这里的Factory仅仅是为了将领域对象创建的过程通过一种单独的模式独立出来。Factory是承载将系统对外提供的请求模型转换为领域模型功能的一系列对象,它把创建对象的细节封装起来,巧妙的实现了依赖反转。
仓库(Repository)
Repository提供了领域对象重建和持久化的功能,它隔离了领域模型与数据库系统的复杂性,使开发人员可以将关注点分离开,在处理业务逻辑的时候,不需要考虑数据库实现的问题;
领域服务(Service)
当领域模型中某个动作或者操作不能看作某个领域对象自身的职责时,可以将其托管到一个单独的服务类中,这种服务类,我们把它叫做领域服务。领域驱动设计给我们提供了一种分层治理的思路,将系统内的服务类分为几个大类:应用层服务、领域层服务、基础设施层服务。应用层服务用于处理输入输出、与领域模型和领域服务之间的调度、连接基础设施层服务。
我认为领域驱动设计是一种软件工程的思想,它不是一套模板,它的思想精髓值得软件工程师领。领域驱动设计也不是银弹,在软件开发过程中没有必要完全DDD一把梭,对于一些不复杂的项目,使用传统设计更简单高效。同时业务本身的复杂度不是依靠某种软件设计思想或者设计范式就能规避的,DDD只是架构师们在架构设计过程中的一种指导思想,它本质上是一种工具。