• 系统复杂性根源于隐晦(难理解),耦合(难改动)和变化(难扩展),DDD正是应对系统复杂性的重要方法。本文针对B端营销系统设计中的复杂性,从战略设计,战术设计到代码架构,详细介绍DDD在各个阶段的实践。
  • 3 战略设计实践
  • 4 战术设计实践
  • 5 代码架构实践
  • 6 总结
  • 7 参考资料

1 背景

通过营销活动实现客户/用户拉新、留存和促活。为实现商户增长和留存,构建营销系统支撑商户的线上营销运营。在系统建设过程中,面临着业务体量大、行业跨度大、场景多样、客户结构复杂,需求多变等挑战。本文从0到1构建面向商户的营销系统过程中,并通过DDD(领域驱动设计)来应对系统设计和建设中遇到的业务复杂度高、需求多变、维护成本大等问题。

2 基本概念

2.1 软件系统的复杂性

隐晦
  • 抽象层面的隐晦,抽象系统时,每个人都有自己特定的视角,你需要站在对方的角度才能明白他为什么这么做
  • 实现层面的隐晦,代码是一种技术实现,通常与现实世界的业务概念脱节,无形中增加了理解成本
耦合

代码层面的耦合扩大了修改范围;模块层面的耦合需要跨模块/服务交互;系统层面的耦合则需要跨团队协作。从代码到模块再到系统,耦合的影响逐渐扩大,成本随之增加。

变化

业务需求决定了系统功能,不同的用户需求不一样,不同的业务发展阶段需求在不断变化,系统功能要随着业务需求的变化不断调整,这时就涉及到系统改动的频次和范围。

2.2 难点

DDD是应对软件设计复杂性的方法之一,它能很好解决上述三问,但概念体系复杂,学习曲线陡峭,即便深入研读DDD的两本经典著作,项目落地也“捉襟见肘”。

B端营销系统的DDD实践_用例

2.3 DDD历史

  • 早期,计算机创新更多聚焦在语言方面,为软件工程师提供功能更强大的语言来操作计算机,充分使用计算机的算力。
  • 60年代,面向对象语言诞生,通过封装、继承、多态等特性进一步增强了语言的表达能力。
  • 80年代,出现OOP,解决咋构建类模型的问题,帮助我们更好地使用面向对象语言来实现系统,但没有解决如何把物理世界映射到计算机世界的问题。
  • 2000年,出现DDD,通过分析业务,抽取概念,建立对应的领域模型,再采用面向对象的分析与设计方法构建对应的类模型,达成从物理世界到计算机世界的映射

B端营销系统的DDD实践_用例_02

2.4 啥是领域?

由三部分组成:

  • 领域里有用户,即涉众域
  • 用户要实现某种业务价值,解决某些痛点或实现某种诉求,即问题域
  • 面对业务价值,痛点和诉求,有对应的解决方案,这是解决方案域

2.5 啥是领域驱动设计?

针对特定业务,用户在面对业务问题时有对应的解决方案,这些问题与方案构成了领域知识,它包含流程、规则以及处理问题的方法,领域驱动设计就是围绕这些知识来设计系统。

如营销系统所服务的用户有4类:运营、销售、电销人员和商户。解决3个核心问题:

  • 咋发券
  • 发给谁
  • 发啥(红包还是折扣券)

解决方案:通过营销活动来承载发券,不同的活动类型对应不同玩法(如买赠、折扣、充送等);通过目标人群来确定发给谁;通过权益定义发啥(如:红包、代金券、折扣券等)。

B端营销系统的DDD实践_概念模型_03

本文将从战略设计、战术设计和代码架构分3个部分介绍DDD的落地步骤:

  • 战略设计:确定用例,统一语言和划分边界
  • 战术设计:概念模型转化成类(代码)模型
  • 代码架构:将系统设计映射为系统实现

B端营销系统的DDD实践_解决方案_04

3 战略设计

战略设计前,先

3.1 确定用例

即业务咋玩的。常见方法:

  1. 用例图:最简单直观的表达了用户与系统的交互
  2. 用户故事:敏捷开发模式下用的较多,从Who、What和Why三个维度描述了业务需求
  3. 交互原型:用户操作的页面及其操作流程,其缺点是过于关注用户体验,而忽略了业务底层逻辑
  4. 事件风暴:关注业务的底层逻辑,但使用门槛较高。事件->命令->操作人,适用于大型而复杂的业务分析)适用于大型而复杂的业务分析

