领域驱动设计:

什么是领域驱动设计(DDD)

领域驱动设计是一种软件开发方法,它围绕业务概念构建领域模型,指导我们将复杂问题进行拆分,解决大型复杂系统在落地中遇到的问题,设计出能够准确表达业务意图的软件模型。

领域驱动设计解决的两个核心问题:

  1. 业务架构如何合理的划分。

  2. 系统架构与业务架构保持一致。 将业务架构与系统架构对应起来,在响应业务变化调整业务架构时,也随之变化系统架构,建立针对业务变化的高响应力的系统架构。

领域驱动设计同时提供了战略上的和战术上的建模工具来帮助我们设计高质量的软件模型。

什么是领域模型

领域模型是关于某个特定领域的软件模型,通常,领域模型通过对象模型来实现,这些对象同时包含了数据和行为,并且准确表达了业务含义。表达通用语言的软件模型就是领域模型。

领域建模是通过对业务和问题域进行分析,建立领域模型,维持业务和代码的逻辑一致性,向上通过限界上下文指导微服务边界设计,向下通过聚合指导实体对象设计。

领域模型与数据模型

数据模型关注的是数据存储,所有的业务都离不开数据,都离不开对数据的CRUD;领域模型关注的是领域知识,领域模型对应的是业务实体,它更加关注业务语义的表达,而不是数据的存储和数据之间的关系。

两个模型往往被混淆在一起,因为两者都强调实体和实体关系,正确的做法应该是把领域模型、数据模型区别开来,让他们各司其职,从而更合理的设计我们的系统。

为什么要用DDD

  1. 统一通用语言,提升日常产研工作沟通效率

  2. 科学合理的领域划分和边界界定,理清系统边界和职责,指导架构设计,降低问题复杂度

  3. 优雅的系统设计,系统能够快速响应业务变化,有利于系统架构的演进,提升系统扩展性和可维护性,设计出准确表达业务意图的软件模型

  4. 切实提升个人的架构设计能力,思考深度和广度有明显提升。

DDD与微服务

DDD是一种架构设计方法论,微服务是一种架构风格、是技术实现,两者从本质上都是从业务视角去分离应用系统复杂度的手段,将复杂问题进行分治。

DDD方法建立的领域模型,可以清晰地划分微服务的逻辑边界和物理边界,从而搭建一个高内聚低耦合的微服务体系,所以我们一般使用DDD来作为微服务设计的指导思想。

战略设计和战术设计:

战略设计

战略设计主要从业务视角出发,建立业务领域模型,划分领域边界,建立通用语言和限界上下文,限界上下文可以作为微服务设计的参考边界。

战术设计

战术设计则从技术视角出发,侧重于领域模型的技术实现,使开发人员能够按照领域专家的思维模型开发软件,减小业务和技术之间的鸿沟,完成软件开发和落地,包括:聚合根、实体、值对象、领域服务、应用服务和资源库等代码逻辑的设计和实现。战术设计是根据领域模型进行微服务设计的过程。

事件风暴:

事件风暴是一种灵活的研讨会格式,用于协作探索复杂的业务领域。事件风暴是DDD战略设计中经常使用的一种方法,它可以快速分析和分解复杂的业务领域,完成领域建模。

参与人员:组织者、领域专家、产品、架构师、技术、测试等参与开发这个项目的成员。

步骤:

  1. 寻找领域事件:领域事件一般用橘色的便利贴表示,书写领域事件的规则是使用被动语态,并按照时间顺序贴在白纸上。

  2. 寻找命令:命令代表系统中用户的意图、动作和决定,一般用蓝色的便利贴表示。命令也可由外部系统触发,外部系统通常用粉色的便利贴表示。

  3. 寻找角色:角色表示一类特定用户,一般用黄色便利贴表示。它们之间的关系是“角色”发送“命令”产生了“领域事件”。
  4. 寻找聚合:我们把跟一个概念相关的命令和事件集合到一起,并用黄色的较大的便利贴表示聚合。把跟这个领域模型相关的命令放到左边,事件放到右边。
  5. 划分子域和限界上下文:根据业务语义将一个或多个聚合划定到同一个限界上下文,进行领域和限界上下文的划分,明确领域边界、职责、关系,并在限界上下文内完成领域建模。

架构:

分层架构:

分层架构一般分为用户接口层、应用层、领域层和基础层四层,每个层都应该具有良好的内聚性,并且只依赖比自身更低的层。分层架构分两种,松散分层架构和严格分层架构,松散分层架构允许任意上层与任意下层发生耦合;严格分层架构则只能访问其下方的层,一般来说我们会使用严格分层架构。

  • 用户接口层:负责向用户显示信息和解释用户命令,这一层聚集了接口适配相关的功能。

  • 应用层:应用层指应用服务,用来协调多个领域服务和领域对象完成服务编排和组合,协作完成业务操作,不处理业务逻辑。应用层也用来调用其它微服务,完成微服务间的服务编排和组合。

  • 领域层:领域层实现领域模型的业务逻辑,该层包含聚合根、实体、值对象、领域服务等领域模型中的领域对象。

  • 基础层:基础层为其他层提供技术基础服务,比如数据库、缓存、消息等。基础层一般会贯穿所有其他层,通过依赖反转设计原则,基础层实现所有其他层中定义的接口。

