领域驱动设计 (DDD) 的思考

写在前面

打开 DDD 相关的书籍,你会被一系列生硬、高深的概念充斥,拜读完毕,满头雾水。这不是你的问题,而是 DDD 自己的问题,表现形式太概念化。学习它的内核,就不要被它给出的概念所迷惑,而要去思索这些概念背后所蕴含的设计原则,多问一些为何,本质无外乎是 SOLID。最重要的,要学会运用设计原则去解决问题,而非所谓的 “设计规范”。web

本文将会以系列解答的方式展开,由浅入深,篇幅不长,无妨一看。缓存

啥是 DDD?

本质上是一种方法论,提供了一套系统开发的设计方法。面对须要解决的问题,从复杂的现实中抽象出业务模型的思惟方式与实践技巧。初衷是清晰设计思路,规范设计过程。架构

啥是驱动?

DDD 强调是说得先把 “领域” 中涉及到的数据、流程、规则等都弄明白了,而后以面向对象的观点为其创建一个模型(即领域模型),而这个模型,决定了你将用什么技术、什么架构、什么平台来实现这个系统。因此技术在这个过程当中是 “被动的”,是被 “选来” 实现 “领域模型” 的。对于项目的成败,技术不是决定性因素,领域模型是否符合事物的本质才是关键。app

能够看出,领域驱动设计的出发点是业务导向,技术服务于业务。运维

我有误解?

学习 DDD 有一些常见的误区。第一个要避免的就是,你必需要清楚,DDD 对于工程领域没有提出多么创新的技法,它更可能是把前人在生产系统中用惯的技法概括,总结,包装了一下 ——dom

  1. DDD 抛开它晦涩的表达和术语,内核无外乎是 SOLID。书中的技法不是做者的发明创造或独门秘籍,而是当时业已存在并已在使用的作法,只不过没有被系统地加以记录 —— 换而言之,只要遵循 SOLID 的原则,这些所谓的概念技法彻底可能在无心识的状态下自发出如今生产代码中。
  2. 这些技法在各类大型系统被屡次使用。换而言之,只要你接触足够多的代码、足够多的项目,必然会接触到这些 DDD 的实际应用。只不过在看过 DDD 一书以前,你可能意识不到这是一个成体系的设计手段。—— 若是你没法理解这样一个设计理论,看实际生产代码的应用场景是惟一真正有效的方法,毕竟设计理论自己就是从具体代码中总结出来的。即便你以为本身懂了,抽象思惟和开发经验也可能还未达到正确使用它的水平。
  3. DDD 最大的价值在于对设计手段进行了有效的整理,造成了一个完整的系统设计规范,但过于晦涩和概念化的表述,也几乎消解了它对业界的贡献。

啥时候用?

你可能认为 DDD 是一把 “瑞士军刀”,可以解决全部的设计问题,而实际上 “DDD 只是一把锤子”,有个谚语叫作 “若是你手里有一把锤子,那么全部的问题都变成了钉子”,若是你拿着 DDD 这把锤子处处去敲,要么东西被敲坏,要么就不起做用。curl

为何说 DDD 只是一把锤子呢?做者明确指出,DDD 只适合业务复杂度很大的场景,不适用于技术复杂性很大但业务领域复杂性很低的场景。能够看出,DDD 只专一一个领域,高复杂业务 —— 经过它能够为你的系统创建一个核心、稳定的领域模型,灵活、可扩展的架构。分布式

DDD 是拥抱复杂的,拥抱变化的,但自己也是有成本有前提的;简单系统,不必搞这么复杂,强上 DDD 就是一种反模式了。因此,当你遇到一个问题就想到 DDD 的时候,必定要注意 “DDD 只是一把锤子”,不要拿着这把锤子处处去敲!工具

啥是复杂?

如何判断业务是否复杂,判断依据不胜繁数。在我看来,没那么复杂,就两个:单元测试

  • 宽度:链路广度大,关注多个纬度的消息来源,覆盖了较多的业务场景
  • 深度:链路深度深,关注对象整个的生命周期,从数据建立、到变动、再到后期运维,流程运转长

只要知足其中一个,我认为就是复杂的。

具体解决啥?

DDD 并不是 “银弹”,天然也不是解决全部疑难杂症的 “灵丹妙药”。在我看来,它只解决一个问题:过分耦合。

为啥会耦合?

业务初期,系统功能大都很是简单,CRUD 就能知足,此时的系统是清晰的。随着业务的不断演化,系统的频繁迭代,代码逻辑变得愈来愈复杂,系统也愈来愈冗余。模块彼此关联,谁都很难说清模块的具体功能意图是啥;修改一个功能,每每光回溯该功能须要的修改点就须要很长时间,更别提修改带来的不可预知的影响面。

归根到底在于系统架构不清晰,划分出来的模块内聚度低、高耦合,致使代码很差复用、很差扩展、很差运维。

咋解决耦合?

第一种解决方案:按照演进式设计的理论,让系统的设计随着业务的演进而增加。这固然是可行的,工程实践中的重构、持续集成就能够对付各类混乱问题。问题在于,只是没有章法的、小范围的代码重构,很难具有通用型,容易变成了重构者的自娱自乐,代码继续腐败,从新重构…… 无休止的循环

