本文整理自美团技术沙龙第73期《基于领域驱动设计(DDD)的架构演进和实践》,主要介绍了DDD的核心概念、常见的设计思路,并结合DDD介绍大众点评交易系统的演进过程,最后做了一些总结和思考。希望这些内容能够对大家有所帮助或启发。
1 大众点评交易业务介绍
本文主要涉及境外出行、商场团购和内容商业化等三类交易业务场景。在大众点评App里,在境外城市站有美食、购物、商场、景点、门票、当地玩乐等频道入口,可以购买境外出行交易产品,在境内的逛街/商场频道可以找到商场团购优惠以及商场团购代金券。
此外,商家如果有推广需求可以在商家端App(开店宝App)“点星”入口购买达人的创作服务,最终达人交付的笔记,在点评App信息流里进行展示。具体来说,境外出行产品覆盖景点门票、餐厅订座和休闲娱乐;商场团购产品包含普通团单和秒杀团单,适用于商场的优惠活动;内容商业化产品则允许商家购买达人的图文或视频笔记,以此来推广自己的服务或产品。
2 领域驱动设计概述
| 2.1 什么是领域驱动设计
领域驱动设计是一种软件设计方法,它主要用于处理复杂业务需求。我们可以将其分解为“领域”、“驱动”和“设计”三个部分来理解。“领域”指的是特定的业务范围或问题域,如电商、医疗、保险等。确定领域后,我们就能明确核心的业务问题。例如,在电商中,核心问题可能涉及商品、库存、仓储和物流;在保险领域,则可能关注投保、承保和理赔等方面。
“设计”在DDD中通常指的是领域模型的设计,DDD强调领域模型是系统的核心,它反映了业务概念和业务规则。“驱动”有两层含义:一是业务问题域驱动领域建模的过程;二是领域模型驱动技术实现或代码开发的过程。确保领域模型的准确性是关键,因为它可以保证代码实现能够真实反映并解决业务的核心问题。
领域驱动设计是一种处理高度复杂领域的设计思想,它通过分离技术实现的复杂性,围绕业务概念构建领域模型来控制业务的复杂性,以解决软件难以理解、难以演化等问题。领域驱动设计是一种设计思想,首先体现了分离的思想,它分离了业务复杂性和技术复杂性,其次体现了分治的思想,它通过领域模型、限界上下文或子域进行分治。
| 2.2 领域驱动设计核心概念
领域驱动设计涉及到的核心概念非常多,我们重点强调一下“统一语言”和“限界上下文”。“统一语言”贯穿领域驱动设计从战略设计到战术设计到最后的代码实现全过程,对于需求分析、知识提炼和最后代码的实现,都是非常重要的。
“限界上下文”是连接问题空间和解决方案空间的桥梁,一方面我们在问题空间分析问题时,它是语言的边界和模型的边界;另一方面,在解决方案空间我们通过限界上下文来确定应用的边界和技术的边界,从而帮助我们确定整个系统及各个限界上下文的解决方案。
| 2.3 领域驱动设计的过程
首先,领域驱动设计需要业务、产品、研发以及QA共同来参与,应基于对问题域以及业务愿景的理解,并进行充分讨论而达成统一认知,在这过程中提炼领域知识,并建立统一语言。同时在领域知识基础上进一步提炼,分解问题域为核心子域、支撑子域和通用子域,再通过模型驱动设计思想,设计领域模型,通过领域模型连接业务和系统,并且在模型驱动设计过程中,会有新的认知迭代。通过这些认知迭代进一步丰富统一语言,因此领域知识是一个不断迭代、螺旋式推进的过程。
3 大众点评交易系统演进
点评交易系统的发展历程从业务视角和技术视角看,分别有三个阶段。从业务视角看:
第一阶段是单业务线单业务形态阶段,这个阶段我们只支持了境外出行交易业务场景,包含了预订的业务形态;
第二个阶段是单业务线多业务形态阶段,业务形态变得更加丰富;
第三个阶段即多业务线多业务形态阶段。
而从技术视角看,主要是经历了包括简单架构、微服务架构和平台化架构等三个阶段的演进。
| 3.1 简单架构阶段
这个阶段是我们业务和系统起步的阶段,当时我们只支持了预订形态的一两个品类的交易,整体上相对比较简单,同时我们当时团队的规模也很小,为了快速支持业务上从0到1这个过程中不断的探索和试错,我们在技术系统建设的主要思路是按照业务环节对业务功能模块做了一些简单的划分,从而做到能够快速的迭代和交付。
在这个阶段,我们的系统架构也相对简单,根据业务进行了基础的拆分。具体来说,接入层分为商家B端、商品C端和订单C端,而服务层则划分为商家、商品和订单三个部分。整体上采用了传统的MVC分层架构。这种架构在项目初期确实展现出了其优势,即“简单”和“快”。
然而,随着业务需求的不断增加和变得复杂,系统开始暴露出一些问题,主要可以归纳为两个方面:
首先,我们采用的是数据驱动设计,通常是先建立数据库表,这导致模型无法直观地反映业务实际情况。其次,由于采用了传统分层架构,我们将数据库表映射为持久化对象(PO),然后在服务层通过CRUD操作进行过程式编程。这在多个场景下出现了功能相似但又有所不同的需求时,经常导致重复编写相似的代码,最终造成了逻辑上的分散,系统整体的内聚性不足。
以订单退款逻辑为例,我们面临着包括订单确认前/后退款、履约前/后退款等多种场景,以及需要考虑由用户(买家)、商家(卖家)、客服和系统等不同角色发起的退款。虽然这些不同场景和角色发起的退款业务逻辑在很大程度上是相似的,但它们之间也存在一些差异。
在传统的MVC架构模式下,由于缺乏对业务领域的深入理解和沉淀,服务间的调用往往缺乏清晰的结构,导致逻辑交织在一起。此外,研发团队在系统迭代过程中可能没有足够重视高内聚和低耦合的设计原则。因此,系统内部往往会出现多处重复且相似的订单退款代码逻辑,这不仅降低了系统的可读性,也给系统的可维护性带来了挑战。
| 3.2 微服务化阶段
随着业务品类的增加和业务模式的多样化,我们的业务和系统复杂度迅速上升,团队规模也相应扩大。这种复杂性主要由三个因素造成:
首先,业务规模的扩张带来了系统规模和代码量的增加;其次,业务需求的累积导致了系统内部的重复代码、复杂的依赖关系,以及为了满足高可用性和高性能需求而引入的各种技术组件和并行、异步解决方案;最后,业务需求的频繁变动也增加了系统的复杂性。
为了应对这些挑战,我们的主要思路是:通过分治的方法来管理软件规模,利用系统分层和关注点分离的原则来优化系统结构,以及通过隔离变化来应对频繁的需求迭代。这些策略都是领域驱动设计(DDD)的核心理念,基于此,我们实施了微服务架构的拆分,以更好地管理和控制系统复杂性。
在领域驱动设计的落地方法上,我们参照行业实践内容并且结合自身的理解,我们将DDD的实施过程划分为以下四个阶段:
理解问题域:这个阶段的核心是深入分析业务价值、需求以及构建业务概念模型。产出统一语言和子域划分,确保团队在业务理解上达成共识。
识别限界上下文:在这一阶段,我们通过组织、业务和应用的边界来确定限界上下文,并且明确不同上下文间的关系和交互。
领域建模:包括领域分析、设计建模,以及模型的持续迭代。这个阶段的目标是构建能够反映业务核心概念和规则的模型。
模型实现:实现阶段主要依赖于应用分层架构、微服务架构和应用集成,确保领域模型能够在系统中得到有效实施。
理解问题域
业务价值分析有助于评估系统的复杂性,并且可以指导我们识别最为关键的业务领域。业务需求分析是一个关键的知识提炼过程,其中涉及多种方法和工具,例如事件风暴、四色建模以及用例分析等,我们采用的是相对轻量的用例分析法。
在进行用例分析之前,我们首先需要对业务流程进行细致的分析。这一步骤通过拆解业务流程和环节,帮助我们发现和识别具体的业务用例。这里简化了交易业务流程的分析,因为大多数人对电商类业务流程较为熟悉。对于不熟悉的业务领域,我们将需要进行更深入的业务流程和场景分析。
我们将交易业务流程分为四个主要部分:客户合作流程、商家上单流程、在线交易流程和资金结算流程。有了这些流程的分解,我们就可以进入到具体的用例分析阶段。
在用例分析阶段,我们以商家上单流程和在线交易流程为例来说明。在商家上单流程中,涉及到的主要角色包括商家和运营。商家负责创建新商品、商品上架、商品下架以及更新商品库存等操作。而运营人员则参与商品审核,包括审核通过、审核驳回、查看审核列表等关键用例。至于在线交易流程,其参与方主要是买家、商家以及客服。买家的行为包括购买商品、支付、申请退款和查看订单等,商家则处理订单确认、发送凭证、核销凭证和订单检索等关键用例。客服则参与售后服务,涉及订单退款、订单赔付等核心用例。
在完成业务流程和用例分析之后,我们可以根据相关性对问题进行初步分类,并划分为不同的子域,建立统一语言。为了更好地进行知识提炼,为识别限界上下文和建立领域模型提供必要的信息,我们需要深入分析每个用例,并制定用例规约来提取关键概念。
在实际操作中,我们没有严格制定用例规约,而是使用产品需求文档中的描述。在技术方案设计阶段,我们也会使用类似于时序图和接口描述的方法来详细阐述用例。无论采用哪种描述方式,关键在于坚持使用统一语言,这对于从描述中提炼出核心概念至关重要。这样做不仅有助于团队成员之间的沟通,也便于后续的设计和开发工作。
在业务流程分析、用例分析以及用例规约的制定和编写之后,我们对交易业务的领域知识已经有了充分的了解,并构建了相应的概念模型。在这个模型中,销售签约商家,商家负责商品的创建,用户选择商品进行下单,下单购买过程中可能会使用优惠,在订单完成之后需要财务介入对商家进行结算。
对这些关键的概念进行归类之后,我们识别出了商家、商品、订单、优惠和结算等几个子域。这里或许会产生一个疑问:对于我们已经熟悉的领域,是否真的需要经过这样复杂的分析和提炼过程来划分子域?实际上,对于有经验的架构师而言,确实可以迅速地完成子域的识别和划分,这也展示了领域驱动设计过程中的一种艺术性。然而,这些系统性的分析步骤确保了即使是不熟悉领域的团队成员,也能够准确地理解业务并作出恰当的架构决策。
在问题域分析阶段主要的输出包括两大部分:一是统一语言,二是子域划分。
在统一语言上,通过用例分析我们提炼了商家、买家、商品等统一语言,通过用例规约的整理对统一语言进行了丰富,包括售卖规则、售卖单元、订单项等等,我们可以使用这些统一语言进行交流并且用于后面的模型设计和代码实现。
在子域划分上,我们最终识别出了如图所示的这样几个子域,结合我们在价值分析阶段得到的为用户提供一站式服务体验,以及为商家提供一体化售卖平台的这样的核心价值,我们将商品域和订单域作为核心域进行重点建设。
此外,对于子域的划分方法,可以分别按照业务和组织两个视角来看,从业务视角上可以按照业务环节或业务方向进行划分,我们使用的其实就是按照业务环节来划分的,将商家合作到商品上单再到交易和结算的整个业务流程进行阶段划分,按照划分出来的每个环节确定子领域。
当目标系统为客户提供多个业务方向的产品时,可以根据业务方向进行子领域划分,比如银行系统可以从储蓄、理财、外汇等几个方向来进行拆分;当目标系统用于企业的管理时,可以从组织视角按照业务职能部门进行划分。
识别限界上下文
在对问题域进行了充分的分析之后,我们进入了限界上下文识别的阶段。前面提到了限界上下文的重要性,它是连接问题空间与解决方案空间的重要桥梁。一方面我们在问题空间分析问题时,它是语言的边界和模型的边界,也就是业务的边界,另外在解决方案空间我们通过限界上下文来确定应用的边界。
所以我们在限界上下文识别的时候,也主要是从业务边界和应用边界两方面来进行。首先我们基于语义相关性和功能相关性对我们在问题域分析阶段所罗列的业务活动进行归类,优先考虑功能相关性,得到初步的限界上下文划分,在我们交易系统的分析过程中,这个结果与子域划分结果基本上是一致的。
那么限界上下文具体要到什么粒度呢,这里跟我们的业务复杂度、技术复杂度以及团队规模有一定的关系,结合我们的实际情况,我们对商品和订单这两个核心域的限界上下文做进一步的识别和划分。
仔细思考后我们发现,尽管商品和订单是贯穿整个业务流程的核心概念,但在业务流程的不同阶段涉及不同的参与方和关注点,对应到系统能力上的诉求也不尽相同。
以商品为例,其涉及到商品的创建、审核发布以及用户端的展示销售等环节。在商品创建阶段,商家关注录单效率和商品制作过程的管理;在审核阶段,运营关注审核需求和审核效率;而在展示销售阶段,用户关注商品信息、价格库存以及如何做出购买决策。订单的情形也类似,在购买、履约和售后各个阶段,关注点也有所不同。因此,我们对商品和订单的限界上下文进行了细分,以确保系统设计能够更精准地满足各阶段的业务需求。
限界上下文的识别过程虽然本质上仍然是对问题域拆分和求解的过程,但同时限界上下文也是应用的边界和技术的边界,所以我们也需要考虑一些质量需求和技术因素,不过需要注意的是我们仍然要遵循先业务后技术的原则,并且在考虑技术因素时,仍然要保证领域模型的完整性和一致性。
我们从质量属性、服务集成和功能复用三个方面对限界上下文做进一步的划分,以商品计算为例,商品计算量大、任务多、规则复杂,为了避免影响正常的商品展示和售卖,所以从展销上下文进行了拆解。此外,我们的商品和订单都涉及到要与很多第三方的系统进行对接,这里面将第三方服务的集成划分为单独的直连上下文,从而隔离三方系统差异对内部商品和订单相关系统带来的变化。在功能复用上,我们考虑对多个限界上下文都涉及的功能进行提炼,作为单独的一个上下文,比如商家权限上下文。
限界上下文封装了按照纵向切分的业务能力,那多个限界上下文如何协作来完成一个完整的业务场景呢,这就涉及到限界上下文的映射,按照通信集成模式和团队协作模式来划分,有多种映射关系,这里面我们用到最多的是通过防腐层、开放主机服务和发布语言三者联动来隔离上下游的变化、维护整个领域模型的稳定性。
领域建模
在领域建模阶段,我们整体上分为领域分析建模和领域设计建模。首先,主要是对用例以及用例规约和用户故事进行详细的分析,从中通过名词法和动词法寻找领域概念来构建我们的领域分析模型。在此基础上,我们基于DDD战术设计的元模型,识别出这些概念中的实体和值对象,并且根据业务规则的不变性设计聚合。
以订单为例,这里是我们简化之后的模型,包括订单、支付单、履约单、凭证以及退款单这样几个聚合,在存在状态变化时,聚合之间通过领域事件进行协作。
模型实现
在完成限界上下文的识别以及领域模型的设计之后,接下来进入到代码实现阶段,那我们如何将具体的业务流程或业务活动映射到我们的系统进行代码实现呢。这里我们首先是从业务视角对业务流程和业务活动进行分层结构化拆解,其实我们之前的用例分析和用例规约就是这个拆解过程,在拆解之后,我们按照一定的映射关系将其映射到用户接口、应用服务、领域服务、聚合和端口的实现上。
最后,我们按照限界上下文划分微服务,服务内部按照分层架构进行实现。整体上基于关注点分离和SOLID原则,分为接入层、应用层、领域层和基础设施层。最终需要维护领域层的稳定性,对上由接入层和应用层来隔离变化,对下由基础设施层通过依赖倒置的方式来隔离数据以及外部依赖的差异性和变化。
| 3.3 平台化阶段
随着业务的不断发展,出现了商场团购、内容商业化等更多的交易业务场景,在技术上可以通过平台化的思路将底层系统能力进行复用来提升各业务的支持效率。同时,DDD的战略模式也在重点关注组织上如何更好的管理大型业务系统,因此我们可以结合DDD来构建平台领域模型和业务扩展模型,从而更加高效地完成平台化改造。
我们主要以业务最为复杂的境外交易业务作为基础的主领域模型,并按照DDD领域建模过程对商场团购和商业化业务进行拆解得到的领域模型与主领域模型进行映射匹配,经过同类项识别、归并和重组,得到平台领域模型和各业务的扩展模型。在我们的实际落地过程中,为了实现在多业务之间进行最大化复用的目标,我们在平台领域模型的构建上做了进一步的提炼,将平台领域模型拆解为基础领域模型,以及预订业务模型、团购业务模型等按照业务形态划分的领域模型。
此外,为了提升业务BP和平台团队的协作效率,在平台领域模型和业务领域模型划分的基础上,我们采用了基于插件化的集成开发模式。通过扩展点的定义,由各业务线在各自的插件包里基于业务扩展模型进行业务定制化实现,再集成平台领域模型和业务扩展模型,最后实现完整的业务流程和业务场景。
4 总结和思考
DDD是一种开放的思想体系,其核心在于通过领域模型的建立来引导整个设计过程。
第一,本文认为战略设计的重要性可能要高于战术设计,因为它涵盖了对业务流程和核心概念的理解和组织。
第二,领域建模是一个动态的、迭代的过程,而非一成不变的瀑布式流程。这个过程类似于一个建模涡流,从战略设计到战术设计,不断迭代。在战术设计过程中,如果发现某些方面不合理,就需要对战略设计做出调整。同样,子域的划分和限界上下文的识别也是动态的,需要根据新的发现不断优化。
第三,DDD不强迫采用特定的架构模式,它关注的是业务与技术复杂性是否得到了有效分离。无论是整洁架构、六边形架构还是传统的DDD分层架构,只要能够实现这一目标,它们都是可行的选择,即便是采用MVC分层架构,只要能够分离业务和技术复杂性,也同样适用。
最后,我们来简要强调一下工程师的思维模型,这些在领域驱动设计(DDD)的实施过程中也至关重要。一方面,工程师需要培养用户思维、业务思维和产品思维,这有助于深入理解业务和问题域。基于这样的理解,工程师可以运用结构化思维来分解问题,并通过抽象思维来提炼模型。另一方面,结合分层、分治和工程思维,工程师可以有效地将设计转化为实际的代码实现。