我眼中的DDD系列二

之前曾经写过一篇DDD,总结了一下自己对于DDD的一些认识,包括认为DDD是一种战略,从道法术器上来讲,还属于道的层面,无法很好的落地。时隔一年后,因为工作参与业务中台建设的关系,也啃了两本书,所以再来阶段性总结一下。

在开始总结之前,先立一个小flag,看完要让大家对DDD有一个全局性的认识,如果没有达成,请找我发红包_

DDD就是面向对象

DDD的全称是领域驱动设计,与之前的设计模式相比有较大的不同。假如我们回忆一下编程语言发展的历史,其实就会感受到,编程语言的发展是从指令、到过程式,再到面向对象。假如这份感觉来的不是那么深刻,让我们来举一个例子来感受下。假如说我们来实现创建订单和采购单两个业务,使用这两种方式会有什么不同。

过程式编程

让我们从面向对象的思维跳脱出来,使用过程式来设计。我们就拿到一个单据,然后告诉我们要创建。单据创建的基本步骤都是: 转换 -> 校验 -> 保存。那么我们用过程式的大概实现应该如下所示:

saveForm(Form form) {
     convert(Form form);
     
     validate(Form form);
     
     save(Form form);
  }

convert(Form form) {
  if (form.type == Form.order) {
     ...
  } else if (form.type == Form.purchase) {
     ...
  }
}

validate(Form form) {
  if (form.type == Form.order) {
     ...
  } else if (form.type == Form.purchase) {
     ...
  }
}

save(Form form) {
  ...
}

或者

saveOrderForm(OrderForm orderForm) {
   convertOrderForm(orderForm);
   validateOrderForm(orderForm);
   saveOrderForm(orderForm);
}

savePurchaseForm(PurchaseForm pForm) {
   convertPurchaseForm(pForm);
   validatePurchaseForm(pForm);
   savePurchaseForm(pForm);
}

...
...

从这段代码中我们最为直接的感受是复杂度变得很高,编程的时候要关注每个步骤,每个步骤里的方法与其他的步骤有关联。所以聪明的Alan Kay 联想到了生物类学,为了完成某个任务,每个细胞都要与其他细胞协同完成,同时每个细胞又有自己的关注点。所以这个点如何去总结呢? 我理解到的是仿真,编程语言对现实世界的仿真支持程度不同,就会呈现出不同的复杂度。所以Alan Kay其实是发现了编程语言应该更具有结构性,运用分治的思想隔离复杂度。

使用面向对象的方式

上面说了那么多,看到过程式的复杂度,那么我们可以回到面向对象的方式了。假如使用面向对象的方式去实现,代码应该会类似以下:

class Order {
   private Long orderId;
   ...
   getter();
   setter();
}

class OrderService {
    convert();
    validate();
    save();
}

class Purchase {
  private Long purchaseId;
  ...
  getter();
  setter();
}

Class PurchaseService {
		convert();
		validate();
		save();
}

还是直观的感受,我们感觉将有联系的放在一起之后,复杂度降低了很多。这正是面向对象的三个核心特性之一: 封装

使用DDD的模式进行实现

从上面看来,我们是不是也能看到DDD的影子? 所以领域驱动设计是面向对象的进一步深入或者叫真正的面向对象。为了区分表面的面向对象,Eric Vans就提出了一些区分的方式,这就是大家熟悉的充血模型和贫血模型。Eric认为这种贫血模型仍然容易造成过程式编程,无法解决业务的复杂度问题。从我们现行的代码里可以感受到,仍然大部分都是过程式编程。假如使用DDD的方式会变成什么样呢?

class Order {
   private Long orderId;
   ...
  
   // 以下是业务方法
   validate();
   save();
}

看起来并没有什么不同,但这只是DDD的起点。

在这个部分里,我们理解了DDD的核心思想正是面向对象的封装, 从这个思想去出发,那么应该如何落地呢?

过程式编程一定不好吗?

我们在玩游戏的时候,当输了会给出这个英雄太弱了的理由。但是总是会被队友"劝",没有弱的英雄,只有菜鸡。这个理论在编程语言中也适合,只有合适的场景没有绝对的好坏。比如现在很好的函数式编程语言,大家用起来都是爱不释手。在复杂的中台都是采用领域的方式去建设。

DDD战略设计

