DDD之领域驱动设计知多少

|0x00 数据团队痛点

作为数据领域的开发,面对海量的数据、快速迭代的框架,我并不害怕,因为这是我的主攻方向。但我却有一种困惑,与工程团队是相似的,即面对复杂的业务场景,总有一种剪不断理还乱的心情。

大部分的需求,当我们吭哧吭哧搞完之后,往往会发现如下的问题:

  • 业务逻辑复杂,模块之间耦合度高,没有合适的建模方法;
  • 多团队协同,系统间的依赖关系复杂,没有统一建模语言;
  • 没有清晰的设计文档,业务与功能混杂设计,不能进行有效的聚合;
  • 设计上缺乏灵活性,当需求频繁变化时,难以快速应对。

为此,我们调研了很多的理论,来应对这些情况,例如敏捷开发、例如UML。这些理论通常有一些弊端,即理论都是用于指导技术团队的工作,非常依赖于业务团队给出一份明确的需求,但大多数情况下,业务团队是不清楚技术现状的,因此双方少不了沟通的过程。

受制于一些技术思维,不论是工程团队,还是数据团队,非常倾向于一开始就把数据的格式与规则定义好,并以此为依据讨论需求,但业务人员没有受过专门的训练,通常是不理解这些设计应该遵循什么原则,往往大家在一起争论半天,看似都懂了,其实什么结论都没有达成。

而如何统一技术人员与业务人员的沟通语言,是技术团队痛点的一大痛点,也是面对复杂业务时一种有效的“抓手”。

|0x01 从软件开发的历史说起

在讲DDD之前,先让我们看一下,软件开发历史上,看待现实世界的思维模式,是如何逐步演进的。

早期的软件开发,瀑布模型大行其道,那个时候,软件的设计与开发,是分成两个阶段进行的,设计阶段使用范式对领域进行抽象,给出相应的设计模式,随后的开发阶段则通过编程语言,去实现这些设计模式。像学软件开发的童鞋们,过去都会学习Java开发的二十三原则,都是在这种思路的引导下,发展起来的。在软件开发的时代里,设计才是整个项目生命周期的核心阶段,决定了软件开发的质量与结果,因此需要资深的“架构师”来统筹这个阶段的工作。

UML就诞生于这个时代,在标准的流程中,软件设计被明确的分为了面向对象分析(OOA)、面向对象设计(OOD)和面向对象编码(OOP)。但在实际的工作中,OOD往往被忽略,由OOA和OOP两个阶段分别承担一部分设计的任务,如OOA要负责针对领域进行概念的抽象,OOP用于补充软件开发的相关因素,当两个阶段出现了割裂之后,必然会出现一定的对立情况。

最终,软件开发过程,就出现了两种沟通语言,一种是设计文档和UML图,一种是软件实现的设计模型,这两种方式往往不能很好的结合起来,对于实际的开发人员而言,做设计文档和UML图有一些多余,它应该体现在数据库的设计之中。而这种为了代码的灵活性的方式,为了消除重复性建设的影响,创造了许多与领域抽象概念相差较大的接口,导致在日益复杂的软件开发中,花费了过多的成本在软件代码的维护上,阻碍了最终结果的交付效率。

为了解决这个问题,我们发起了敏捷软件的开发倡议,认为“软件是唯一真正的设计产物”,为此将团队定义为一个个的独立单元,在单元内部,产品、开发与测试能够随时随地的交流沟通,并为单元产品的交付一起负责,为此甚至可以放弃文档的撰写。

敏捷开发在互联网时代获得了巨大的成果,伴随着业务的高速野蛮增长推广到了各个公司中,成为了开发人员的不二圣经。但当互联网增长放缓,精细化开始推广,需要开始切入复杂领域知识的技术设计时,敏捷就变得饱受质疑了。

DDD(Domain-Driven Design 领域驱动设计)诞生于2004年,是由Eric Evans最先提出,目的是对软件所涉及到的领域进行建模,以应对系统规模过大时引起的软件复杂性的问题。近年来被大家开始重视,用于解决敏捷开发带来的各种弊端。

在DDD中,Eric Evans认为最重要的核心资产应该是领域模型,软件开发的参与者都应该围绕一致性的领域模型而工作,而在领域模型的内部组织上,拥抱敏捷软件开发方法。因此,DDD应该首先被认为是一种软件的开发方法,拥抱了敏捷的理念,追求统一的领域模型,彻底的实现模型设计与代码实现相统一。

|0x02 DDD如何解决这些痛点

DDD提供的是一种架构设计的方向,强调技术与业务的融合性,开发人员要与领域专家相互协作,从业务的顶层概念上来规划整体设计。

