DDD系列-领域驱动设计思想概览
blue    2022-11-16 00:27:33    116    0    0
weibo-007   blue

领域驱动设计

什么是领域驱动设计

 《架构整洁之道》里面有一句话定义业务逻辑

严格来讲,业务逻辑就是程序中那些真正用于挣钱/省钱的业务逻辑与过程。更严格得讲,不管这些业务逻辑是计算机上实现的还是人工实现的,他们在省钱/赚钱上的作用是一致的

软件开发通常被应用到真实世界中已经存在的自动化流程。从一开始,我们就必须明白软件脱胎于领域,并且跟领域密切相关。在我们开发软件的时候,当然也可以直接坐下来敲代码,但仅限于在开发价值不大的软件时。

为了创建一个复杂的软件,你必须知道这个软件究竟是什么,在你充分了解金融业务是什么之前,你是做不出一个好的银行业软件系统的,你必须理解银行业的领域

谁了解业务

没有丰富的领域知识能做出复杂的银行业业务软件吗,没门!那谁了解银行业业务呢?软件架构师吗?不,他只是用银行来存储自己的金钱,软件开发人员?别为难他了。只有银行业务人员才是这个领域的专家,他们知道所有的细节,所有的困难,以及所有的规章。这些就是我们永远的起始点:领域

模型

模型是软件设计中最基础的部分,我们对领域的所有思考过程被汇总到这个模型中,我们需要就这个模型跟领域专家就进行交流,跟资深的设计人员进行交流,跟开发人员进行交流,模型是软件的根本。要表现这种模型,常见的方式是将模型图形化:图,用例等。我们需要用模型交流

软件设计方式

软件设计有不同的方法,其中之一是瀑布设计法。这种方法包含了一些阶段,业务专家提出一堆需求同业务分析人员进行交流,分析人员基于那些需求来创建模型并将结果传递给开发人员,开发人员根据他们收到的内容开始编码。瀑布设计法中知识只有单一的流向。这种传统方法虽然应用很多年,但是还是有一定的缺陷和局限。主要是业务专家得不到分析人员的反馈信息,分析人员也得不到开发人员的反馈信息。

另一个方法是敏捷方法学,这些方法不同于瀑布方法的一堆动作,产生背景是预先很难确定所有的需求,特别是需求经常变化的情况。敏捷开发试图解决另一个问题被称为"分析瘫痪",团队成员会因为害怕做出任何设计决定而无济于事。敏捷开发者反对预先设计,相反,他们应用大量的灵活设计。敏捷开发也有自己的不足,

敏捷开发缺乏真实可见的设计原则,由开发人员执行持续重构会导致代码难以理解或者难以变更。瀑布流会过度工程,过度工程可能会带来另一种担心:害怕做出深度,彻底的设计。

领域驱动设计

本书介绍领域驱动设计原则,这些原则会增进对领域内复杂问题进行建模和实现的开发过程能力。我们来用一个飞机飞行控制系统,看看领域知识是如何被构建的。在一个给定的时刻,空中会有成千上万的飞机,很重的的一点是保证它们不会发生碰撞。

这里我们从复杂的空中交通系统的一个子集-飞机监控系统来阐述这个问题。这个监控系统会跟踪制定区域内任意航班,判断班机是否遵循来预定的航线,以及它们是否有可能发生碰撞。

按照我们前面的描述,最了解飞机监控系统的领域内专家就是空中交通控制人员,他们是这个领域的专家。所以我们第一跟他们进行讨论,这时,你会听到很多飞机起飞,着陆,飞机升空和碰撞危险等知识。这时候可能我们找到了关键词"飞行器","起始机场","目的机场'。

但是飞机从一个地方起飞到另一个地方,中间发生了什么?这个时候控制人员会说给每架飞机指定一个飞行计划。当听到飞行计划的时,你可能想到了一个有趣的词-路线。所以,不同于将飞行器把起始机场和目的机场联系起来,看起来更应该用路线将起始机场联系起来。

