DDD如何设计落地?(库存,产品账示例)

一. 背景

     本文以预算管控服务建设作为一个DDD设计的例子介绍,目标是是呈现一次DDD设计的过程,为了减少绘图和描述的工作量,文中会对预算管控业务需求和功能做简化。请重点关注设计的流程,这是我们想传达的重点,忽略设计细节的合理性。

另外,对于预算管控服务来讲,不一定要用DDD来进行分析设计,基于传统的数据驱动就完全可以满足需求,但作为介绍DDD实施过程,预算管控是一个不错的例子(不需要画太多的图)。在这里我们不讨论什么类型项目合适DDD,可以参考:

大致的共识为复杂度高的业务适合DDD。而复杂度一般体现在:

业务流程长

业务场景多

业务概念多

业务系统干系人多

业务系统需要长期维护且持续有变更

 

业务背景

需要设计一个适用于本地生活场景的资源预算规划和管控服务,业务需求上主要包括两方面的用例:

  1. 品牌发放权益需要有一定的限额,不能无限制的发放。包括品牌、门店、活动、人群、权益等维度

  2. 个人消费者参与活动领取权益有次数的限制,不能无限额领取或使用。包括在活动、品牌、门店、商品等维度

目前各业务线针对以上需求,各自实现了部分能力,整体上看较零碎、不完善、不统一。本次目标是设计一个统一的平台为各业务方提供基础能力

二. 战略设计

2.1 业务梳理

2.1.1 业务定位&目标分析

协同分析阶段,需要各干系方共同参与,如,业务运营,业务产品,运营产品,平台架构,业务系统方的技术等。

目标:聚焦业务需求和平台定位,确定平台的能力范围和服务方式

输出需求文档:

  1. 提供一个统一的记账能力,以平台的方式为各个系统提供记账服务。

主要功能:

  • 记账

  • 销账

  • 各维度的查账

  • 库存创建

  • 库存扣减、查询

  • 库存缩扩容

细化要求:

  • 作为平台能为客户提供逻辑上的数据隔离,即A产品方默认不能访问B产品方数据。如需要访问需要经过授权同意

  • 作为平台需要提供同步记账能力和异步记账能力,并提供明确的“能力范围”承诺

  • 作为平台需要为产品方提供方案避免重复记账

  • 除记账之外,需要提供对应的销账能力

  • 需要提供常用的记账周期(账期),如时账,日账,周账,月账,季度账,年度账,终身账。

  • 需要提供自定义记账周期(账期)的能力

  • 需要提供一单多账记账能力,即一张单据,需要同时记录日账和终身账

  • 需要提供多维度的查账能力,如按产品方,记账主体,产品,账期时间,以及基于这些维度的组合条件查账

  • 需要提供批量查账能力,如主体下单一产品的批量账期时间,主体下的批量产品的单一账期时间,及其它可能的批量组合

  • 技术上需要保证账单存储和记账动作的事务

  • 技术上需要保证分库分表的数据存储均衡性

  • 技术上需要尽量保证分库分表的数据库读写均衡分布,对可能出现的数据倾斜场景,需要给出明确的说明,和使用限制性规范

  • 能提供性能基准承诺,由测试团队对典型场景压测给出《平台性能报告》,作为平台对外服务的一部分

  • 库存的创建,扣减,查询,扩容,缩容(缩容量不能少于剩余库存)

  • 库存冻结,解冻

  1. 库存管理

主要功能:

  • 库存创建

  • 库存扩容

  • 库存缩容

  • 库存扣减

  • 库存回补

  • 库存查询

细化要求:

  • 满足去UMP的所有要求(去UMP为一个内部项目,各种限定型规则在此不细列)

2.1.2 业务抽象可视化

通过事件风暴或四色建模法来可视化。我们这里选择事件风暴法。过程主要涉及

 

  • 识别领域名词(示意,不包括全部)

DDD如何设计落地?(库存,产品账示例)

 

 

  • 识别领域命令(示意,不包括全部)

这里列了主要的命令