下图是营销系统的用例图(起初并没有这么完整,这是多次迭代后的结果):

3.2 统一语言

3.2.1 抽取概念

从用例里抽取概念,并对概念进行甄别(去伪存真,抽象合并)找到真正描述业务的概念。

如多种方式描述活动规则:充值送规则、返还规则和档位等,技术可能泛称为规则,业务人员则用档位描述(如充值送活动,充1000送100红包,充2000送300红包,充3000送500红包,那1000、2000、3000就是业务所认为的档位)。抽取概念时,尽量采纳业务侧的叫法,这样统一语言较易推行。

B端营销系统的DDD实践_概念模型_05

3.2.2 明确含义

明确概念的含义,概念由术语、Term(术语的英文版)和含义三部分构成。含义明确的术语就是统一语言,这些术语将用在日常需求沟通、产品文档,技术设计以及代码实现中。

3.2.3 理清关系

明确概念后,理清概念之间的关系(1对1,多对1,多对多),确定概念所代表的的业务实体的核心属性和行为,从而得到概念模型。后续在业务需求讨论、产品和技术方案设计时,基于这个概念模型,使用统一语言进行描述,大家能很容易对齐;同时精心抽出的概念和建立的概念模型更接近业务本质,为后续的战术设计打下了基础。

B端营销系统的DDD实践_概念模型_06

3.2.4 达成共识

基于统一语言和概念模型,业务 - 产品 - 技术三个角色比较容易就需求达成共识,保障沟通的一致性。

缺少这些就很容易出问题,如:刚开始做营销系统,如何描述“商户”,没统一语言:

  • 资金域有三个概念描述商户(资金账户、账号ID、资金账号)
  • 商家域有四个概念描述商户(商家账号、商家ID、登录号、登录ID)
  • 营销域,不同人采用不同概念描述商户

沟通混乱。给商户发红包时,“资金账户、账号ID、资金账号、商家账号、商家ID、登录号、登录ID”这些概念都可描述商户,但业务人员不清楚这些概念之间区别,导致ID误用,红包发错。事后对这些概念进行梳理和统一,营销域只关注资金账户和商家账号,系统功能上明确使用资金账户或商家账号发红包,就不易出错。

B端营销系统的DDD实践_概念模型_07

概念模型是一张大网,描述了概念间的关系以及关键属性,但还不能直接映射为代码模型,要映射为代码模型,还需拆解,化繁为简。

  • 本源论认为世界的本质是简单的,复杂问题由多个简单问题构成
  • 康威原理认为系统架构受制于组织沟通架构,系统落地时,首先要确定系统边界,再依据系统边界组织分工

这两个原理表明:可将复杂问题拆解为多个简单问题,并针对团队资源组织分工协作。

这里给出一种拆解方法,纵横两维拆:

  • 纵,从业务价值和目标维度划分
  • 横,从功能的通用性维度划分

这里尝试从业务角度来拆,没有系统支持时,业务要在线下运转,通常根据要达成的业务目标,将业务流程或业务组分拆解为多个节点,并定义每个节点的职责以及对应的规范和标准,安排对应的组织或人员执行。即从业务问题和解决方案出发,拆解到对应人。因此基于业务的拆分通常能实现系统用户、业务问题和解决方案之间的一致性。业务系统是把业务的玩法从线下搬到线上,在进行系统拆分时,也可使用该思路。从三层进行:

  1. 基于涉众域拆解:也就是按用户相关性进行拆解,不同的用户使用不同的系统功能,如:CRM由市场人员、销售人员、客服人员三类角色协同完成客户触达,签约合作,售后服务三大职能,针对这三个角色建设相应的系统能力。这种拆解方式比较简单,但也存在较大的局限性,可能导致功能的重复建设
  2. 基于问题域拆解:不同角色/用户要解决的问题是相同/相似的,可基于问题域进行拆解,如营销系统的用户包括销售、商户、销运等角色,但它核心是要解决如何发券(活动),发给谁(人群),发什么(权益)的问题。基于问题域的拆解相较于基于涉众域的拆解更加抽象,但也可能复用性不够
  3. 基于解决方案域拆解:不同的问题,可能有相同的解决方案,如HR域有请假审批、财务上有报销流程、CRM领域存在客户资质审批,三个领域各自需要解决审批流程的问题,可以构建通用的审批流引擎来统一解决,这是基于解决方案域进行拆解。基于解决方案域的拆解最抽象,也最贴合业务本质,但也易陷入过度设计的陷阱