跟控制人员再交流,你会发现路线由小的区间段组成,这些区间段按照一定的次序组织起来就会构成从起始机场到目的机场的一条曲线。所以,路线其实是一序列连续的方位点,从这个角度看,起始机场和目的机场只是那些方位中的两个点。注意,这种抽象会给后来带来很大的方便

看见没有,实际上你和领域专家在交谈,你们在交换知识,他们从空中交通领域挖掘基础概念,这些概念可能没有经过组织,但他们是理解领域的基础。你需要尽可能得从专家处学习领域知识,并且开始构建领域模型。这是设计中很重要的一点。注意,这可能会发生相当长时间的碰撞,但是作为软件方面的专家和领域专家,他们会一起创建领域的模型,这个模型会体现两个专业领域的交汇。这看上去是很耗费时间的过程,并且确实如此,但是他们也应该这样做,因为软件的最终目的是解决真实领域中的业务问题

通用语言

通过前面一个章节的了解我们得知油软件专家和领域专家通力合作开发出一个领域的模型是绝对需要的。但是两种完全不同的角色交流起来会有些困难,开发人员满脑子都是类,方法,算法。但是领域专家对此一无所知,他们只了解他们专有的技能。

为了克服这种交流困难,在建立模型时,我们必须通过沟通来交换对模型和模型中设计元素的想法,应该如何连接他们。当团队成员不能通过公共语言来讨论问题的时候,项目会面临严重的问题,比如在项目的交流的过程中,需要做翻译才能让其他人理解这些概念。比如开发人员可能努力使用外行人的语言解释设计模式,但是领域专家可能会创造出一种新的行话以努力表达他们的想法,在这种痛苦的交流中,往往就产生歧义。

领域驱动设计一个核心的原则是使用一种基于模型的语言。并且需要确保团队使用的语言在所有的交流形式中看上去都是一致的,因为这个原因,这种语言也被称为"通用语言",那么,我们应该如何去构建一种语言呢。可用通过下面的对话,我们了解一下,注意飘红的词语

开发人员:我们想监控空中交通,应该从哪儿做起

专家:让我们从最基础的开始吧,所有的交通由飞机组成,每架飞机从一个出发点起飞,并在一个目的地着陆

开发人员:很容易嘛,在飞行时,飞机会按照驾驶员的意愿任意空中路线吗,只要他们能到达终点

专家:哦不,驾驶员会收到一条他们应该遵循的飞行路线,并且他们必须尽可能跟这条飞行路线吻合

开发人员:我会把这条路线考虑成空中的3D路线,如果我们使用笛卡尔坐标系,那么飞机就被简化成3D的点

专家:我可不这么认为,我们不会这样看待飞行路线的,飞行路线实际上是飞机预期的空中路线在地面上的映射,飞行路线会穿过一系列地面上的点,而这些点我们可以用经纬度来表示

开发人员:哦,那我们可以称每一个这样的点为方位,因为他是地球表面上一个固定的点,我们将使用一系列2D的点来描述线路。另外,出发点目的地都属于方位,我们不在将他们考虑成不同的概念。飞机必须遵循飞行计划,那他是否可以按照自己的意愿选择飞行高度。

专家:不,飞机在一个特定的时刻海拔高度也会在飞行计划

开发人员:飞行计划?那是什么意思

专家:在离开机场之前,驾驶员会接到一个详细的飞行计划,包括所有关于这次飞行的信息,飞行路线,巡航高度,巡航速度和飞机的类型甚至机组成员信息。

开发人员:看起来飞行计划相当重要,我们可得把它加到模型中

开发人员:好多了,当我看到这幅图的时候,我能一眼就是看出,我们对飞机不感兴趣,我们对飞行感兴趣。

领域驱动设计

问题 