DDD如何设计落地?(库存,产品账示例)

 

  • 场景分析:

主要是识别发出命令的主体是谁,如C端消费者,B端消费者还是某个系统。主要是通个主体在具体Usecase中去串联命令对于领域对象(对应领域名词)的影响。串联业务流程完成领域分析

  • 识别领域事件

在命令发出后对一个领域对象(聚合根)将产生影响,往往对内(聚合根)会生成数据或发生状态变更;对外(向其他聚合根)发送消息或触发事件。

这些事件是业务专家重点关心的结果

 

这里是先识别领域事件,还是先识别命令可以根据设计者的习惯和熟悉度,自行选择

 

最后,整合命令,领域对象和领域事件的关系,得到业务梳理的输出文档(实际命令可能比图中多,如库存冻结和预扣等):

DDD如何设计落地?(库存,产品账示例)

 

2.2 统一领域语言(示意,不包括全部)

2.1章中几个阶段是一个来回讨论的阶段,通常需要经过很多轮的修改和妥协,以至于早期列出的领域名词、领域事件和命令远多于上面的图例,但最后大家需要统一确定其中关键的领域名词、领域事件,并统一领域语言,在后续的讨论和设计阶段均使用统一语言建模。这里我们用下面的统一语言仅示例产品账:

术语

描述

记账主体(principal)(mainPrincipal)(subPrincipal)

记账主体(id),如,抽奖活动中的消费者记账,则为cid

账单(accounting document)(accounting doc)

名词,一次记账请求提交的数据为一条记录。指产品方提交给记账平台的原始单据数据

记账(keep account)

动词,记录record的过程

销账(write off account)

动词组,记账的反向操作

金额(amount)

记账的数量

账(account)

按账期 统计的在该周期内的数额总和相关数据

账期(account cycle)

账期(会计周期)的类型,如,日账,月账,终身账等

账期值(account cycle value)

账期值。如对于自然日类型的账期,账期值可以是“20210415”代表4月15这天的账

记账类型(operate type)

操作类型指,记账或销账

 

2.3 限界上文识别

最后,当领域名词、领域事件和命令都统一并清理好之后,我们需要圈定合适领域出来,这里要注意,并没有统一的最佳答案,圈定原则只是遵循现实世界的松紧耦合关系,某些场景下可能有多种选择,本例较简单,示例如下

DDD如何设计落地?(库存,产品账示例)

 

 

2.4 问题子域识别

在战略设计阶段的最后,按“一个子域负责解决一个独立业务价值的问题”的原则,将限界上下文划分到不同的问题子域(Subdomain)中,同时还需要从更大的域视角来俯览全局,并按照以下三种类型进行标注:

 

  • 核心域(Core Domain):是当前产品的核心差异化竞争力,是整个业务的盈利来源和基石,如果核心域不存在,那么整个业务就不能运作。对于核心域,需要投入最优势的资源(包括能力高的人),和做严谨良好的设计。

  • 通用子域(Generic Subdomain):该类问题在界内非常常见,所以很可能有现成的解决方案,通过购买或简单修改的方式就可以使用。

  • 支撑子域(Supporting Subdomain):该类问题解决的是支撑核心域运作的问题,其重要程度不如核心域,又不属于通用子域,具备强烈的个性化需求,难以在业内找到现成的解决方案,需要专门的团队定制开发。

 

问题子域,是对业务问题的进一步澄清和划分,同时也是对于资源投入优先级的重要参考,相对限界上下文来说,问题子域是对业务问题更大粒度的划分,是在限界上下文识别后与问题域匹配的一个过程。

 

通过对于子域进行识别、划分和类型标注,团队能够实现软件架构在业务边界上的内聚和解耦,便于逆向应用“康威定律”。

 

在 DDD 的概念中,限界上下文和问题子域是两个不同维度的概念,限界上下文可能只是真实问题子域的一部分表达,也可能限界上下文中的一些领域名词超出实际问题子域的范围,理论上来说没有绝对的依赖关系。需要根据实际需求和成本综合考虑,既要保证便资源分配的合理,又要在降低落地成本的同时保证后期演进的适度兼容。

 

