Fork me on GitHub

去哪儿网|国内酒店交易 DDD 应用与实践——理论篇

图片

第二篇:去哪儿网|国内酒店交易 DDD 应用与实践——代码篇

作者介绍—— 李鑫

  • 2014年加入去哪儿网机票目的地事业群
  • 担任软件研发工程师,现负责国内酒店交易技术团队
  • 涉及订单交易、订单履约、资金结算、预售等相关业务领域迭代扩展和系统架构优化,对高并发、微服务高可用,有建设优化经验
  • 专注业务复杂度分离和突破技术复杂性、提升系统弹性之道
  • Qunar技术系列-领域驱动设计优秀讲师

一、前言

一般情况软件设计开发主要分两种:

【瀑布式】经过大量的业务分析后,会基于现有需求整理出一个基本模型,再将结果汇总传递给开发人员形成需求文档;我们发现瀑布式研发的方式最大的缺点就是反馈频率低,而且最终交付结果和预期可能产生会较大偏差。

【敏捷式】前期同样也需要经过大量的业务分析,但敏捷式的核心重点不是快,简单地来说,敏捷开发并不追求前期完美的设计,而是力求在很短的周期内开发出产品的核心功能,尽早发布出可用的版本,后续不断迭代升级,同时拥抱变化。

敏捷式研发的缺点,可能存在大量需求或业务模型变更、最终导致系统维护成本会变大,对开发和系统伸缩性各种要求也比较高。

二、DDD 是什么

领域驱动设计,通常需要进行大量的业务的知识梳理,然后软件设计最后软件开发,在业务知识梳理梳理过程中,必然会形成某个领域知识,根据领域知识一步步驱动软的设计,就是领域驱动设计的基本概念

在微服务架构的模式下,DDD 又一次走进我们视野,成为微服务架构的指导思想。DDD 体系比较庞大,最核心的是领域边界的划分,对于我们开发来讲,学习 DDD 思想,应用 DDD 战术工具,对于提升我们的业务架构能力,驾驭复杂业务系统的设计都会有一定提升和帮助;

同时 DDD 也是一种架构设计方法论,通过事件风暴、让我们能够更清晰发现和理解业务价值,抽象化构建通用语言,帮我们设计出清晰的领域和边界,再通过边界划分将复杂业务的领域化,领域驱动设计并不是降低业务的复杂性。

三、DDD 有什么价值

清晰的业务模型边界,可以让我们更专注业务视角,够帮助业务人员和开发人员梳理清楚复杂的业务规则,开发出来的软件也能够准确表达业务规则;

通过建立通用语言、可以减少产品与研发、业务人员之间的沟通鸿沟,在不断完善领域模型过程中可以使用新的工具战略技术、战术技术, 让我们更清晰的划分模块与依赖、进行业务建模或应用于微服务拆分等。

四、DDD 核心支柱是什么

通用语言和限界上下文是 DDD 两大核心支柱。

如何构建通用语言,其最大的挑战就是需要我们花费时间、精力去思考、分析业务和专家沟通构建改进通用语言,最终达成共识,再通过事件风暴,划清业务边界、分析领域模型、识别出限界上下文。

五、DDD 核心概念

让我们先初步认识 DDD 中重要概念包括的内容:战略设计、战术设计、领域、限界上下文、实体、值对象、领域服务、聚合、工厂、领域事件、事件风暴、仓储等,接下里我们讲结合案例为大家一一讲解:

六、DDD 战略设计

图片

战略设计是让我们从业务视角出发,建立业务领域模型,划分领域边界,建立通用语言、识别限界上下文的过程。

如何实现战略设计,就是通过事件风暴,采用用例分析或场景分析拆解业务,建立领域模型,梳理领域对象之间的关系;

而事件风暴过程必定会产生各种实体、命令、事件等领域对象,我们将这些领域对象归类形成聚合、同时划分限界上下文,建立领域模型,这是一个从发散到收敛的过程;

战略设计思想

图片

战略设计思想、需要考虑问题空间与解决方案空间:

什么是问题空间, 其实每个业务都有一个对应的业务模型,其中需要注意的业务模型不是领域模型,在我们设计业务模型的过程中无需关注软件设计思想(比如对象的抽象、继承、存储、性能),而是我们需要从业务本身出发,分析提炼业务边界范围、业务概念和确认业务概念之间的关系;