相比于传统三层架构,DDD的分层架构将业务逻辑层拆分到了应用层和领域层,应用层快速响应前端的变化,领域层实现领域模型的能力。

六边形架构(端口与适配器):

六边形架构也称为端口与适配器架构,该架构的思想是将内部核心的领域逻辑与外界依赖进行隔离,对于每种外界类型,都有一个适配器与之相对应。不同客户可以通过各种方式与系统交互,只需要添加一个新的适配器将客户输入转化成能被系统API理解的参数就行,系统输出也都有对应的适配器负责完成相应的转化功能。六边形每条边代表不同类型的端口,要么处理输入,要么处理输出。

  • 端口一般指:http,消息机制
  • 适配器一般指:servlet,消息监听器

CQRS

CQRS旨在解决数据显示复杂性问题,将领域模型一分为二,命令模型和查询模型分开存储。

  • 如果一个方法修改了对象的状态,该方法便是一个命令。
  • 如果一个方法返回了数据,该方法便是一个查询。

核心概念介绍:

主要讲解DDD的核心知识概念,从战略设计和战术设计两方面进行介绍,具体包括:领域、子域、核心域、通用域、支撑域、通用语言、限界上下文、上下文映射、实体、值对象、聚合、领域服务、应用服务等概念。

战略设计

  • 领域
  • 通用语言
  • 限界上下文
  • 上下文映射图

领域、子域、核心域、通用域、支撑域

领域的核心思想就是将问题域逐级细分,来降低业务理解和系统实现的复杂度。通过领域细分,逐步缩小微服务需要解决的问题域,构建合适的领域模型,而领域模型映射成系统就是微服务了。

DDD中一个领域被分成若干子域,根据对业务支撑程度分为核心域、支撑域或通用域,通过领域划分,区分不同子域在公司内的不同功能属性和重要性,从而公司可对不同子域采取不同的资源投入和建设策略,其关注度也会不一样。

  • 核心域:整个业务系统的核心,业务成功的主要促成因素,主要关注的。
  • 支撑域:专注于业务的某个方面。
  • 通用域:被用于整个业务系统。

通用语言

通用语言是一种团队协作模式,用于捕捉特定业务领域中的概念和术语,一个特定领域的软件模型一般通过不同的名词、形容词和动词来表达。在事件风暴过程中,通过团队交流达成共识的,能够简单、清晰、准确描述业务涵义和规则的语言就是通用语言。通用语言反应了领域专家对于软件系统的思维模型,项目成员通过通用语言表达软件模型。通用语言是作用于某个限界上下文之内的。

通用语言的价值:解决交流障碍的问题,使领域专家和开发人员能够协同合作,从而确保业务需求的正确表达。领域模型把通用语言表达成软件模型。

限界上下文:

限界上下文是领域模型的显示边界。通用语言作用于某个限界上下文之内,限界上下文为通用语言提供了一个语义边界,来保持通用语言和领域概念的一一对应关系。通用语言和限界上下文构成了DDD的两大支柱,相辅相成。一般可以通过事件风暴的方式来划分限界上下文。

限界上下文的价值:让团队明确模型的职责边界是什么从而合理的进行微服务设计和拆分,一个限界上下文理论上就可以设计为一个微服务。然后使用上下文映射图在战略层面对限界上下文进行集成;在边界内部,团队成员使用战术建模工具实现领域模型。

上下文映射图

用于表达限界上下文之间的集成映射关系。

限界上下文之间的关系,不同团队之间的关系存在多种组织和协作模式,常见的比如:客户方供应方开发。

  1. 客户方(Downstream 下游)-供应方(Upstream 上游)开发:表示上下游团队的关系,上游团队可以独立于下游团队完成开发。下游依赖上游,上游影响下游。

集成使用的技术:

  1. 开放主机(OHS):定义一种协议让其他系统通过该协议访问你的服务。
  2. 发布语言(PL):两个限界上下文之间翻译模型需要的公用语言,一般指编码方式,比如xml,json等,一般与开放主机共同使用。
  3. 防腐层(ACL):该层作为上游系统的代理向系统提供服务。客户端领域服务访问上游开放主机服务,远程服务以发布语言形式返回,防腐层将上游返回的发布语言翻译成本地上下文的领域对象,客户端领域服务service作为防腐层接口,委派给Adapter,Adapter先访问远程开放主机,然后由Adapter通过Translator将发布语言翻译成本地领域对象。

战术设计

  • 实体

  • 值对象

  • 聚合

  • 领域服务

  • 领域事件

  • 模块

  • 工厂

  • 资源库

  • 应用服务

实体:

实体一般对应业务对象,它具有业务属性和业务行为,一个实体拥有一个固定的身份标识,可以对实体进行多次修改,由于身份标识不变,他们依然是同一个实体。
实体类通常采用充血模型,与这个实体相关的所有业务逻辑都在实体类的方法中实现,跨多个实体的领域逻辑则在领域服务中实现。

唯一标识和可变性是实体和值对象的区别。

值对象:

值对象用于度量和描述事物,一般用来对实体的状态和特征进行描述。一个值对象可以只处理单个属性也可以处理一组相关联的属性。

值对象没有分身标识;不可变,只能整体替换;值对象的行为方法属于无副作用函数,不会改变内部属性。

聚合:

聚合就是由业务和逻辑紧密关联的实体和值对象组合而成的,聚合对象树的根节点就是聚合根。

聚合的原则:

  1. 在一致性边界内建模不变条件。不变条件表示一个业务规则,该规则总是保持一致的;一致性指事务一致性,一个事务中只能修改一个聚合。
  2. 设计小聚合,可以提高伸缩性、性能、可用性。
  3. 通过唯一标识引用其他聚合。
  4. 聚合的边界外使用最终一致性。

聚合内的业务功能内聚,能独立完成特定业务逻辑。聚合可以作为最小基础单元,完成领域模型和微服务架构的设计和演进,一个聚合就可以作为一个微服务。

领域服务:

领域中的服务表示一个无状态的操作,用于实现特定于某个领域的任务。当操作不适合放到实体和值对象上时,或者当某些功能是无法映射到具体的对象中时,最好的方式就是使用领域服务了。领域服务可以用来组合多个实体或者值对象,实现复杂的业务逻辑。当多个领域服务的组合编排在多个应用服务中被复用,也可以将其抽取成新的领域服务。

领域事件:

表示领域专家关心的发生在领域中的一些事件,有很强业务逻辑的地方,一般就是业务关注的发生的事件,一个领域事件将导致进一步的业务操作。在建模时,应该根据通用语言来命名事件,如果事件由聚合上的命令操作产生,那么通常根据操作方法的名字命名领域事件,事件的名字表明了聚合上的命令方法执行成功后所发生的事情。

领域事件可以由本地限界上下文消费、也可以由外部的限界上下文消费。

模块:

模块表示了一个命名的容器(在Java中可以是一个包名),用于组织领域中内聚在一起的类,将类放在不同模块中的目的在于达到松耦合性,通常,对于一个或一组内聚的聚合来说,我们都应该相应的创建一个模块,模块名应该反映出他们在领域中的概念。

当通用语言中的术语非常模糊时,不清楚如何划分上下文边界,可以先将他们放在一起,使用模块对进行进行划分,而不是限界上下文。

工厂

工厂提供创建对象的接口,负责复杂对象和聚合的创建,该接口封装了创建过程中的复杂操作过程。

工厂可以是一个单独的工厂对象,只用于创建某种聚合的对象;工厂方法也可以放在聚合根上,表达通用语言;复杂情况下也可以由领域服务扮演工厂的角色。

资源库

资源库提供了对持久化机制的抽象,通常我们将聚合实例存放在资源库中,之后再通过该资源库获取实例,一般由应用层来访问资源库获取领域对象,持久化机制的事物管理一般也放在应用层中进行管理。

资源库一般使用仓储模式,仓储接口放在领域层中,仓储实现放在基础层,通过依赖倒置实现应用层对基础资源库的解耦。

应用服务

应用服务是领域模型的直接客户,负责协调用例任务,管理事务,并执行一些必要的安全授权。不同于领域服务,应用服务只是很薄的一层,用来协调多个领域服务和领域对象完成服务编排和组合,协作完成业务操作,不处理业务逻辑。应用层也用来调用其它微服务,完成微服务间的服务编排和组合。

思考

​ 目前各大互联网公司都在推行一些DDD的思想和实践。个人认为,在工作中,我们更多可以应用的是使用DDD的战略建模方法,用来指导我们进行合理的领域划分,清晰的划分出各个模块或团队的职责和边界,设计出良好的微服务架构;而战术建模方法,偏向于技术实现,要求从需求分析阶段,产品经理、项目经理、架构师、开发工程师和测试等成员就建立统一的模型语言进行沟通,要求他们都懂一些产品和建模相关知识,并要求团队成员都对DDD有较深入的研究,这对团队成员能力有比较高的要求,人员持续培养成本也比较高,不然即使初期进行了良好的战术建模,后续迭代过程中也很容易走偏,最后落得个不伦不类。就目前所从事过的公司来看,应该没有一家真正严格按照DDD进行代码实现的,大家日常更多的是使用数据模型设计的贫血模型,虽然从理论上来说贫血模型有各种缺点,但在实践上,这种设计方式通常更容易理解,研发效率更高。

​ 总体上来说领域驱动设计,我们更多的是要学习其中的优秀设计思想,在战略层面进行系统架构设计和实现时提供科学的指导思路,而不一定非要在代码实现层面也完全遵循战术建模方法。

参考

《实现领域驱动设计》

事件风暴