1.先“物”后“事”还是先“事”后“物”?
通常在面对一项业务需求时,如果是面向数据的开发方式,程序员们首先想到的是会有哪些信息?要存储哪些字段?如何设计一个数据库表。然后再针对数据库表进行增删查改,完善业务逻辑,操作这些信息,最后完成需求。
我总结为,这是一个先形成“物”的概念,然后再完成“事”的概念,是自下而上的开发(从数据库开始编码到控制界面编码)。
这看起来很自由,毕竟有了数据,随便事情怎么变动,都可以完成逻辑。
然而,这是基于数据库表不再变动的情况下的,如果之前考虑不周,在数据库表上要删除或增加几个字段,那改起来就麻烦了。
在领域驱动设计中,思考方式就有所不同,我们想的是先有一件“事”,比如某人购物,付款等这些场景并不怎么变动,变动的是其实现方式。所以我们先限定其场景范围(限界上下文),然后再针对事情中的“物”进行建模。如果场景变了,就说明我们需要再另外新建立一套模型,而不是像面对数据库表开发那样,继续改动字段,改动逻辑然后完成需求变动。这就像一个大泥球一样麻烦。
以前我时常面临这些麻烦,自从看了DDD之后,我就发现领域驱动设计其实可以解决这样的麻烦。
2.领域驱动设计到底是什么?
我认为java作为一个面向对象的语言,应该以面向对象的方式去解决问题。对象有自己的行为逻辑,或者说业务逻辑。例如,一个订单有若干订单明细项,订单总表的金额是订单明细各项金额之和。这是订单这个对象的业务规则,也必须保持这样的规则。我们称订单是个聚合,它有自己的业务逻辑,并且包含订单明细项的对象集合。
若以面向数据的方式开发,订单包含订单总表和订单明细表两张数据库表以及它们的各字段。通常在service层中实现程序要求的各种业务逻辑,那么在修改订单的数据时,在代码的各处都要注意保证类似“订单总表的金额是订单明细各项金额之和”这样的业务逻辑。程序员需要准备修改2张数据库表的对象,同时要额外注意准备修改的数据不会导致bug。保证业务逻辑的重复代码散落在程序各处,并且该逻辑还不是实现意图的核心逻辑,这增加了巨量的复杂度,过段时间或者新接手的程序员根本看不懂代码,导致出现程序能正常运行就不要去改它的现象,因为也不知从何改起。
而以领域驱动设计的方式开发就不同了,业务逻辑都集中在对象中,我想改任何数据,我都得去重建对象,调用对象的方法,保证修改的任何数据都符合它自身的业务规则,防止因为意外情况篡改了数据。即使有人跳过应用程序,直接修改数据库,若数据不符合规范,那么对象就无法重建,就会直接报错提醒管理员。这样开发人员就可以将关注点分离,可以放心地调用领域对象中的行为方法实现逻辑,再根据领域对象中的数据转化为持久化对象存储到数据库中。同时,当核心业务规则发生变化时,也可以集中了解业务知识,然后只用修改领域对象中的业务逻辑。
领域驱动认为内存中的数据,才是本体,而数据库中的数据只是其备份而已。由于内存昂贵,对象必须尽量小最好,所以一个作为聚合的对象应该只包含其具有约束的对象。例如,订单的总金额等于各项之和,订单明细项不能脱离订单总表而独立存在。
3.面向数据开发和领域驱动开发的实现区别
经过一段时间摸索,开发人员面临一个陌生业务时,也就是我们首先思考:
什么样的对象完成什么样的事情?
例如,“将产品提交到店铺售卖”这一业务。
1.在面向数据库表开发时,先有属性,然后完成事件,开发的时候可能会这样做。
public class Product extends Entity{
//店铺ID,一般作为产品表的外键关联店铺表的主键
private ShopId shopId;
//产品类型,作为产品表的一个字段
private ProductType type;
...
public void setShopId(ShopId shopId){
this.shopId = shopId;
}
public void setProductType(setProductType productType){
this.productType = productType;
}
...
}
然后调用的客户端代码,比如service层,完成业务事件的实际流程。
public class ProductService{
...
product.setShopId(shopId);
product.setProductType(productType);
...
}
2.而在领域驱动开发中,是这样做的。
public class Product extends Entity{
private ShopId shopId;
private ProductType type;
...
//行为逻辑由实体自身完成
public void commitTo(Shop aShop){
//自我验证
if(!this.isEmptyForShop){
throw new IllegalStateException("货架已满,请腾出空位!");
}
//完成业务逻辑
this.setShopId(aShop.id());
this.setProductType(productType);
//发布领域事件
DomainEventPublisher
.instance()
.publish(new ProductedCommitted);
}
...
}
客户端代码调用
public class ProductService{
...
product.commitTo(shopId);
...
}
由此,看出如果面向数据开发,以数据为中心,客户端代码必须懂得数据核心,虽然看起来操作自由,但容易设置错误。
而面向领域驱动开发,行为由领域实体自身完成,不会向客户端暴露业务逻辑。关注点在事件,而不是数据。领域驱动开发若出错,容易找到bug,封装性更好,所以方便进行迭代开发。