什么是解决方案空间, 我们在进行DDD领域驱动设计实践时,需要进行需求分析、领域划分、领域建模等工作,系统落地实时则需要考虑解决方案,但是如果这个解决方案过大,其弊端必定会导致各模块或组件都耦合一起,不利于整个系统的维护、演进、伸缩。

所以我们需要把解决方案拆分为一个个独立【领域】和【解决方案】,领域和解决方案本别代表 问题空间 和 解决方案空间。

解决方案该如何拆分?需要我们分析业务结合原则最终得出一个综合考虑后的拆分结果。所以,解决方案的拆分是多维度的,可能需要从系统性能,架构,伸缩性等角度拆分,多考虑一些因素,使我们更好的进行解决方案空间的拆分。

【领域模型】 ,是DDD软件设计方法论中的核心概念,它是业务分析、软件设计的综合结果,是一个系统设计模型。领域模型存在于某个解决方案空间里。

所以,任何一个领域模型,都是在特定的边界内才有意义,同时领域可以划分大小,领域划分,而划分出来的【子领域】简称子域,每个子域对应一个小的问题域和业务;

因为各个子域的重要性不同,所以才有了核心子域支撑子域 的区分;限界上下文, 就是为了表达上面某个解决方案的上下文边界。

七、DDD 战术设计

图片

图示 DDD 战术建模元模型体系图, 通过元模型我们会对【战略建模】过程中识别出来的问题子域进行抽象, 元模型图用于描述如何去创建一个 DDD 的模型。

DDD 的战术建模包括如下内容:

实体-Entity、值对象-Value Objects、领域服务-Domain Services、领域事件-Domain Events、模块-Modules、聚合-Aggregate、资源库-Repository

战术设计原则

图片

图示战略设计思想脑图。

业务模型 并不是领域模型,而是业务概念,其核心是提炼业务核心价值。

领域 是业务边界,是问题空间和业务边界,或者是不同子域,是一个个问题域( 核心域 + 支撑子域)的组合。

解决方案 需要进行业务分析、领域划分 、领域建模,其原则是依据业务和设计原则模式、进行多维度考虑(如系统性能、架构演进等)。

领域建模 是业务模型 加 模型提炼的过程,而模型提炼需要我们依赖设计思想原则。

限界上下文 是边界,有了这个边界我们才可以准确表达和定义边界内的领域模型中所有对象概念含义,而缺失限界上下文边界,则会造成对同一概念在不同上下文的理解偏差。

八、通用语言

针对对同样的领域知识,不同的参与角色可能会有不同的理解,为了避免沟通鸿沟,DDD 领域建模过程中就出现了“通用语言”和“限界上下文”这两个重要的概念也是 DDD 两个核心支柱。

通用语言是在通过团队交流、沟通、协商形成的统一定义,其作用能够简单、清晰、准确描述业务涵义和业务规则的一种语言.通用语言也包含业务术语,可以直接反映在代码逻辑,其中通用语言中的名词可以给领域对象命名、通用语言中的动词则表示一个动作或一个对应领域事件或者一个命令。

通用语言贯穿 DDD 的整个设计过程,基于它,我们就能够开发出可读性更好的代码,将业务需求准确转化为代码设计。

那么如何构建通用语言: ** 事件风暴**

图片

九、事件风暴

图片

事件风暴,是一种快速探索复杂业务领域建模的实践, 从领域中关注业务事件,过程中团队经过充分讨论,统一语言,找到领域模型。

通过收集寻找领域事件、命令、角色,聚合、划分领域,识别限界上下文,划分子域(核心域、支撑域、通用域、或其他子领域)。

事件风暴过程回顾

图片

以上都是基础概念、接下来我们将结合案例讲解

1、分析订单问题空间、确认解决方案空间

酒店交易在落地 DDD、深入分析订单问题空间,从酒店订单、搭售和预售等订单流程,抽象分析,从用户生单、落地存储、支付退款解冻、交互平台支付中心、订单完结通、结算等业务流程中,有高度重合现象。抽取大致以下 7 点共同特征,如产品快照、规则解析、资金抽取、支付表单、交互支付中心、履约流程和逆向流程等。

经过高度抽象问题域形成解决方案空间,构建订单领域模型、形成稳定区间、确认不同问题域解决方案空间的大小。如图示