常常出现这样的情况,软件分析人员和业务领域专家在一起工作了若干个月,一起发现了领域的基础元素,强调了元素之间的关系,创建了一个正确的模型。然后这个模型被传递给了软件开发人员,开发人员看模型时可能会发现模型中的有些概念或者关系无法使用代码来正确的表达。所以他们使用模型作为灵感的来源,但是创建了自己的设计。久而久之,随着开发的进行,原始模型和最终实现的差距会越来越大

那如何才能优雅的完成从模型到代码的转换?

方案一

一个推荐的技术设计是创建分析模型,它被认为是与代码设计相互分离的,通常是由不同的人完成的。分析模型是业务领域分析的结果,此模型不需要考虑软件如何实现。

这样的模型只保证分析层面是正确的,软件实现不是这个阶段考虑的

这样设计出来的模型到达开发人员那里后,由他们来做设计的工作,因为这个模型中不包含设计原则,它可能无法很好的为目标服务。因此开发人员不得不修改他,或者创建分离的设计。在模型和代码之间也不再存在映射关系。最终的结果是:分析模型在编码开始后就被抛弃了

这种方法中存在的一个主要问题分析无法预见模型中存在的某些缺陷以及领域中所有的复杂性,非常重要的细节直到设计和实现过程才被发现,因此开发人员会被迫做出他们自己的决定,并会做出设计变更以解决一个实际问题。

虽然模型可以被表达成图形或者文字形式,但是开发人员常常难以掌握模型的完整含义,或者某些对象关系以及他们完整行为。所以开发人员只能做出自己的理解,并且基于这种理解做出自己的改动。

方案二

一种更好得方法是将领域建模和设计紧密关联起来。模型在构建时就考虑到软件实现和设计,开发人员应该被加入到建模的过程中来。主要的想法是选择一个能够在软件实现中恰当的表达的模型,这样设计过程会很顺畅并且基于模型将代码紧密关联,将会使代码更有意义,并且与模型保持相关。

有了开发人员的参与,就会获得反馈,它能够确保模型能够在软件中得到实现。如果某处有错误,会在早期就被标识出来,问题也能够很容易得到纠正。

写代码的人应该很好的了解模型,应该感觉到自己有责任,保持它得完整性。他们应该意识到对代码的一个变更其实就隐含着对模型的变更,否则,如果哪里的代码不能表达最初模型的话,他们将会对代码做出重构。

任何技术人员对模型做出贡献,必须花费一些时间来接触代码,无论他在项目中担任是什么样的主要角色。每位开发人员必须参与到一定级别的领域讨论中并且和领域专家保持联络

分层架构

在一个面向对象的程序中,用户界面,数据库,以及其他支持性代码经常被写到业务对象中。但是,当领域相关的代码被混入到其他层时,要阅读和思考这些代码也变得极其困难。表面上看上去是对UI的改动,其实变成了业务逻辑的修改。同时,对业务规则的变更,可能要谨慎跟踪用户界面代码,数据库代码。

因此,将一个复杂的程序划分多个层,每一层开发一个内聚的设计,让每层仅依赖它底下的那些层。领域驱动设计的一个通用架构有4个概念层

用户界面/展现层 负责向用户展现信息,解释用户命令
应用层很薄的一层,用来协调应用活动。它不包含业务逻辑。它也不保留业务状态,但它保留应用业务的进度状态
领域层本层包含关于领域的信息,这是业务软件的核心所在,在这里保留业务对象的状态,对业务对象和它们的状态持久化委托到了基础设施层
基础设施层本层作为其他层的支持库存在,它提供了层间的通信,实现对业务对象的持久化,包含对用户界面层的支持库

 

领域层应该关心核心的领域问题,它不应该关心基础设施方面的活动。用户界面也不应该与业务逻辑紧密捆绑。应用层是非常有必要的,它会成为业务逻辑之上的管理者,用来监督和协调应用的一切活动。

