当前位置: 首页 > 科技 > 人工智能 > 你了解过领域驱动设计吗?如何运用领域驱动设计来进行业

你了解过领域驱动设计吗?如何运用领域驱动设计来进行业

天乐
2020-11-08 08:18:00 第一视角

领域驱动设计与业务建模

好的软件,来自于好的软件设计。软件设计是一门艺术,就像绘画、写作等其他艺术形式一样,它不能通过定理和公式以一种精确科学的方式被教授和学习。虽然通过软件创建的过程,可以发现和获取到有用的规律和技巧,但是也许永远无法提供一个准确的方法,以满足从现实世界映射到代码模型的需要。如今,完成软件设计的方法多种多样,其中领域驱动设计(Domain DrivenDesign,DDD)正是通过对业务领域建模,完成业务知识与代码的映射,从而降低软件开发的复杂性。在大型软件中,DDD可以有效降低构建软件的复杂性。

本节将介绍 DDD的基本概念,以及运用DDD来进行业务建模。

什么是通用语言

开发人员满脑子都是类、方法、算法、模式,总是想将实际生活中的概念和程序组件做对应。他们希望看到要建立哪些对象类,要如何对对象类之间的关系进行建模。他们会按照继承、多态、面向对象的编程等方式去思考及交流,这对开发人员来说太正常不过了。但是领域专家通常对这一无所知。他们对软件类库、框架、持久化,甚至数据库没有什么概念。他们只了解特有的专业业务技能。如果领域专家和技术人员之间进行讨论,领域专家使用自己的行话,技术团队成员在设计中也用自己的语言讨论领域,那么两者将永远也无法达成共识。

在设计过程中,开发人员倾向于使用自己熟悉的“方言”,但是没有一种方言能成为通用的语言,因为它们都不能满足所有人员的沟通需要。在讨论模型和定义模型时,领域专家和开发人员确实需要讲同一种语言。

领域驱动设计的一个核心原则是使用一种基于模型的语言。因为模型是软件满足领域的共同点,它很适合作为这种通用语言的构造基础,这种语言称为“通用语言(Ubiquitous Language)”。通用语言连接起设计中的所有的部分,是建立设计团队良好工作的前提。

但这种语言的形成可不是一蹴而就的,它需要领域专家和开发人员坐在一起,不断讨论业务模块,从而慢慢演变成大家都可以理解的通用语言。通用语言的表达方式可以多种多样,并无固定格式,可以是图、UML、文档或代码。

总之,开发人员应该要理解通用语言的重要性,要建立起模型和语言之间的密切关联。也必须要认识到,对语言的变更会造成对模型的变更,而模型的变更也意味着软件也要跟着变更。

领域驱动设计的核心概念

下面介绍关于领域驱动设计中的一些核心概念。

1.模型驱动设计(Model Driven Design )

通用语言应该在建模过程中需要进行广泛的尝试,从而推动软件专家和领域专家之间的沟通,以及发现要在模型中使用的主要的领域概念。建模过程的主要目的是创建一个优良的模型,而后将模型实现成代码。这是软件开发过程中同等重要的两个阶段。

但从模型到代码这个过程的转换并不简单,一个看上去正确的模型并不代表模型能被直接转换成代码。分析模型是业务领域分析的结果,其产生的模型并不会考虑软件需要如何实现。这样的一个模型可用来理解领域,因为它建立了特定级别的知识,而且模型看上去会很正确。问题是分析的时候不能预见模型中存在的某些缺陷及领域中所有的复杂关系。分析人员可能深人到了模型中某些组件的细节,但却未深入到其他部分。非常重要的细节直到设计和实现过程才可能被发现。主要的模型虽然能够如实反映领域知识,却可能会导致对象持久化的一系列问题,或者导致不可接受的性能行为。而此时,开发人员会被迫做出自己的决定,做出设计变更以解决实际问题,而这个问题在模型建立时是没有考虑到的。主要的结果是,他们建立了一个偏离模型的设计,让模型和实现二者越来越不相关。所以选择一个能够被轻易和准确转换成代码的模型变得很重要。那么应该如何动手处理从模型到代码的转换呢?

所以,最好的方案是,模型在构建时就考虑到软件的设计,而开发人员要参与到整个建模的过程中来。这样,就能够选择一个能恰当在软件中表现的模型,设计过程会很顺畅并且始终是忠于模型的。代码和其下的模型紧密关联会让代码更有意义。

任何技术人员想对模型做出贡献,必须花费一些时间来接触代码,无论他在项目中担负的是什么样的角色。任何一个负责修改代码的人都必须学会用代码表现模型。每个开发人员都必须参与到一定级别的领域讨论中,并和领域专家进行沟通。那些按不同方式贡献的人必须自觉地与接触代码的人使用通用语言,动态交换模型思想。因为对代码的一个变更就可能成为对模型的变更。