图片

2、识别角色、命令、事件

图片

如何寻找领域事件

一个领域事件 可以理解为是发生在一个特定领域中的事件,但是并不是所有发生过的事情都可以成为领域事件。

一个领域事件 必须对业务有价值,有助于形成完整的业务闭环,也即一个领域事件将导致下一步的业务操作。

图示:酒店交易先住后付的业务流程、通过事件风暴、场景分析、定义要素同时结合订单业务正向、逆向流程、履约流程等的全貌梳理;

识别订单流程作用角色、用户、代理商、运营、酒店等多种业务角色;

理解不同的业务角色在业务流程中需要发生什么事件、下一步触发什么动作;

通过以上流程、识别出了创单事件、授信事件、降级事件、取消事件、离店事件、扣款事件、欠款事件、还款事件、完结事件、催收事件和订单履约事件几十个领域事件;

3、确认聚合、划定边界

图片

酒店交易 DDD、识别划分报价领域、订单领域、支付领域、履约领域、调度领域等,其中

订单聚合根:解决信息流、资金流、状态、父子单业务、逆向流程等、封装处理订单问题

支付聚合根:解决支付业务分账、资金正向、逆向流程、与支付中心交互、标准 API 输出、幂等性考虑等、封装处理资金问题

履约聚合根:解决订单履约业务、确认、审核、封装处理 OFC 问题

聚合原则包括:

原则1

如果领域对象不能作为一个独立存在的对象。它应该被另一个领域模型持有和使用,可以考虑把两个模型合起来, 形成一个聚合。

在最上面的模型就是这个聚合的聚合根,其之下的模型都是它的实体或值对象找到领域模型以后,我们应当就可以比较轻松地划分子域和限界上下文了。

在划分限界上下文的时候也可以反过来检验领域模型和通用语言的正确性。如果发现一个模型有歧义,应该重新思考这个模型,必要时进行拆分。

a. 双向依赖:上下文之间缺少一层未被确认的上下文,或者两个上下文其实可被合为一个;

b. 循环依赖:任何一个上下文发生变更,依赖链条上的上下文均需要改变;如何确认清晰的BC,依赖业务架构能力、丰富的行业知识、系统分析能力;如果是陌生的领域、可以通过敏捷迭代不断调整领域模型、演进修改最终得到合理的BC;

原则2

聚合内,需要保证强一致性,其方式需要考虑聚合根实体统一存储;

聚合间,可以考虑通过异步事件驱动、结合重试补偿达到业务最终一致性;

3.1 订单领域

图片

图示:酒店交易订单领域 聚合根定义、包括实体、值对象、领域服务设计;

在设计和构建聚合根、需要保证和遵守以下原则:

**原则1【一致性原则】**聚合是由在一致性边界内的实体和值对象组成,创建一个聚合最基本的原则是领域对象的群集必须基于领域不变条件。领域不变条件是指无论何时数据发生变化都必须满足的一致性原则,这个一致性原则一定要满足真正的业务规则;

原则2【保持不变条件】 要让聚合保持一致性,其组成部分就不应该被外部访问。需要通过为聚合选择一个实体作为聚合根,其聚合根行为方法可以外部进行交互;

原则3【一个事务只修改一个聚合】 大聚合会导致事务失败的几率变大,更新冲突变多,如果我们发现需要在一个事务中修改多个聚合,可以采用最终一致性或其他解决方案;

原则4【小聚合】 聚合应该设计的尽量小,大聚合存在以下缺陷:

缺陷1:大聚合会降低性能,因为大聚合会增加额外的查询,可能导致性能降低。

缺陷2:通常会使用乐观锁版控机制来进行数据库的变更操作,大聚合可能包含了很多业务职责,当多个命令对单个聚合进行操作变更,并发冲突的几率会变大,事务失败的几率也会随之变大。

缺陷3:大聚合扩展性差,意味着更多的模型会产生依赖关系。

原则5【在边界外使用最终一致性】 实现最终一致性的技术手段有很多,可以考虑MQ机制,在事件消费的流程中处理对其他聚合的变更。既可以满足一个事务只修改一个聚合这一基准原则,又可以借助消息队列中间件提供的失败重试机制,完成最终一致性、保证业务准确性。

酒店交易拆分订单聚合、支付聚合、使用最终一致性来保证双方状态的一致性,避免大聚合。