举个例子:用户想要预定一个飞行路线,就需要请求一个位于应用层的应用服务来完成这件事情。应用层从基础设施层取得相关领域对象,然后调用他们的相关方法,例如检查与其他已经被预定的飞行路线的安全界限。当领域对象执行完所有的检查并将他们的状态修改为"已决定"之后,应用服务将对象持久化到基础设施中。

实体

有一类对象看上去好像拥有标识符,它的标识符在历经软件的各种状态变更后仍能保持一致。对这些对象而言,重要的不是其属性,而是其延续性和标识,对象的延续性和标识会跨越甚至能够超出软件系统的生命周期,我们把这样的对象称为实体

OOP语言会把对象的实例存放在内存中,它们会为每个对象保持一个对象引用,但是这个对象引用并不是我们谈论的标识符。比如一个存放天气信息(如温度)的类,很容易产生同一个类的不同实例,这两个类都包含了同样的值,这两个对象是完全相当的甚至可以互换。但是它们拥有不同的引用,它们并不是实体。

在软件中,实现实体意味着创建标志符。对一个人而言,其标识符可能是组合:姓名,出生日期,出生地,父母姓名,当前地址。在祖国大陆这边,身份证号

也可以被认为是唯一标识符。

实体是领域模型中非常重要得对象,并且它们应该在建模开始时就被考虑,决定一个对象是否成为实体也很重要。

值对象

我们已经讨论了实体在建模中以及在建模阶段及早识别实体的重要性。实体在领域建模中是必须得对象。我们应该将所有的对象视为实体吗,每一个对象都应该有标识符吗?

我们考虑一个绘画应用,用户会看到一个画布并且他能够用任何宽度,样式和颜色来画任何点和线。创建一个叫Point的类非常有用,程序会对画布上每一个点创建这个类的一个实例。这样一个点会包含两个属性,对应屏幕的坐标。但是是否有必要为每个点创建一个标识符,这个标识符会有延续性吗?对于这样一个对象而言,它只是一个坐标而已。

有是否我们需要包含一个领域对象的某些属性。我们对它是哪一个对象并不感兴趣,而是只关系它拥有的属性。用来描述领域的特定方面,并且没有标识符的一个对象,叫做值对象

一条箴言是:如果值对象是可共享的,那么他们应该是不可变的。

另外,实体和值对象的界限不是绝对的,例如在一个外卖系统中,地址是一个值对象,从属于人。但在社区管理系统中,地址则是一个实体,与人存在多对多的关系,具体的划分其实要根据业务场景来定。

服务

当我们分析领域兵并分析对象的时,我们发现领域的有些方面难以被映射成对象。在有些领域中的动作,它们是一些动词,看上去不属于任何对象。比如,从一个账户向另一个账户转钱,这个功能应该放在转出的账户还是转入的账户呢,感觉放在两个中哪一个都不对劲。当这样的行为被识别出来的时候,最佳的实践是将它声明成一个服务

一个服务通常变成了多个对象的一个连接点,这也是为什么行为应该很自然的隶属于一个服务而不是被包含在领域对象中的一个原因。一个服务不应该替代通常隶属于领域对象的操作,我们不应该为每一个需要的操作创建一个服务。通常服务具备三个特征

1)服务执行的操作代表了一个领域的概念,这个领域概念无法自然地隶属于一个实体或者值对象

2)被执行的操作涉及到领域中的其他对象

3)操作是无状态的

注意,当我们使用服务时,领域层和应用层都会有服务,很容易混淆领域层的服务和基础设施层的服务。决定一个服务隶属于哪一层是很困难的,如果所执行的操作概念上属于应用层,那么服务就应该被放到这个层。如果操作是关于领域对象,而且确实与领域有关,那它就应该属于领域层。

模块

对于一个大型的复杂项目而言,模型趋向于越来越大,当模型发展到某个规模,将它作为整体来讨论很困难。基于这个原因,必须将模型组织到模块中,模块被用来作为组织相关概念和任务以便降低复杂性的一种方法