2.分层架构(Layered Architecture )

将应用划分成分离的层并建立层间的交换规则很重要。如果代码没有被清晰隔离到某层中,它会马上变得混乱,变得非常难以管理和变更。在某处对代码的一个简单修改会对其他地方的代码造成不可估量的结果。领域层应该关注核心的领域问题,它不应该涉及基础设施类的活动。用户界面既不跟业务逻辑紧密捆绑,也不包含通常属于基础设施层的任务。在很多情况下应用层是必要的,它会成为业务逻辑之上的管理者,用来监督和协调应用的整个活动。

一个典型的DDD分层架构如图6-10所示。

其中:

UI层:负责界面展示或用户接口;

应用层:负责业务流程;

领域层:负责领域逻辑;

基础设施层:负责提供基础设施支持。

越往上层,变动越频繁;越往下层,变动就会越少,越稳定。

3.实体(Entity )

实体是指带有标识符的对象,它的标识符在历经软件的各种状态后仍能保持一致。

如果有一个存放了天气信息(如温度)的类,很容易产生同一个类的不同实例,这两个实例都包含了同样的值,这两个对象是完全相等的,可以用其中一个与另一个交换,但它们拥有不同的引用,而且不是实体。

开发人员可能会创建一个Person类,这个类会带有一系列的属性,如名字、出生日期、出生地等。这些属性中有哪个可以作为Person的标识符吗?名字不可以作为标识符,因为可能有很多人拥有同一个名字。如果只考虑两个人的名字,就不能使用同一个名字来区分他们两个,也不能使用出生日期作为标识符,因为会有很多人在同一天出生。同样也不能用出生地作为标识符。一个对象必须与其他的对象区分开来,即使是它们拥有相同的属性。错误的标识符可能会导致数据混乱。

因此,在软件中实现实体意味着创建标识符。对一个Person类而言,其标识符可能是属性的组合:名字、出生日期、出生地、父母名字、当前地址等。在中国,身份证号码也会用来创建标识符。

通常标识符或者是对象的一个属性(或属性的组合),一个专门为保存和表现标识符而创建的属性,抑或是一种行为。

有很多不同的方式来为每一个对象创建一个唯一的标识符:可能由一个模型来自动产生ID,在软件中内部使用,不会让它对用户可见;它可能是数据库表的一个主键,会被保证在数据库中是唯一的。只要对象从数据库中被检索,它的ID就会被检索出来并在内存中被重建;ID也可能由用户创建,如身份证号码,每个人都会拥有一个唯一的字符串ID,这个字符串在中国范围内是通用的。

另一种解决方案是使用对象的属性来创建标识符,当这个属性不足以代表标识符时,另一个属性就会被加人以帮助确定每一个对象。

实体是领域模型中非常重要的对象,并且它们应该在建模过程开始时就被考虑。

4.值对象(Value Object)

实体是可以被跟踪的,但跟踪和创建标识符需要一定的成本。开发人员不但需要保证每一个实体都有唯一标识,而且跟踪标识也并非易事。需要花费很多精力来决定由什么来构成一个标识符,因为一个错误的决定可能会让对象拥有相同的标识,而这显然不是人们所期望的。将所有的对象视为实体也会带来隐含的性能问题,因为需要对每个对象产生一个实例。

有时,人们对某个对象是什么不感兴趣,只关心它拥有的属性。用来描述领域的特殊方面,且没有标识符的一个对象,称为值对象。

区分实体对象和值对象非常必要。没有标识符,值对象就可以被轻易地创建或丢弃。在没有其他对象引用时,垃圾回收会处理这个对象。这极大地简化了设计,同时对于性能也是非常大的提升。

值对象由一个构造器创建,并且在它们的生命周期内永远不会被修改。当希望一个对象拥有不同的值时,就会简单地去创建另一个对象。这会对设计产生重要的结果。如果值对象保持不变,并且不具有标识符,那么它就可以被共享了。

所以,如果值对象是可共享的,那么它们应该是不可变的。

5.服务( Service )

当开发人员分析领域并试图定义构成模型的主要对象时,就会发现有些方面的领域是很难映射成对象的。

对象通常要考虑的是拥有属性,对象会管理它的内部状态并暴露行为。在开发通用语言时,领域中的主要概念被引入到语言中,语言中的名词很容易被映射成对象。语言中对应那些名词的动词变成那些对象的行为。但是有些领域中的动作,它们是一些动词,看上去却不属于任何对象。它们代表了领域中的一个重要的行为,所以不能忽略它们或简单地把它们合并到某个实体或值对象中去。