开始之前,我们先看一下wiki上战略的定义: 战略/策略,是指为实现某种目标(如政治军事经济国家利益方面的目标)而制定的高层次、全方位的长期行动计划。历史上著名的战略: 远交近攻、三分天下、联吴抗曹、西进巴蜀、农村包围城市、改革开放。

在DDD落地过程中,可以算做战略设计的步骤,包括领域、子域和核心域的划分,限界上下文、上下文映射图、代码架构几个部分。我们一个个来吧。

领域

从广义上讲,领域即是一个组织所做的事情以及其中所包含的一切。商业机构通常会确定一个市场,然后在这个市场中销售产品和服务。每个组织都有它自己的业务范围和做事方式。这个业务范围以及其中所进行的活动便是领域。

除了领域,我们肯定还听过核心子域、支撑子域、通用子域这几个名词,从名字上可推断语义一二。接下来我们就来举个例子一起感受下。

在设计整个电商系统的时候,假如我们区分出了用户域、订单域、订单物流信息域。如下图所示。

在这里插入图片描述

订单在电商中是必须的,所以它是核心子域,用户是电商每个系统都会涉及,属于基础系统,所以将它看做通用子域,

物流信息这种既不核心也不通用的,被称为支撑子域。在划分领域后,我们已经将自己的业务聚集到了一堆,但是我们的业务都需要互相大交道,所以需要以一个相对隔离的方式联系起来。这时候限界上下文就登场了。

限界上下文

从上图里来说,可以看到实体的线,不仅围住了自己的领域,还交叉了一些其他的领域,这是领域之间的交互。为了理解为什么这么画,我们先来看两个概念,问题空间和解决方案空间。

问题空间: 是领域的一部分,对问题空间的开发将产生一个新的核心域。对问题空间的评估应该同时考虑已有子域和额外所需子域。个人理解问题空间是业务人员或者业务专家的角色对于业务的诉求,比如在电商领域需要有订单、物流这种颗粒度大的领域划分。

解决方案空间: 包括一个或者多个限界上下文,即一组特定的软件模型。这是因为限界上下文即是一个特定的解决方案,它通过软件的方式来实现解决方案。比如订单领域模型、服务、领域事件到最后部署的订单应用等。

从这两个概念可以看出来,问题空间代表的是以业务为主进行领域划分,从限界上下文开始进入到了设计解决方案的阶段。在设计的阶段仍然需要各个角色一起讨论,所以为了避免各个角色对某个概念称呼不一致造成的交流障碍,DDD要求统一语言。当然,只有限界上下文中,统一语言才有可能实现。

因为限界上下文已经属于解决方案空间,所以它一般不会只包括领域模型,还会包括领域服务、领域事件、值对象、schema等。

说了这么多限界上下文,让我们总结一下,要不总是模糊的。让我们来抓住限界上下文的重点: 限界上下文是对领域边界的划分,建立统一语言,业务研发共同设计出领域模型

上下文映射图

U表示上游,D表示下游

大家去网上搜上下文映射图,一般就会看到这样的一张图。看起来只要是上下游的一个定义,那么果真就只有如此吗? 在《实现领域驱动设计》一书中说到,这个映射只是基本的,还需要考虑领域之间交互的方式,比如共享内核、防腐层、开放主机服务等交互设计,这些名词对我们研发可能不熟悉,但是如果换成应用之间的通信方式设计,就会比较简单。这里我们只抓住两个重点: 上下游关系边界交互方式

定义上下游关系重要吗? 从上下游的规则来看,下游应该理解上游的知识,上游应该对下游无感知。这是一种比较理想的模式,在真正的实践过程中,会发现有一些上下游关系因为设计不当,造成边界模糊。大家可以回忆一下,是否在自己系统内做了一些下游的逻辑,造成之后的设计开发困难。

架构

看到这个小节,研发同学终于可以松一口气了。代码架构毕竟是我们接触比较多的,比较熟悉。在DDD落地的时候,经常会采用六边形架构或者叫clean架构。

下面是网上找的一个图,比较全面。主要分为四层:

  • 用户接口层(facade):主要负责api模型基本校验和转换。
  • 应用层(usecase): 负责领域服务的编排,领域事件的发布和订阅,事务等。
  • 领域服务(domain): 是工程的核心,是聚合和领域服务所在的地方。
  • 基础层(infrastructure): 负责远程调用、数据库、文件服务、配置中心等。

虽然看起来比较全面,但是即使设计的领域模型已经非常接近真实的对象,在落地的时候仍然会碰到不小的困难。个人认为会遇到解决的代码难点有:

a. 什么是领域内,什么是不归领域管的,比如保存数据库成功,保存缓存失败这种需要重试的场景。

b. 领域服务和应用服务的区分与设计。

c. 事务一致性的保证。尤其是老项目改造的时候会有很深的体会。

在这里插入图片描述

DDD战术设计

同样的战术设计,我们也看一下它的定义: 战术是指取得小规模胜利或实现小规模优化的技术。在战争、经济、贸易、游戏和协商中战术经常被使用。具体而言战术就是战略的过程,即执行战略的方法。单纯的战术上胜利往往未必能够令战争成功,如姜维的剑阁山之战中就是明显的例子。即使能够对敌阵造成大量伤亡,仍然无法逆转战略的失败。

战略设计给我们已经解决了边界的问题,那么接下里就是要细化的去设计了。这里我们会讲到值对象、实体、聚合、领域服务、领域事件、工厂、资源库。我们把值对象、实体、聚合、领域事件一起讲,因为这几个关联会比较紧密。

实体:许多对象不是由它们的属性来定义,而是通过一系列的连续性(continuity)和标识(identity)来从根本上定义的。只要一个对象在生命周期中能够保持连续性,并且独立于它的属性(即使这些属性对系统用户非常重要),那它就是一个实体。

值对象:当你只关心某个对象的属性时,该对象便可作为一个值对象。为其添加有意义的属性,并赋予它相应的行为。我们需要将值对象看成不变对象,不要给它任何身份标识,还应该尽量避免像实体对象一样的复杂性。与实体主要的区分标准是: 根据什么进行判断是否同一个,如果是标识就是实体,否则就是值对象。但注意实体也能根据值对象去获取,实体也可以只包括几个值对象没有其他属性。

聚合: 根具有全局的唯一标识,而实体只有在聚合内部有唯一的本地标识,值对象没有唯一标识,不存在这个值对象或那个值对象的说法。所有内部实体需要通过根的标识进行访问。

领域事件:用来表示领域中发生的事件。忽略不相关的领域活动,是模型对象中的状态更改的变更通知。

实体和值对象往往是比较容易发现的,但是聚合和领域事件有时候并不容易发现。领域事件的难点不在于发现,而在于判断是否。如果只是研发角色去定义领域事件就会比较危险,这往往会产生一些业务上不关心的事件。多角色共同去沟通设计可以有效避免这个问题。领域事件的问题解决了,但是聚合的这个难题应该怎么办呢? 聚合从语义上是对存在的实体进行分类聚集,就像我们面对一片森林,怎么能够凭借我们的认知去划分成不同的区域,比如灌木林区、桦林区。所以就有聪明的同学说你先观察,观察到他们变化的不同就可以了。这也是事件风暴的起源。

在这里我们简单讲一下事件风暴的逻辑是什么,还是一样的,我们抓重点。事件风暴分为四步:

  1. 命令风暴或者事件风暴。这个阶段需要完成事件和命令的收集,并且按照时间顺序的排列,包括调用者外部系统、角色、自动触发。

  2. 根据第一步的每个命令,可以得到命令的实体对象或值对象。

  3. 根据实体对象在时间轴上的范围,确定生命周期,从而确定聚合。

  4. 细化聚合。按照生命周期产生的聚合有可能很大,或者因为一些其他业务场景会有一些通用,所以需要细化。

扩展阅读: 事件风暴更多的细节可以阅读此文章: https://cloud.tencent.com/developer/article/1556397

领域服务工厂资源库

在《领域驱动设计》中给的描述是有一些行为,需要跨越多个聚合,但是又属于业务的场景,就需要使用领域服务。通过这个描述我们可以感知到一些与应用服务的不同,但是这个界限又不十分清晰。

工厂和资源库都是我们现在也在使用的,所以比较好理解。工厂主要为了解决聚合难以创建的问题,资源库拿出来讲主要是目前现行的资源库框架对于DDD都是不太友好的,难以解决内存和并发的问题,所以在设计聚合的时候也会受到资源库的限制。

总结

看了很多资料,看了书,也写了一些代码,尝试了事件风暴,趁着对DDD小有所感的时候记录一下。当然我认为DDD至今还不能称之为战术,不过也确确实实地在逐步改变我们的认知,影响整个编程世界,细想下来与低代码、人工智能、云计算等现在火热的事情都有关联,值得去思考和学习。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值