营销系统基于问题域拆解为五个子域:

  • 活动域
  • 权益域
  • 人群域
  • 推送域
  • 数据域

每个子域解决特定的问题,各子领域相对内聚和简单:

B端营销系统的DDD实践_解决方案_08

业务系统要运转,需子域相互配合,就要定义上下文映射,实现不同子域间的协作。如活动域关注的两个目标人群:一是资金账户(表示已签约的商户);另一个是商家账号(表示未签约商户)。资金账户是财务域定义的,而商家账号是账号域定义的,两个概念都不是营销域原生概念。此时,营销域需通过某种方式依赖外部概念,将外部概念映射到营销域,通过防腐层来对接外部服务来实现这种映射。

B端营销系统的DDD实践_用例_09

营销系统的整体上下文关系:

B端营销系统的DDD实践_概念模型_10

从用例分析,统一语言到子域拆分,初步完成战略设计,但这并非终局,战略设计是一个持续迭代的过程,迭代来源主要有3个:

  1. 用例精化:在探讨需求的过程中,用例不断丰富
  2. 需求变更:业务不断发展带来需求变化,进而影响用例及相关概念的内涵,概念模型亦随之调整和迭代
  3. 方案选型:当产品,业务或技术发生较大变化时,可能要采用另一种方式实现,这时所采用的概念会有所不同。如早期构建营销活动域,通过参与规则定义谁可参加活动,将商户与参与规则匹配,符合就能参与。这种方式带来问题是无法提供一个完整的活动人群列表,除非将所有商户匹配一遍。随业务方越来越重视活动参与商户的分层,触达和转化,引入目标人群概念,通过目标人群保存所有可参加活动的商户。从参与规则到目标人群,概念变化,底层模型完全不同(参与规则是一套规则体系,而目标人群由筛选服务提供),实现战略设计上的迭代。

有了战略设计,构建了统一语言和概念模型后,咋验证概念模型?两个方法:

  1. 场景走查:把模型代入到所有的场景确认一遍,确定所抽象出来的概念模型和统一语言能正确描述它。
  2. 业务预判:未来业务的变化会在哪里,当变化发生时,概念模型的内涵和外延是否方便扩展并支持到变化。

B端营销系统的DDD实践_解决方案_11

4 战术设计

4.1 目的

战略设计得到概念模型,战术设计则是将概念模型映射为代码模型。

对此,有很多

4.2 编程范式

  • 事务脚本:围绕动词展开
  • 表模式:介于事务脚本与面向对象之间
  • 面向对象:实体,值对象,聚合根,领域服务
  • 函数式:尝试

最好的方式是

4.3 面向对象实现

  • 从概念模型到对象模型
  • 职责决定了封装粒度
  • 封装粒度决定了聚合根大小
4.3.1 从概念模型到对象模型
  • 概念是分层的,如营销活动是泛化概念,其下有充值送活动、消费返活动,买赠活动等具体活动。构建对象模型时,通过派生/继承实现概念分层
  • 概念关系映射成对象关系,如营销活动包含档位、库存,构建营销活动对象时,可通过组合实现这种包含关系(档位对象和库存对象成为营销活动对象的属性)
  • 概念的属性行为,可直接变成对象的属性和行为;概念的状态机以及生命周期也会变成对象的状态机

两类对象:实体和值对象,区别为是否有统一标识和自己的状态。

B端营销系统的DDD实践_解决方案_12

有了对象模型,还需通过聚合根完成封装

咋确定聚合根的粒度?

营销活动包含活动、库存、档位、档位项、目标人群五个对象,若采用

小聚合根模式

一个对象对应一个聚合根,每个聚合根都很简单。但从业务角度看,库存或档位会影响活动的状态,如:修改库存或档位,活动需重新审批和上下线,这种业务耦合需技术处理。就得在小聚合根上构建领域服务封装这些逻辑。

大聚合根

围绕活动,把活动相关概念(活动、库存、档位、档位项、目标人群)都封装,但聚合根较复杂,影响活动加载(一些活动的目标人群上百万,懒加载可解决问题,但增加了复杂度)。

聚合根设计原则
  1. 满足业务一致性、数据完整性、状态一致性。如库存档位和活动状态要一致,在数据上也要完整,不存在无档位的活动,也不存在无库存的活动
  2. 技术限制。有些实体会带来技术挑战,如数据量太大,可抽出来单独考虑。
  3. 业务逻辑不灭,在业务封装与适度的职责边界之间寻找平衡。不管大聚合根小聚合根,业务逻辑永远存在,就看把它放哪