图片

3.2 实体

【实体(Entity)】

一个实体模型就是一个独立的事物,具有业务属性和业务行为。每个实体都拥有一个唯一的标识符;

可以对一个实体进行多次修改,修改后的数据和原来可能会不同,但它们依然是同一个实体,因为唯一标识没变

实体模型分四类

图片

失血模型 简单来说,就是对象只有属性的 getter/setter 方法的纯数据类,所有的业务逻辑完全由 business object 来完成。通常在 MVC 模式下一种实体定义的数据模型,仅是一个数据的载体,没有业务含义。

贫血模型 domain object 包含了不依赖于持久化的领域逻辑,而那些依赖持久化的领域逻辑被分离到Service层。贫血模型切分的原则是什么呢?可重用度高的密切关联的放在实体中,可重用度低放在Service中。

模型的优点:各层单向依赖,结构清楚,易于实现和维护。

模型的缺点:Service 层过于厚重。

充血模型 充血模型和第二种贫血模型类似、最大的区别是如何划分业务逻辑,绝大多业务逻辑都应该被放在 domain object 里面(包括持久化逻辑),而 Service 层应该是很薄的一层,仅仅封装事务和少量逻辑,不和 DAO 层打交道。

模型的优点:符合 OO 的原则 Service 层很薄.

模型的缺点:如何划分 Service 层逻辑和 domain 层逻辑需要标准化,在实际项目中,由于设计和开发人员的水平差异,可能导致整个结构的混乱无序

胀血模型 domain object(事务封装,业务逻辑) <---> DAO

模型优点:简化了分层

模型缺点:domain object 暴露给 web 层过多的信息,很多不是 domain object 的逻辑也强制放到模型中、导致模型的不稳定。

以上模型的分类说到底是粒度的问题:

颗粒度细重用好,但类多、结构复杂、繁琐;颗粒度粗(干脆用一个类干所有的事)重用差,但类少、结构简单;

传统开发模式,将数据与业务逻辑彻底分离,通过 get set 方法改变对象属性,但是违反了 OOP 的三大特性之封装特性。面向过程的编程方式不用使用太多的设计模式和过多的设计,缺少了拓展性;

充血模型的设计要比贫血模型更有难度,需要转化思想,当我们设计一个复杂业务场景是需要充血模型DDD、同时也遵循了OOP特性(封装、继续、多态(抽象),如果一个实体行为方法过多、可以考虑结合业务逻辑拆分和设计模型解决该问题;

3.3 值对象

【值对象(Value Object)】,用于描述领域的某个方面而本身没有概念的对象称为值对象,值对象被实例化之后用来表示元素,对于这些元素,我们只关心它们是什么,不关心它是谁。无状态、不可变,没有身份标识,属性可度量或者描述 当度量和描述改变时,可以用另一个值对象予以替换。

实体和值对象的区别

图片

3.4 应用服务、实体行为、领域服务区别

图片

十、理论小结

图片

图片

1:一种细想、一种科学方法论,从战略到战术设计过程是我们设计思路更清晰、更规范;

2:领域驱动处理高度复杂业务的方式,不是降低复杂度,而是让我们去思考如何应对、建议核心稳定的领域内核模型,有利于知识领域传承;

3:领域驱动强调团队和领域专家合作,建立沟通良好的团队;

4:有助于 RD 提升 OO 思想和驾驭复杂业务系统设计能力,战略设计为我们提供一种高层视野来审视我们的软件系统,主要包括领域/子域、通用语言、限界上下文和架构风格等概念, 战术设计的目的是保证战略的实现;

在 DDD 中,再需要那些无法得到实时更新的设计文,代码就是设计本身;

5:创建行为饱满的领域对象,将领域对象当做是服务的提供方,而不是数据容器,多思考一个领域对象能够提供哪些行为;领域模型也是随着业务发展不断迭代演化完善、服务用户、做好产品、体现平台价值。

本文主要围绕DDD领域驱动设计核心概念和理论,从战略到战术进行了分享总结,希望对大家有所帮助,下一篇 ——我们结合酒店交易业务流程,分享酒店交易DDD领域驱动实施落地过程及代码层面注意事项和标准、原则。


本文地址:https://www.6aiq.com/article/1652457838293
本文版权归作者和AIQ共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出