问题子域识别过程的产出物,如下图所示:

 

DDD如何设计落地?(库存,产品账示例)

 

2.5 限界上下文映射(示意,不包括全部)

这里只示例产品账的。明确限界上下文映射关系,是为了更明确各context之间的关系,在IDDD中给出了9种关系,在本例种只涉及到3种,实际项目中可能比这个复杂的多,尤其是涉及集成和遗留系统的场景。

明确contex之间关系,有助于后续保证系统之间的依赖关系,为后续架构模式的补充模块做好准备。

 

DDD如何设计落地?(库存,产品账示例)

 

 

 

三. 战术设计

3.1领域建模

3.1.1 领域对象提取(聚合/实体识别)

偷个懒,这里只示意产品账的实体和部分值对象

DDD如何设计落地?(库存,产品账示例)

 

3.2 业务服务识别

业务服务识别,是为后续系统实现进行的基于业务边界的模块拆分分析,常见的拆分方法有:

  • 基于限界上下文进行拆分:每个限界上下文为一个服务,优点是每个服务都很小,代码量少;缺点是拆分粒度太细,导致服务数量过多,增加架构设计的复杂度和运维成本。

  • 基于子域进行拆分:每个子域为一个服务,优点是服务数量相对较少,架构复杂度和运维成本相对更低;缺点是拆分粒度在某些场景下会非常大,导致单个服务变成“小单体”,增加开发成本和代码分层复杂度。

 

通过对于业务服务进行划分,团队能够获得对软件架构模块拆分的直接指导,并且还能够依据“逆康威定律”依据架构结果进行开发团队的划分和组建。

下面是预算管控子域的服务拆分示例

子域

服务

预算管控子域

库存服务

产品账服务

 

3.3 业务服务接口识别

单独对业务服务的接口能力进行识别,是符合面向接口编程原则的,提前定义服务的概要设计方案,可以让后续团队成员更快开展工作,也方便后续接口的详细设计

 

这里提前识别服务接口,是为了避开技术实现细节的影响。我们在基于具体技术实现的情况下设计接口,通常会干扰领域驱动的设计。我们试想下基于swagger文档,设计API时,我们是否容易保证API的归属正确领域服务。所以提前的概要识别和设定很重要

 

下面是库存和账服务接口识别示例:

子域

聚合根/实体

接口能力

读写

账上下文

账单

记账

账本

单主体单产品单账期查账

单主体批量产品单账期查账

库存上下文

库存

创建库存

扣减库存

缩扩容库存

查询库存

 

四. 技术实现

在完成了战略设计和战术设计之后,就可以考虑具体的技术详设,这个阶段会设计到具体的架构模式选择,架构风格和基础技术,存储等的选择。

包括且不限于:

  • 架构风格选择,单体,soa,微服务,restful,rpc,webservice,ODATA等

  • 架构模式选择,传统分层,六边形,简洁,洋葱等

  • 补全组件,如rpc客户端,mtop,gatway,acl等,这里要分清应用层,,基础设施和领域

  • 技术框架选型,技术栈,服务治理体系

  • API设计,openapi,swagger,blueprint等

  • 领域模型类设计,参考领域模型设计类图

  • 持久化选择,这里要考虑哪些需要存储RDB,哪些用Nosql,哪些只需要内存中。在上例产品账中的账本就不需要持久化

  • 应用层设计模式选择,因应用需要,或运营策略需要支持能力要考虑合适的模式支持

  • 考虑其他需求的实现,易测试性,性能,易维护和运维,安全等

  •  

在本例里只示例产品账的领域模型参考:

DDD如何设计落地?(库存,产品账示例)  

 

其中账本(accountbook)不需要持久化,其他领域对象均需要持久化

五. 总结

最后需要时刻提醒的。没到最后实现阶段之前应该杜绝提前考虑技术细节和技术实现,否则很容易偏离DDD

 

上一篇:关于高大上的DDD,白话入门篇


下一篇:白话-raft协议(一)