第二种解决方案:从新抽象,如何抽象,DDD 建议咱们左右开弓:一、分治(实体对象、值对象、聚合根);二、分层(展现、应用、领域、通用)

咋作分治

核心概念三句话:

  • 聚合根:一组相关对象的集合,做为一个总体被外界访问。聚合根的 ID 全局惟一
  • 实体:有生命周期,有状态,经过 ID 进行惟一标识
  • 值对象:属性,配合描述实体的状态

核心关系一句话:

  • 经过聚合根来引用实体,挂载值对象,对外屏蔽内部的实体逻辑

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 设计举例:

  • 基础要素:createdTime、modifyTime
  • 核心要素:developerId、appId、businessId、ePoiId、poiId
  • 扩展要素:开发者的联系电话、店面的联系电话
  • 冗余要素:feature

于此,咱们能够概括出领域模型设计的通常步骤:

  • 根据需求划分出初步的业务域
  • 识别出哪些领域是实体,哪些是值对象
  • 对实体、值对象进行聚合,划分出聚合根
  • 工程实践中检验模型的合理性,倒推模型中不足的地方并从新概括

这里多说一句,若是你读过金字塔原理的话,会发现思惟方式分为两种:概括性思惟和演绎性思惟。人的原始思惟方式是演绎性的,这就决定了咱们常常基于流程去思考问题,而面向对象建模偏偏须要的是概括性思惟。这也是着重须要自我训练的地方。

咋作分界

模块

首先须要划分模块。模块(Module)是 DDD 中明确提到的分层前置手段,在工程实践中,较为常见的模块策略有两种:

技术职责分包:

  • client:富客户端,thrift 接口划分在这一层,亦用于将来组织前置缓存、业务无关的通用校验等功能
  • gateway:网关层,业务鉴权、请求过滤,并将请求对内映射成统一的 event 事件
  • common:定义 core 和 client 所共用的内容
  • core:业务服务的实现
  • qatest:单元测试用例
  • web:https 请求接口、业务自测 curl 接口

业务职责分包:(就拿开放平台 open 举例)

  • open-base-client:兄弟系统的业务 rpc
  • open-erp-client:erp 开发者 rpc
  • open-pushrecord-client:推送订单 rpc
  • open-web:外网请求

复杂系统建议采用技术分包策略,提升模块代码的复用性。

分层

而后咱们再来说模块内的分层,这里特指 core 模块的内部分层。DDD 描述了几个分层概念,分别是:防腐层、服务层、资源库、领域对象、基础层。

如代码中所示,通常的工程中包的组织方式为 {com. 公司名。组织架构。业务。上下文.*},这样的组织结构可以明确的将一个上下文限定在包的内部。

  • com.open.adapter.* :防腐层(适配层,DO 转义)
  • com.open.service.* :服务层(逻辑流转层)
  • com.open.tair.* :资源库(分布式缓存、本地缓存)
  • com.open.dao.* :资源库(关系型、非关系型)
  • com.open.domain.* :领域对象(DO)
  • com.open.util.* :基础层(工具层)

这里着重释义一下防腐层,该层主要是将外部系统 DO 转义成本系统 DO,避免外部 DO 一旦发生变化,本系统改动范围过大的状况,收敛影响面。

//接口层
  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. 给出架构设计,给出核心代码,开发人员去作扩展、实现

1 不可控,2 不现实。推荐 3 的作法:架构师给出设计方案,并给出骨干实现,开发人员有了可类比的代码,就可以比较准确的去作功能开发。这比空讲要有效的多。

此外,落地中还要注意三点:

  1. 创建统一语言,避免认知误差。客户、产品经理、架构师、开发人员在需求理解上的误差,是后期返工的一大来源
  2. 频繁沟通,迭代共识。 因为周期较短,即便发现了实现误差,也能及时纠偏
  3. 快速反馈。开发人员模糊地带主动询问架构师,尽量保证模型与实现的一致

我的感悟

DDD 实际上是面向对象方法论的一个升华。咱们回头来看它,无外乎是经过划分领域(聚合根、实体、值对象)、领域行为封装到领域对象(充血模式)、内外交互封装到防腐层、职责封装到对应的模块和分层,从而实现了高内聚低耦合 —— 这也是它最精华的部分。

那么 DDD 的各个概念重要么?并不重要。概念掌握确实有助于提升咱们的业务思考能力,但 DDD 强调的是理论结合实践,没有通过实战考验,都是纸上谈兵。经验丰富的开发人员,即使没有据说过 DDD,其思想也每每与 DDD 相符。我更建议的是:不要受限于对概念的掌握,而要透过层层表象思考它的本质,追求设计原则与实践方法的融汇贯通。只有如此,才能针对不一样的场景灵活地运用 DDD,而非生搬硬套。毕竟,设计老是如此,即便前人已经总结了许多的方法,也不能像数学公式那样套用获得准确无误的结果。它不存在惟一的答案。

经验有限,我对 DDD 的理解不免会有不足之处,欢迎你们共同探讨,共同提升。

参考书籍

一、领域驱动设计精简版二、实现领域驱动设计