如下图是营销系统的聚合根:

B端营销系统的DDD实践_解决方案_13

聚合根已非常接近代码实现,落地代码时,还会纠结贫血 or 充血模型:

  • Spring MVC通常运行在单例模式,引入充血模型增加理解成本和技术复杂度
  • 不适合放在聚合根里的领域逻辑,可放在领域服务,如:同时存在多个充值送活动时,用户只能参加优先级最高那个,在充值送活动聚合根里会标识活动优先级,但挑选优先级最高的活动并非聚合根职责,但确实是领域逻辑的一部分,即可领域服务实现

从概念模型,类模型到代码实现,整个过程都要使用统一语言。在落地代码时,代码要体现出业务含义,如下图,要避免左边updateStatus()这样的方法,它没有体现业务含义(必须阅读代码实现,才知道这个方法做了什么);图中右边的submitCampaign(),approveCampaign(),cancelCampaign()则有明确的业务含义。

B端营销系统的DDD实践_解决方案_14

5 代码架构实践

完成战术设计后,咋组织代码架构?无论六边形架构、整洁架构还是洋葱架构,本质都是围绕领域模型展开,应用层、基础设施层和外部接口都依赖领域模型:

B端营销系统的DDD实践_用例_15

工程实践,与前三个图本质一样。领域层和应用层次放在中间(两者都属于领域逻辑),基础设施和用户接口依赖中间层:

B端营销系统的DDD实践_解决方案_16

6 总结

  • 大部分系统都不是全新系统,如CRM、HR或SCM等,已有很多业界实践,可充分借鉴这些实践,没必要创新
  • 重视统一语言。无统一语言就不会有概念模型,没有概念模型就不可能有靠谱的代码模型,拿到需求后就开始设计代码模型是不靠谱
  • 领域驱动设计是团队工作。现实没有一个是严格意义领域专家,所有参与这项工作的人都可为领域专家,整个工作可以由技术团队主导,但一定要落地到产品和业务
  • 拥抱变化,持续迭代。模型是相对稳定的,但并非一成不变,业务理解的深度,抽象的角度与方式,业务的变化都会影响到领域模型,领域模型的建立是持续迭代的过程

FAQ

深陷领域驱动设计的概念体系。在代码里生搬硬套领域驱动设计里的概念,比如聚合根、值对象、实体等,掰扯概念之间的细微差异,设计复杂的领域事件等。这反而增加理解成本,让系统变得复杂。领域驱动的精髓在于从业务出发,抽象出业务领域知识,构建概念模型,一步一步将这些概念模型映射成系统。至于如何采用聚合根、领域服务、实体、值对象、领域事件等,可以灵活取舍。

试图通过精心设计来获得领域模型。领域模型不是设计出来的,而是通过战略设计的几个步骤,从业务中抽象出来的,最重要是理解业务,对业务进行抽象。

使用了DDD就一定会产生好的领域模型的想法也不可取,我们知道飞机怎么造,但我们不一定能够造出好飞机,但如果我们知道这个方法,可以少走弯路。

在聊需求的那一刻,设计就开始了,统一语言就是设计的一部分。

解决方案域在模型维度分为四层:

  1. 功能模型:产品表达给我们业务的玩法,我们把它变成了用例,从用例里抽取出功能模型。
  2. 概念模型:对功能模型进一步抽象,统一语言,形成概念模型。
  3. 代码模型:将概念模型映射为代码模型。
  4. 数据模型:业务数据需要存储,需要设计对应的表结构。

这里有两个陷阱:

  1. 看到功能模型后,就开始设计数据模型,考虑数据该怎么创建、怎么更新、什么时候该删除,沦落为CRUD boy。
  2. 看到功能模型后,就开始考虑操作数据的流程是什么,陷入到事务脚本陷阱。(对于一些简单的功能,不排斥使用事务脚本,但对于复杂功能,事务脚本的维护成本非常大)

领域至少可以分为两大类:

  • 学科型,比如财务、会计、图形学、动力学,这类系统的设计须先深入理解学科知识
  • 实践型,如CRM、订单交易等,是业务经验的总结,这类系统的设计不妨参考前人的实践

当然,如果自己的业务具有独特性,那就只能靠自己摸索了。