给一个对象增加这样的行为会破坏这个对象,让它看上去拥有了本不该属于它的功能。但是,要使用一种面向对象语言,就必须用到一个对象才行。它不能只拥有一个单独的功能,而不附属于任何对象。通常这种行为类的功能会跨越若干个对象,或许是不同的类。例如,为了从一个账户向另一个账户转钱,这个功能应该放到转出的账户还是在接收的账户中?感觉放在这两个账户中的哪一个也不对。当这样的行为从领域中被识别出来时,最佳实践是将它声明成一个服务。这样的对象不再拥有内置的状态了,它的作用是为了简化所提供的领域功能。

一个服务应该不是对通常属于领域对象操作的替代。开发人员不应该为每一个需要的操作建立一个服务。但是当一个操作凸显为一个领域中的重要概念时,就需要为它建立一个服务了。以下是服务的三个特征。

服务执行的操作涉及一个领域概念,这个领域概念通常不属于一个实体或值对象。

被执行的操作涉及领域中的其他对象。

操作是无状态的。

当领域中的一个重要的过程或变化不属于一个实体或值对象的自然职责时,向模型中增加一个操作,作为一个单独的接口将其声明为一个服务。根据领域模型的语言定义一个接口,确保操作的名称是通用语言的一部分。最后,应该让服务变得无状态。

6.模块(Module )

对一个大型的复杂项目而言,模型会趋向于越来越大。当模型最终大到作为整体也很难讨论时,理解不同部件之间的关系和交互将变得很困难。基于此原因,非常有必要将模型以模块方式进行组织。模块被用来作为组织相关概念和任务,以便降低软件复杂性的一种非常简单有效的方法。

使用模块另一方面也可以提高代码质量。好的软件代码应该具有高内聚性和低耦合度。虽然内聚开始于类和方法级别,但它其实也可以应用于模块级别。强烈推荐将高关联度的类分组到一个模块,以提供尽可能大的内聚。

有多重内聚的方式。最常用到的是通信性内聚(Communicational Cohesion)和功能性内聚(Functional Cohesion)。在模块中的部件操作相同的数据时,可以得到通信性内聚。把它们分到一组很有意义,因为它们之间存在很强的关联性。在模块中的部件协同工作以完成定义好的任务时,可以得到功能性内聚。功能性内聚一般被认为是最佳的内聚类型。

给定的模块名称会成为通用语言的组成部分。模块和它们的名称应该能够反映对领域的深层理解。

7.聚合(Aggregate )

聚合是一种用来定义对象所有权和边界的领域模式。

聚合是针对数据变化可以考虑成一个单元的一组关联的对象。聚合使用边界将内部和外部的对象划分开来。每个聚合都有一个根。这个根是一个实体,并且它是外部可以访问的唯一的对象。根对象可以持有对任意聚合对象的引用,其他的对象可以互相持有彼此的引用,但一个外部对象只能持有对根对象的引用。如果边界内还有其他的实体,那些实体的标识符是本地化的,只在聚合内有意义。

将实体和值对象聚集在聚合之中,并且定义各个聚合之间的边界。为每个聚合选择一个实体作为根,并且通过根来控制所有对边界内的对象的访问。允许外部对象仅持有对根的引用。对内部成员的临时引用可以被传递出来,但是仅能用于单个操作之中。因为由根对象来进行访问控制,将无法盲目地对内部对象进行变更。这种安排使强化聚合内对象的不变量变得可行,并且对聚合而言,它在任何状态变更中都是作为一个整体。

8.资源库(Repository )

在模型驱动设计中,对象从被创建开始,直到被删除或被归档结束,是有一个生命周期的。一个构造函数或工厂可应用于处理对象的创建。创建对象的整体作用是为了使用它们。在一个面向对象的语言中,必须保持对一个对象的引用以便能够使用它。为了获得这样的引用,客户程序必须创建一个对象,或者通过导航已有的关联关系从另一个对象中获得它。例如,为了从一个聚合中获得一个值对象,客户程序需要向聚合的根发送请求。问题是现在客户程序必须先拥有一个对根的引用。

对大型的应用而言,这会变成一个问题,因为必须保证客户始终对需要的对象保持一个引用,或者是对关注的对象保持引用。在设计中使用这样的规则,将强制要求对象持有一系列它们可能其实并不需要保持的一系列的引用。这增加了对象间的耦合性,创建了一系列本不需要的关联。

客户程序需要有一个获取已存在领域对象引用的实际方式。如果基础设施让这变得简单,客户程序的开发人员可能会增加更多的可导航的关联,从而进一步使模型混乱。从另一方面讲,他们可能会使用查询从数据库中获取所需的数据,或者拿到几个特定的对象,而不是通过聚合的根来递归。

