一. 背景
本文预算管控服务建设作为一个DDD设计的例子介绍,目标是是呈现一次DDD设计的过程,为了减少绘图和描述的工作量,文中会对预算管控业务需求和功能做简化。请重点关注设计的流程,这是我们想传达的重点,忽略设计细节的合理性。
另外,对于预算管控服务来讲,不一定要用DDD来进行分析设计,基于传统的数据驱动就完全可以满足需求,但作为介绍DDD实施过程,预算管控是一个不错的例子(不需要画太多的图)。在这里我们不讨论什么类型项目合适DDD,可以参考:
大致的共识为复杂度高的业务适合DDD。而复杂度一般体现在:
业务流程长
业务场景多
业务概念多
业务系统干系人多
业务系统需要长期维护且持续有变更
需要设计一个适用于本地生活场景的资源预算规划和管控服务,业务需求上主要包括两方面的用例:
-
品牌发放权益需要有一定的限额,不能无限制的发放。包括品牌、门店、活动、人群、权益等维度
-
个人消费者参与活动领取权益有次数的限制,不能无限额领取或使用。包括在活动、品牌、门店、商品等维度
目前各业务线针对以上需求,各自实现了部分能力,整体上看较零碎、不完善、不统一。本次目标是设计一个统一的平台为各业务方提供基础能力
二. 战略设计
2.1 业务梳理
2.1.1 业务定位&目标分析
协同分析阶段,需要各干系方共同参与,如,业务运营,业务产品,运营产品,平台架构,业务系统方的技术等。
目标:聚焦业务需求和平台定位,确定平台的能力范围和服务方式
输出需求文档:
-
提供一个统一的记账能力,以平台的方式为各个系统提供记账服务。
主要功能:
-
记账
-
销账
-
各维度的查账
-
库存创建
-
库存扣减、查询
-
库存缩扩容
细化要求:
-
作为平台能为客户提供逻辑上的 数据隔离 ,即A产品方默认不能访问B产品方数据。如需要访问需要经过授权同意
-
作为平台需要提供 同步记账 能力和 异步记账 能力,并提供明确的“能力范围”承诺
-
作为平台需要为产品方提供方案 避免重复记账
-
除记账之外,需要提供对应的 销账 能力
-
需要提供 常用的记账周期 (账期),如时账,日账,周账,月账,季度账,年度账,终身账。
-
需要提供 自定义记账周期 (账期)的能力
-
需要提供 一单多账记账 能力,即一张单据,需要同时记录日账和终身账
-
需要提供 多维度的查账 能力,如按产品方,记账主体,产品,账期时间,以及基于这些维度的组合条件查账
-
需要提供 批量查账 能力,如主体下单一产品的批量账期时间,主体下的批量产品的单一账期时间,及其它可能的 批量组合
-
技术上需要保证账单存储和记账动作的 事务
-
技术上需要保证分库分表的 数据存储均衡 性
-
技术上需要尽量保证分库分表的 数据库读写均衡 分布,对可能出现的数据倾斜场景,需要给出明确的说明,和 使用限制 性规范
-
能提供 性能基准 承诺,由测试团队对典型场景压测给出《平台性能报告》,作为平台对外服务的一部分
-
库存的 创建,扣减,查询,扩容,缩容(缩容量不能少于剩余库存)
-
库存冻结,解冻
-
库存管理
主要功能:
-
库存创建
-
库存扩容
-
库存缩容
-
库存扣减
-
库存回补
-
库存查询
细化要求:
-
满足去UMP的所有要求(去UMP为一个内部项目,各种限定型规则在此不细列)
2.1.2 业务抽象可视化
通过事件风暴或四色建模法来可视化。我们这里选择事件风暴法。过程主要涉及
-
识别领域名词 (示意,不包括全部)
-
识别领域命令 (示意,不包括全部)
这里列了主要的命令
-
场景分析:
主要是识别发出命令的主体是谁,如C端消费者,B端消费者还是某个系统。主要是通个主体在具体Usecase中去串联命令对于领域对象(对应领域名词)的影响。串联业务流程完成领域分析
-
识别领域事件
在命令发出后对一个领域对象(聚合根)将产生影响,往往对内(聚合根)会生成数据或发生状态变更;对外(向其他聚合根)发送消息或触发事件。
这些事件是业务专家重点关心的结果
这里是先识别领域事件,还是先识别命令可以根据设计者的习惯和熟悉度,自行选择
最后,整合命令,领域对象和领域事件的关系,得到业务梳理的输出文档(实际命令可能比图中多,如库存冻结和预扣等):
2.2 统一领域语言(示意,不包括全部)
2.1章中几个阶段是一个来回讨论的阶段,通常需要经过很多轮的修改和妥协,以至于早期列出的领域名词、领域事件和命令远多于上面的图例,但最后大家需要统一确定其中关键的领域名词、领域事件,并统一领域语言,在后续的讨论和设计阶段均使用统一语言建模。这里我们用下面的统一语言仅示例产品账:
2.3 限界上文识别
最后,当领域名词、领域事件和命令都统一并清理好之后,我们需要圈定合适领域出来,这里要注意,并没有统一的最佳答案,圈定原则只是遵循现实世界的松紧耦合关系,某些场景下可能有多种选择,本例较简单,示例如下
2.4 问题子域识别
在战略设计阶段的最后,按“一个子域负责解决一个独立业务价值的问题”的原则,将限界上下文划分到不同的问题子域(Subdomain)中,同时还需要从更大的域视角来俯览全局,并按照以下三种类型进行标注:
-
核心域(Core Domain): 是当前产品的核心差异化竞争力,是整个业务的盈利来源和基石,如果核心域不存在,那么整个业务就不能运作。对于核心域,需要投入最优势的资源(包括能力高的人),和做严谨良好的设计。
-
通用子域(Generic Subdomain): 该类问题在界内非常常见,所以很可能有现成的解决方案,通过购买或简单修改的方式就可以使用。
-
支撑子域(Supporting Subdomain): 该类问题解决的是支撑核心域运作的问题,其重要程度不如核心域,又不属于通用子域,具备强烈的个性化需求,难以在业内找到现成的解决方案,需要专门的团队定制开发。
问题子域,是对业务问题的进一步澄清和划分,同时也是对于资源投入优先级的重要参考,相对限界上下文来说,问题子域是对业务问题更大粒度的划分,是在限界上下文识别后与问题域匹配的一个过程。
通过对于子域进行识别、划分和类型标注,团队能够实现软件架构在业务边界上的内聚和解耦,便于逆向应用“康威定律”。
在 DDD 的概念中,限界上下文和问题子域是两个不同维度的概念,限界上下文可能只是真实问题子域的一部分表达,也可能限界上下文中的一些领域名词超出实际问题子域的范围,理论上来说没有绝对的依赖关系。需要根据实际需求和成本综合考虑,既要保证便资源分配的合理,又要在降低落地成本的同时保证后期演进的适度兼容。
问题子域识别过程的产出物,如下图所示:
2.5 限界上下文映射(示意,不包括全部)
这里只示例产品账的。明确限界上下文映射关系,是为了更明确各context之间的关系,在IDDD中给出了9种关系,在本例种只涉及到3种,实际项目中可能比这个复杂的多,尤其是涉及集成和遗留系统的场景。
明确contex之间关系,有助于后续保证系统之间的依赖关系,为后续架构模式的补充模块做好准备。
三. 战术设计
3.1领域建模
3.1.1 领域对象提取(聚合/实体识别)
偷个懒,这里只示意产品账的实体和部分值对象
3.2 业务服务识别
业务服务识别,是为后续系统实现进行的基于业务边界的模块拆分分析,常见的拆分方法有:
-
基于限界上下文进行拆分: 每个限界上下文为一个服务,优点是每个服务都很小,代码量少;缺点是拆分粒度太细,导致服务数量过多,增加架构设计的复杂度和运维成本。
-
基于子域进行拆分: 每个子域为一个服务,优点是服务数量相对较少,架构复杂度和运维成本相对更低;缺点是拆分粒度在某些场景下会非常大,导致单个服务变成“小单体”,增加开发成本和代码分层复杂度。
通过对于业务服务进行划分,团队能够获得对软件架构模块拆分的直接指导,并且还能够依据“逆康威定律”依据架构结果进行开发团队的划分和组建。
下面是预算管控子域的服务拆分示例
3.3 业务服务接口识别
单独对业务服务的接口能力进行识别,是符合面向接口编程原则的,提前定义服务的概要设计方案,可以让后续团队成员更快开展工作,也方便后续接口的详细设计
这里提前识别服务接口,是为了避开技术实现细节的影响。我们在基于具体技术实现的情况下设计接口,通常会干扰领域驱动的设计。我们试想下基于swagger文档,设计API时,我们是否容易保证API的归属正确领域服务。所以提前的概要识别和设定很重要
下面是库存和账服务接口识别示例:
四. 技术实现
在完成了战略设计和战术设计之后,就可以考虑具体的技术详设,这个阶段会设计到具体的架构模式选择,架构风格和基础技术,存储等的选择。
包括且不限于:
-
架构风格选择,单体,soa,微服务,restful,rpc,webservice,ODATA等
-
架构模式选择,传统分层,六边形,简洁,洋葱等
-
补全组件,如rpc客户端,mtop,gatway,acl等,这里要分清应用层,,基础设施和领域
-
技术框架选型,技术栈,服务治理体系
-
API设计,openapi,swagger,blueprint等
-
领域模型类设计,参考领域模型设计类图
-
持久化选择,这里要考虑哪些需要存储RDB,哪些用Nosql,哪些只需要内存中。在上例产品账中的账本就 不需要持久化
-
应用层 设计模式 选择,因应用需要,或运营策略需要支持能力要考虑合适的模式支持
-
考虑其他需求的实现,易测试性,性能,易维护和运维,安全等
-
在本例里只示例产品账的领域模型参考:
其中账本(accountbook)不需要持久化,其他领域对象均需要持久化
五. 总结
最后需要时刻提醒的。没到最后实现阶段之前应该杜绝提前考虑技术细节和技术实现,否则很容易偏离DDD