本文结合实例来分析下领域驱动设计 (DDD)
本质上是一种方法论,提供了一套系统开发的设计方法。面对需要解决的问题,从复杂的现实中抽象出业务模型的思维方式与实践技巧。初衷是清晰设计思路,规范设计过程。
领域驱动设计(英语:Domain-driven design,缩写 DDD)是一种通过将实现连接到持续进化的模型[1]来满足复杂需求的软件开发方法。领域驱动设计的前提是:
领域驱动设计是一种由域模型来驱动着系统设计的思想,不是通过存储数据词典(DB表字段、ES Mapper字段等等)来驱动系统设计。领域模型是对业务模型的抽象,DDD是把业务模型翻译成系统架构设计的一种方式。
DDD 强调是说得先把 “领域” 中涉及到的数据、流程、规则等都弄明白了,然后以面向对象的观点为其建立一个模型(即领域模型),而这个模型,决定了你将用什么技术、什么架构、什么平台来实现这个系统。所以技术在这个过程中是 “被动的”,是被 “选来” 实现 “领域模型” 的。对于项目的成败,技术不是决定性因素,领域模型是否符合事物的本质才是关键。
可以看出,领域驱动设计的出发点是业务导向,技术服务于业务。
学习 DDD 有一些常见的误区。第一个要避免的就是,你必须要清楚,DDD 对于工程领域没有提出多么创新的技法,它更多是把前人在生产系统中用惯的技法归纳,总结包装了一下:
作者明确指出,DDD 只适合业务复杂度很大的场景,不适用于技术复杂性很大但业务领域复杂性很低的场景。可以看出,DDD 只专注一个领域,高复杂业务 —— 通过它可以为你的系统建立一个核心、稳定的领域模型,灵活、可扩展的架构。
DDD 是拥抱复杂的,拥抱变化的,但本身也是有成本有前提的;简单系统,没必要搞这么复杂,强上 DDD 就是一种反模式了。所以,当你遇到一个问题就想到 DDD 的时候,一定要注意 “DDD 只是一把锤子”,不要拿着这把锤子到处去敲!
如何判断业务是否复杂,判断依据不胜繁数。在我看来,没那么复杂,就两个:
只要满足其中一个,我认为就是复杂的。
DDD 并非 “银弹”,自然也不是解决所有疑难杂症的 “灵丹妙药”。在我看来,它只解决一个问题:过度耦合。
业务初期,系统功能大都非常简单,CRUD 就能满足,此时的系统是清晰的。随着业务的不断演化,系统的频繁迭代,代码逻辑变得越来越复杂,系统也越来越冗余。模块彼此关联,谁都很难说清模块的具体功能意图是啥;修改一个功能,往往光回溯该功能需要的修改点就需要很长时间,更别提修改带来的不可预知的影响面。
归根到底在于系统架构不清晰,划分出来的模块内聚度低、高耦合,导致代码不好复用、不好扩展、不好运维。
第一种解决方案:按照演进式设计的理论,让系统的设计随着业务的演进而增长。这当然是可行的,工程实践中的重构、持续集成就可以对付各种混乱问题。问题在于,只是没有章法的、小范围的代码重构,很难具备通用型,容易变成了重构者的自娱自乐,代码继续腐败,重新重构…… 无休止的循环
第二种解决方案:重新抽象,如何抽象,DDD 建议我们双管齐下:1、分治(实体对象、值对象、聚合根);2、分层(展示、应用、领域、通用)
核心概念三句话:
核心关系一句话:
通过聚合根来引用实体,挂载值对象,对外屏蔽内部的实体逻辑
talk is cheap,show me the code
//聚合根
class Order {
public String id;//订单ID,全局唯一
public Address customerAddress;//配送地址
public List<Item> items;//商品信息
public Pay pay;//支付信息
public LogisticsDetail logisticsDetail;//物流信息
public Pingjia pingjia;//评价信息
}
//实体
class Item {
public Long id; //商品ID,实体主键,Order内唯一
public String name;//商品名
public float price;//价格
public int count;//数量
}
//实体
class Pay {
public Long id; //支付ID,实体主键,Pay内唯一
public String source;//支付方式
public int currency;//币种
public float total;//价格
}
//实体
class LogisticsDetail {
public Long id; //物流ID,实体主键,LogisticsDetail内唯一
public int cpCode;//物流公司
public String mailNo;//物流单
public float status;//当前状态
}
//实体
class Pingjia {
public Long id; //评价ID,实体主键,Pingjia内唯一
public String desc;//描述
public byte[] image;//图片
}
//值对象
class Address{
public String province;//省
public String city;//市
public String county;//区
}
可以看到,通过业务限界,DDD 将大型复杂的 DO 分解为若干简单的 DO,从而保证 DO 的扩展性、灵活性。具体到 DB 层面,需要五张表:
但如果业务没有这么复杂,我依然推荐 DO 的 4 要素设计:基础要素、核心要素、扩展要素以及冗余要素。就拿 open 店面绑定的 App 设计举例:
于此,我们可以归纳出领域模型设计的一般步骤:
这里多说一句,如果你读过金字塔原理的话,会发现思维方式分为两种:归纳性思维和演绎性思维。人的原始思维方式是演绎性的,这就决定了我们经常基于流程去思考问题,而面向对象建模恰恰需要的是归纳性思维。这也是着重需要自我训练的地方。
首先需要划分模块。模块(Module)是 DDD 中明确提到的分层前置手段,在工程实践中,较为常见的模块策略有两种:
技术职责分包:
业务职责分包:(就拿开放平台 open 举例)
复杂系统建议采用技术分包策略,提高模块代码的复用性。
然后我们再来讲模块内的分层,这里特指 core 模块的内部分层。DDD 描述了几个分层概念,分别是:防腐层、服务层、资源库、领域对象、基础层。
如代码中所示,一般的工程中包的组织方式为 {com. 公司名。组织架构。业务。上下文.*},这样的组织结构能够明确的将一个上下文限定在包的内部。
//接口层
public class Consume {
public BaseResultDTO consumeMessage(OrderDetailMessage orderDetailMessage ) throws Exception, Throwable{
OpenOrderDO openOrderDO = transferAdapter.orderDetailMessage2openOrderDO(orderDetailMessage);//调用防腐层
openOrderService.handleOrderMeasage(openOrderDO);//调用服务层
}
}
//防腐层
public class TransferAdapter {
public OpenOrderDO orderDetailMessage2openOrderDO(OrderDetailMessage orderDetailMessage){//转换DO
OpenOrderDO openOrderDO = new OpenOrderDO();
openOrderDO.setTradeId(orderDetailMessage.getTradeId());
openOrderDO.setOrderSource(orderDetailMessage.getOrderSource());
openOrderDO.setBuyerUid(orderDetailMessage.getBuyerId());
openOrderDO.setSellerUid(orderDetailMessage.getSellerId());
return openOrderDO;
}
}
落地就是模型到代码的转换,核心是保证模型和代码的一致性。实际情况下,由于没有好的保持模型和代码一致的办法,很多系统往往开始搞的不错,慢慢就不一致了,也就逐渐烂掉了。
落地的方式,理论上有三种:
1 不可控,2 不现实。推荐 3 的做法:架构师给出设计方案,并给出骨干实现,开发人员有了可类比的代码,就能够比较准确的去做功能开发。这比空讲要有效的多。
此外,落地中还要注意三点:
DDD其实是面向对象方法论的一个升华。我们回头来看它,无外乎是通过划分领域(聚合根、实体、值对象)、领域行为封装到领域对象(充血模式)、内外交互封装到防腐层、职责封装到对应的模块和分层,从而实现了高内聚低耦合 —— 这也是它最精华的部分。
那么 DDD 的各个概念重要么?并不重要。概念掌握确实有助于提高我们的业务思考能力,但 DDD 强调的是理论结合实践,没有经过实战考验,都是纸上谈兵。经验丰富的开发人员,即便没有听说过 DDD,其思想也往往与 DDD 相符。我更建议的是:不要受限于对概念的掌握,而要透过层层表象思考它的本质,追求设计原则与实践方法的融汇贯通。只有如此,才能针对不同的场景灵活地运用 DDD,而非生搬硬套。毕竟,设计总是如此,即使前人已经总结了许多的方法,也不能像数学公式那样套用得到准确无误的结果。它不存在唯一的答案。
经验有限,我对 DDD 的理解难免会有不足之处,欢迎大家共同探讨,共同提高。