阿里妹导读
本文作者结合在团队的实践过程,分享了自己对领域驱动设计的一些思考。
了解过领域驱动设计的同学都知道,人们常常把领域驱动设计分为两部分:战术设计和战略设计。这两个概念本身都是抽象的,有人把战术设计看作是领域内的设计过程,而战略设计看作是领域间关系的设计过程。也有一种认知是把战术设计看作是编码的设计,把战略设计看作是架构的设计。实际上领域驱动设计的作者Eric Evans本无意将这两者进行割裂,相反两者之间相辅相成,缺一不可。我将在本文中结合团队的实践过程,分享我对领域驱动设计的一些思考。
转变思维
被忽视的面向对象
我们在刚开始学习面向对象的时候,知道面向对象的三个特性:继承、封装、多肽,也知道面向对象的SOLID原则,但很不幸的是,当我们在实际工作以后,这些特性和原则好像并无用武之地。目前我在公司看到过的大部分代码中的对象只有两种类型:服务类(Service Object)和数据类(Data Object),所有的数据对象,被无脑的开放了所有的Getter和Setter方法,加之lombok等语法糖的推波助澜,对象的封装变得更加困难了。而所有的业务逻辑都被堆在了各种Service中,当然好的团队依然会对这些Service类做很好的分层设计,使代码的演进还能够正常的进行。
实际上我并不是要说这种开发方式不好,相反它能够在程序员中被广泛认可,其优势不言而喻,它能够让一个只要掌握编程语言的新手,快速的承接需求并交付,无需在代码设计和怎么写的问题上花费更多的精力和学习成本。
大部分情况下,团队内的架构师只需要做好接口设计和数据库的设计,这个需求就可以完全交给一个新人去实现了。
我把这种方式看作是一种通过确定【输入】和【输出】来控制软件开发确定性的方式,
输入即程序对外提供的可以执行程序的入口,我们常见的像RPC接口、HTTP接口、消息监听、定时任务等。
输出是程序对外部环境或者状态的影响,可以是数据库的写入、消息的广播推送、外部系统的调用等等。
在一个系统刚开始的阶段,这种方式能够以非常高的效率完成交付,这个阶段业务的本质复杂性低,技术的复杂性也低,程序的输入和输出链路比较单一。更重要的是在人的方面,每个人都能够很好的理解这种开发方式,只要从输入到输出的转换没有问题,程序员们不会去关注其中潜在的设计问题,无论是新人还是老手,开发这样的软件都能得心应手。相比于使用领域驱动设计的思维进行开发,面向过程的这种开发方式更简单直接,对人和团队的要求更低,在人员变动频繁的现状中,它能带来更快速的交付。
复杂度的膨胀
然而随着系统逐渐的演进,业务的核心复杂性变高,系统之间的联系逐渐变多,面向过程的这种开发方式就显得捉襟见肘了。不知道大家能否在自己团队中找到这样的代码:
上千行的方法 上百个属性的类 循环依赖的Service 无法控制的数据一致性 越来越多的分支逻辑 ...
这些问题本质上并不是我们采用哪种开发方式就能解决的,但它们一定能说明我们当前的代码设计是存在问题的,这就像埋在我们系统中的一个个定时炸弹,如果你足够小心,团队的质量保障足够充足,这颗炸弹在你工作的期间可能并不会引爆,但根据墨菲定律,它们早晚是会引爆的。潜在的风险是一方面,另一方面是我们的交付速度,理解成本,沟通成本,知识的传递,都会因为这些混乱的代码而变得缓慢和困难。
但是程序员们总是会有办法的,用战术上的勤奋来弥补战略上的懒惰,花更多的时间去讨论,去梳理,写更多的文档,做更多的测试,掉更多的头发。当系统最终无法应对业务的变化时,要么一走了之,要么从头再来搞个2.0。
应对软件复杂度的方法有很多,即使是使用面向过程的开发方式,也有很多设计模式和方法论能够去解决这些问题。如果你还没有找到一个特别好的方式,不妨尝试一下领域驱动设计。
基于面向对象
在进行领域驱动设计落地的过程中,我感觉到最大的一个困难点是面向对象思维的转变,领域驱动设计实际上是基于面向对象设计的一种更高维度的设计模式,但我们之中大部分的开发者,已经习惯于按照面向过程的方式来进行开发,即使我们在很多场合都在强调我们在使用面向对象,但实际上却事与愿违。
经验越丰富,越资深的工程师,越无法跳出之前长期积累的认知,这种先入为主的思维定式改变起来尤为困难。
还有源源不断的新人逐渐开始进入这个行业,成为一个软件工程师,他们被要求能够尽快的开始交付和产出,他们也只能去模仿团队现在的代码,逐渐熟练以后,也只会把这种开发方式奉为圣经,再次传承下去。
随着实践领域驱动设计逐渐进入到深海区,我越来越感受到,面向对象至关重要,长期面向接口编程、面向数据库编程、面向中间件编程,已经让大家的思维很难去转变。即使我们有再好的领域设计,边界划分,如果无法将其在代码中表现出来,那也只会是空中楼阁,无法发挥领域驱动设计的真正作用。
领域模型
之前提到,我们现在的开发现状是通过【输入】和【输出】来进行设计,而领域驱动设计则是在其基础上增加了一层:【领域模型】。即所有的输入都要转换为领域模型,所有的输出也都要通过领域模型去完成。领域驱动设计的所有模块、模式、方法都是基于领域对象,为领域对象服务的。
领域模型本身作为对现实世界中我们所解决问题空间的抽象,它的演进与问题空间的演进原则上是一致的,之所以使用面向对象来作为领域模型的承载,主要原因还是面向对象更加符合当下人们对现实世界的认知,理解和使用都更加简单。现实世界中大部分的“系统”,都是可以用对象,以及对象之间的关系来描述,认识、理解、描述现实世界中的客观事物是人类哲学最早开始思考的问题,先秦时期的名家,古希腊的形而上学,都是基于此目的建立的。今天我们的工作又何尝不是在混乱复杂的世界中,寻找规律,将其通过有限的模型表达出来,再转换为机器可以理解的语言,形成软件或者系统,简化人与人,人与物,物与物之间的交互过程。
每次想到这些我就会热血沸腾,虽然生活限制了你人身的自由,但并没有限制你思维的自由,去认识世界、抽象现实,软件工程不光只有埋头敲代码的吗喽,也可以有像苏格拉底一样探索世界本质的思考者。
当然如何建模以我现在掌握的技巧和经验,实在有点拿不出手,还需再沉淀一下,本文还是主要关注于如何把领域模型在代码中进行落地。
领域对象
1. 实体(Entities)
Many objects are not fundamentally defined by their attributes, but rather by a thread of continuity and identity.
领域模型中最核心的是领域对象,而领域对象中最核心的是实体,《领域驱动设计》里对实体的定义如上,意思是,实体从根本上不由其属性来定义,而是由连续性和唯一性来定义。
类似于“白马非马”的哲学问题,“白马”是名(Defination),“马”也是名,只有你看得见摸得着实际存在的那匹白马才是实(Instance),假设这个世界是一个巨大的Java虚拟机,唯一能代表那匹白马的,只有它在内存里的地址。即使这匹马之后染了黄毛,起了个名字叫“小黑”,它还是它,不因其属性或者特征的变化而成为另外一匹马。直到这匹马死去,尸骨化为养分,它的存在不再有任何意义,系统收回它所占用的内存,这个实也就彻底不存在了。在领域模型中,需要通过一个唯一标识而不是其属性来区分,且在其生命周期中具有连续性的对象,我们将它定义为一个实体。概念的解释过于抽象,我们来通过我们最熟悉的订单Order为例:
class Order{
private String id;
private Date createTime;
private Status status;
void complete(){
this.status = Status.COMPLETED;
}
}
// 实体的一生
void lifeOfOrder{
// 创建:对象的首次创建,需要通过一个符号来唯一标识它
Order order = new Order("ID", new Date(), Status.INIT);
// 存储:存储到数据库或者文件中
new OrderRepository().save(order);
// 重建:冲数据库或文件中读取
Order orderRef = new OrderRepository().get("ID");
// 修改:对象在修改其属性并重新持久化
orderRef.complete();
new OrderRepository().save(orderRef);
// 删除:从数据库或文件系统中存档或永久删除,系统将无法再次重建该对象
new OrderRepository().delete(orderRef);
}
使用数据库主键作为实体唯一标识
注:使用这种策略,实体只有经过持久化以后,才能产生唯一标识,实际使用的过程中很容易出错,不建议使用。
//领域对象
class Order{
private Long id;
}
//ORM框架数据库表对象
class OrderDO{
//数据库主键
private Long id;
}
class OrderFactory {
public Order buildOrder(){
return new Order();
}
}
class OrderRepository {
private OrderDao orderDao;
public void insert(Order order){
OrderDO orderDO = new OrderDO()
orderDao.insert(orderDO);
//从ORM对象中获取表自增ID回填领域对象
order.setId(orderDO.getId());
}
}
使用随机UUID作为实体唯一标识
//领域对象
class Order{
private String id;
public Order(String id){
this.id = id;
}
}
//ORM框架数据库表对象
class OrderDO{
//数据库主键
private Long id;
//Order唯一标识
private String orderId;
}
class OrderFactory {
public Order buildOrder(){
return new Order(UUID.randomUUID().toString());
}
}
class OrderRepository {
private OrderDao orderDao;
public void insert(Order order){
OrderDO orderDO = new OrderDO()
orderDO.setOrderId(order.getId());
orderDao.insert(orderDO);
}
}
使用Sequence生成实体唯一标识
//领域对象
class Order{
private String id;
public Order(String id){
this.id = id;
}
}
//ORM框架数据库表对象
class OrderDO{
//数据库主键
private Long id;
//Order唯一标识
private String orderId;
}
class OrderFactory {
//序列生成器,可以参考TDDL Seq:https://mw.alibaba-inc.com/tddl/DeveloperReference/sequence
private SeqGenerator seqGenerator;
public Order buildOrder(){
return new Order("PREFIX_" + seqGenerator.nextInt());
}
}
class OrderRepository {
private OrderDao orderDao;
public void insert(Order order){
OrderDO orderDO = new OrderDO()
orderDO.setOrderId(order.getId());
orderDao.insert(orderDO);
}
}
2. 关联(Association)
一个实体往往会关联另外一个实体,这种关联关系主要包含一对一、一对多、多对多这三种类型,这个相信大家在数据库设计的过程中已经很熟悉了。在领域模型里,一对多,多对多的关联,往往会让代码复杂度急剧上升。
以订单为例,一个订单(Order)可以包含多个产品(Product),一个产品也可以属于多个订单。
class Order{
private String id;
private List<Product> products;
}
class Product{
private String id;
private List<Order> orders;
}
规定一个遍历的方向:仅允许通过订单遍历该订单下所有的产品,这样订单和货品之间多对多的关系就简化为一对多。 添加限定:限定订单只允许包含一个产品,这种限定可能作用于某种特殊类型的订单,这样订单和产品的关系就会简化为一对一。 消除不必要的关联:产品对订单的引用,往往并没有实际作用的场景,这种情况我们可以消除产品对订单的关联关系。
简化后的领域对象:
class SingleProductOrder{
private String id;
private Product product;
}
class Product{
private String id;
}
3. 领域对象的持久化(Persistence)
这个章节本来想放到最后去说,但是想想又不得不把这部分提到前面来讲,因为这部分可能是我们在设计领域模型过程中最容易出现问题的。我们大部分应用使用的ORM框架,基本上都是用Mybatis,因此我们往往都需要有一个对象来映射数据库表结构,这里我将它命名为数据库对象,我们在代码中一般会通过DO、BO等后缀来进行区分。也正因为这个原因,我们很多时候都会直接将数据库模型作为代码设计的目标,代码逻辑也是为了操作数据库对象来写,导致代码中缺失真实业务场景的还原。
所以首先要强调的是一定要将领域模型和数据库模型分离开,这样我们的业务代码仅需要关注领域模型,到需要持久化的时候再去关心如何将领域模型转换为数据库模型。如此,即使之后数据库的选型发生变化,对代码的改动也仅限于对象转换的那部分逻辑;领域模型的迭代演进也可以更加自由,不受数据库设计的约束。
领域模型到数据库模型转换的过程中需要注意几个细节:
不要将数据库关注的属性,无脑添加到领域对象中去,比如id、gmt_created、gmt_modified等。
实体间的关联,在数据库中经常会通过关系表来表达,但在领域对象中,完全可以通过类的引用关系来表示,不需要将关系抽象为实体(除非这个关系有特殊的业务意义)。
将领域对象转换为数据库对象:
class Order{
private String id;
private List<Product> products;
}
class Product{
private String id;
}
class OrderDO{
private Long id;
private String orderId;
private Date gmtCreated;
private Date gmtModified;
}
class OrderProductRelationDO{
private Long id;
private String orderId;
private String productId;
private Date gmtCreated;
private Date gmtModified;
}
class OrderRepository{
void save(Order order){
orderDao.insert(new OrderDO(order.getId()));
order.getProducts().forEach(orderProduct -> {
orderProductRelationDao.insert(new OrderProductRelationDO(order.getId(), orderProduct.getId()));
});
}
}