例如我们在做后端开发时,基于的是MVC思路,DAO是数据库的抽象,而几乎所有的业务逻辑,都在Service中实现,因此我们讨论需求的时候,想的就是如何设计数据库,而不是业务概念,这往往会导致业务与技术对同一件事情的理解产生GAP。

DDD的价值,就体现在此,它主要面向四个痛点:

  • 统一语言:让团队中的各个成员,能够对同一件事情,产生相同的认知;
  • 知识沉淀:通过领域模型的方式,清晰、准确的沉淀业务知识;
  • 边界清晰:通过领域来划分边界,界定哪些需求在什么领域实现是最合理的;
  • 关注点分离:领域模型与数据模型能够分离,业务复杂性与技术复杂性分离,保持结构上的理解清晰。

通用语言在解决业务与技术的协作问题上,扮演了非常重要的角色,但通用语言不是天生的,而是对领域知识进行提炼的产出物,获取它的方式便是有效的需求分析过程,也就是理解领域知识的过程。

因此,DDD的很多概念,是抽象的产物,单纯的学习概念本身没有意义,只有结合了实际的业务场景做分析,才能对它有比较好的理解。例如近年来刮起了两阵狂风,一阵是微服务,一阵是中台架构,听起来非常的接地气,但实际上手的时候,微服务如何划分边界、拆解的有多细?中台应该如何重构领域,如何定义通用的公共能力?这都是一个又一个细节但却棘手的点。

拍板一个方案很简单,但如果它出现了问题,后续的开发是要付出很大的纠错成本的。

|0xFF DDD核心概念有哪些

这一部分相对枯燥,先只讲两个:领域和界限上下文。

DDD之领域驱动设计知多少_数据

【领域】 

领域是一个非常主观的概念,是对具有某个边界的领域的一个统称,反映了领域内用户业务需求的本质。如果用通俗的话讲,领域就是一个组织,要做事情的综合,是一个范围上的概念,而且是面向业务的,并非面向技术。

例如我要做一个电商平台,里面涉及到了一些旅游产品的售卖,这是两个不同业务领域的事情,因为电商专家能看得懂电商,而旅游专家能看得懂旅游产品,如果我为了图方便,把两个领域放在一起设计,那么彼此的专家都看不懂这个模型在做什么。尽管不论是从性能优化还是开发便捷性上看,可能是很合适的,但这个依然属于很失败的设计。

领域根据拆分方式的不同,又可以分为子领域,子领域有三种:核心子领域、通用子领域、支撑子领域。

核心子领域如何理解?例如在电商系统里,订单、支付、商品,这些都是核心域,是业务最核心的部分。

通用子领域如何理解?我们不能只考虑业务的东西,还需要分析一些其他的数据,比如打点日志、客服系统,这些子领域贯穿着我们整个领域系统,被其他领域共用。

支撑子领域如何理解?因为我们要为用户提供一些增值服务,比如消息通知、论坛等,这些都不是我们的核心子域,没有这些功能,我们依然可以很好的运行业务,但是这些确是支撑着我们核心子域的相关功能。

【界限上下文】

界限上下文(Bounded Context)定义了每个领域模型及子模型的应用范围,用于确保沟通语言的一致性。例如,当我们谈到“苹果”,有人可能想到平时吃的水果,有人也可能想到苹果手机,那么苹果应该在水果生鲜域,手机应该在消费电子域,在这种上下文环境里做理解,才不会产生歧义。

在电商系统中,销售子域是核心域,商品子域和物流子域为支撑子域。在这三个子域中,都要和商品打交道。如果把商品抽象为Product对象的话,按我们一般的常规思路(抛开子域的划分)来说,不管是商品销售还是发货,我们都可以共用同一个Product对象。

但在DDD中,在商品子域和销售子域中,可以共享这个Product对象,但在物流子域,就有点大材小用。为什么呢?因为毕竟物流子域关注的是商品的发货处理和物流跟踪。针对发货流程而言,我只关心商品的数量、大小、重量等规格,而不必了解商品的价格等其他信息。所以说物流子域应该关注的是货物的发货处理而不是商品。

那为什么我们之前的开发思路会共用同一个Product对象呢?

答案很简单,没有进行领域的划分。把整个项目一概而论,统一建模导致的结果。

在DDD的思想下,当划分子域之后,每个子域都对应有各自的上下文。在销售子域和商品子域所在的上下文语境中,商品就是商品,无二义性。在物流子域的上下文语境中,我们也可以说商品的发货处理,但这时的商品就特指货物了。确定了真实面目之后,我想我们也会不由自主的抽象一个新的Cargo对象来处理物流相关的业务。这也是DDD带来的好处,让我们更清晰的建模。

未完待续……

推荐阅读:

作为业务数据开发,我们为什么会焦虑?

数据模型如何论好坏

浅谈数据可视化

数仓治理一场仗

浅谈数据一致性