模块被广泛应用在很多项目中,如果你查看模块包含的内容以及那些模块的关系,就很容易从中掌握大型模型的状况。理解了模块之间的交互后,人们就可以开始处理模块中的细节了。这是管理复杂性最简单有效的方法。

聚合

将实体和值对象聚集在聚合之中,并且定义各个聚合之间的边界。没个聚合选择一个实体作为根,并且通过根来控制所有对边界内的对象访问。允许外部对象持有对根的引用。

聚合的一个简单例子如下图所示,客户是聚合的根,并且其他所有的对象都内部的。

聚合太难理解了,阿里大神的专栏

https://zhuanlan.zhihu.com/p/381228978

保持模型的一致性

界定上下文

每一个模型都有上下文,当我们处理一个独立的模型时,上下文是隐含的。如果将明显不同模型的代码合并在一起,软件就会变得有很多BUG,不可靠,而且难以理解。

解决这个问题主要的思想是定义模型的范围,定出它的上下文边界,然后尽最大可能保持模型统一

常用的划分限界上下文的方法是

对前一步(事件风暴)产生的聚合进行分组,通过业务的内聚性和关联度划分边界,结合限界上下文的定义进行判断,并给出上下文名称。
[服务化设计阶段路径方案]

方法一:"相关性"全凭经验

相关性是一个过于抽象的规则,非常依赖经验,列举一个例子,庞大的电商领域,有丰富经验的架构师肯定知道怎么划分领域,但是要让这个架构师去划分全球飞机航线地图,可能他不一定有相关的经验了。

方法二:不健康的聚合上下文

聚合分组法很容易导向一种按照聚合划分的架构,服务围绕聚合建设,而非针对某个业务价值,也就无法提供正确的业务价值。围绕聚合建设的服务,看上去可以复用,但是会造成服务间的紧耦合,容易成为最糟糕的分布式单体架构。

限界上下文的主题是什么呢?我认为是子域,每个限界上下文专注于解决某个特定的子域的问题。每个子域都对应一个明确的问题,提供独立的价值,所以每个子域都相对独立

那如何划分子域呢,我们通过某种方式,将领域分解成逻辑上相互独立且没有交叉的子域。在这里的方法是通过产品愿景,识别核心域,进而识别核心域周边的子域

核心域

每一个子域甚至每一个领域模型都是为了产品愿景而存在的。我们分解子域的第一步,就是从产品愿景中获取核心域。产品愿景包含“相对抽象的产品价值”,以及“实现该价值的主要功能”。其中,主要功能就是我们寻找核心域的依据。想象一下,如果要做MVP的话,我们会挑选最能够提供其核心价值的功能来开发,以验证产品价值。MVP往往就是核心域

以上述活动运营系统为例,其产品愿景是通过各种吸引用户的优惠活动,以帮助客户通过活动提升用户量和知名度。其核心域是给客户提供吸引用户的多样的灵活的活动,包括活动形式、活动规则和多种奖励

识别核心域周边的子域

核心域往往不会独立存在,会有其他子域同核心域一起才能达成业务目标。

  • 有哪些子域是用来支撑核心域的?
    这些子域是帮助核心域更好的工作。例如提供审批流程以配置核心域,提供各种辅助功能更好的为核心域提供内容。
  • 有哪些子域是核心域衍生出来的?
    核心域经常会产生一些数据,这些数据也有其价值。比如产生各种报表,活动奖励的发放记录。
  • 有哪些子域是用来支撑或衍生自这些新识别出的子域的?
    用来支撑核心域的子域、以及核心域衍生的子域,也有各自的支撑子域和衍生子域。
 
领域建模好文推荐:
https://www.zhihu.com/question/25089273/answer/233316164
https://zhuanlan.zhihu.com/p/157179353
 
 
 

 

Pre: DDD系列-战术设计

Next: DDD系列-限界上下文

116
Sign in to leave a comment.
No Leanote account? Sign up now.
0 comments
Table of content