领域逻辑分散到查询和客户代码中,实体和值对象变得更像是数据容器。应用到众多数据库访问的基础设施的技术复杂性会迅速蔓延在客户代码中,开发人员不再关注领域层,所做的工作与模型也没有任何关系了。最终的结果是放弃了对领域的关注,在设计上做了妥协。

使用资源库的目的是封装所有获取对象引用所需的逻辑。领域对象无须处理基础设施,便可以得到领域中对其他对象所需的引用。这种从资源库中获取引用的方式,可以让模型重获它应有的清晰和焦点。

资源库会保存对某些对象的引用。当一个对象被创建出来时,它可以被保存到资源库中,然后在以后使用时就可以从资源库中检索到。如果客户程序从资源库中请求一个对象,而资源库中并没有该对象时,就会从存储介质中获取它。换种说法是,资源库作为一个全局的可访问对象的存储点而存在。

不同类型的对象可以使用不同的存储位置。最终结果是,领域模型同需要保存的对象及它们的引用之间实现了解耦。领域模型可以访问潜在的任何持久化基础设施。

虽然看上去资源库的实现可能会非常类似于基础设施,但资源库的接口是纯粹的领域模型。

利用DDD 来进行微服务的业务建模

1.利用限界上下文来拆分微服务

DDD对微服务来说,一个重要的指导就是服务拆分。在DDD中,限界上下文(Bounded Con-text)主要用于确定业务流程的边界,同样也适用于微服务之间边界的划定。在一个好的限界上下文中,每一个微服务都应该只表示一个领域概念,无歧义且唯一。一个限界上下文并不一定包含在一个子域中,一个子域也可以包含多个上下文。对于一个领域中的限界上下文不是孤立存在的,而是通过多个限界上下文的协作完成业务的。

在设计API的时候,要抛弃以往以CURD操作为中心的设计,而应使用DDD策略,设计更加符合业务需求的接口。这样,这些操作就会具有良好的定义。不管对于服务提供方还是客户端来说,这样的体验都更好。服务提供方不再需要根据更新字段来推测业务操作的意图,业务操作清晰明了,这样的代码更简单,也更容易维护。而对于客户端来说,它们能执行或不能执行哪些操作也是一目了然的。如果API需要具有良好的文档化,那么可以结合使用Swagger工具,就可以很清楚地了解到API都具有哪些约束。

图6-11展示了对于一个天气预报系统而言,所需要划分的限界上下文。

整个系统可以分为天气数据采集限界上下文、天气数据API限界上下文、城市数据API限界上下文、天气预报限界上下文。其中限界上下文又可以划分为不同的组件,其中:

天气数据采集限界上下文包含数据采集组件、数据存储组件。数据采集组件是通用的用于采集天气数据的组件。数据存储组件是用于存储天气数据的组件;

天气数据API限界上下文包含了天气数据查询组件。天气数据查询组件提供了天气数据查询的接口;

城市数据API限界上下文包含了城市数据查询组件。城市数据查询组件提供了城市数据查询的接口;

天气预报限界上下文包含了数据展示组件。数据展示组件用于将数据模型展示为用户能够理解的UI界面。

.使用领域事件进行服务间解耦

领域事件(Domain Events )是DDD中的一个概念,用于捕获建模领域中所发生过的事情。那么,什么是领域事件?

例如,在用户注册过程中,有这么一个业务要求,即“当用户注册成功之后,发送一封确认邮件给客户”。那么,此时的“用户注册成功”便是一个领域事件。领域事件对业务的价值在于,有助于形成完整的业务闭环,即一个领域事件将导致进一步的业务操作。正如例子中的“用户注册成功”事件,会触发一个发送确认邮件给客户的操作。

在微服务架构里面,“用户注册”和“发送邮件”可能是分布于不同的微服务中,通过事件,将两个服务的业务给串联起来了。

简而言之,通过引入领域事件,我们的软件带来如下好处。

帮助用户深入理解领域模型:因为只有理解了领域模型,才能更好地设计领域事件。

解耦微服务:这也是最终的目的。事件就是为了更好地处理服务间的依赖。

领域事件的实现,往往依赖于消息中间件系统。在本文的最后也介绍了一种“分布式消息总线”的方式,来实现服务间的事件处理。

本篇文章给大家讲解的内容是领域驱动设计与业务建模

下篇文章给大家讲解天气预报系统的微服务架构设计与实现;

觉得文章不错的朋友可以转发此文关注小编;

感谢大家的支持!!

承接上文

提示:支持键盘“← →”键翻页
为你推荐
加载更多
意见反馈
返回顶部