领域驱动设计(DDD):分层架构的三种模式
DDD
DDD(Domain Driven Design,领域驱动设计)作为⼀种软件开发⽅法,它可以帮助我们设计⾼质量的软件模型。在正确实现的情况下,我们通过DDD完成的设计恰恰就是软件的⼯作⽅式。
UL(Ubiquitous Language,通⽤语⾔)是团队共享的语⾔,是DDD中最具威⼒的特性之⼀。不管你在团队中的⾓⾊如何,只要你是团队的⼀员,你都将使⽤UL。由于UL的重要性,所以需要让每个概念在各⾃的上下⽂中是清晰⽆歧义的,于是DDD在战略设计上提出了模式
BC(Bounded Context,限界上下⽂)。UL和BC同时构成了DDD的两⼤⽀柱,并且它们是相辅相成的,即UL都有其确定的上下⽂含义,⽽BC中的每个概念都有唯⼀的含义。
⼀个业务领域划分成若⼲个BC,它们之间通过Context Map进⾏集成。BC是⼀个显式的边界,领域模型便存在于这个边界之内。领域模型是关于某个特定业务领域的软件模型。通常,领域模型通过对象模型来实现,这些对象同时包含了数据和⾏为,并且表达了准确的业务含义。
从⼴义上来讲,领域即是⼀个组织所做的事情以及其中所包含的⼀切,表⽰整个业务系统。由于“领域模型”包含了“领域”这个词,我们可能会认为应该为整个业务系统创建⼀个单⼀的、内聚的和全功能式的模
型。然⽽,这并不是我们使⽤DDD的⽬标。正好相反,领域模型存在于BC内。
在微服务架构实践中,⼈们⼤量地使⽤了DDD中的概念和技术:
微服务中应该⾸先建⽴UL,然后再讨论领域模型。
⼀个微服务最⼤不要超过⼀个BC,否则微服务内会存在有歧义的领域概念。
⼀个微服务最⼩不要⼩于⼀个聚合,否则会引⼊分布式事务的复杂度。
微服务的划分过程类似于BC的划分过程,每个微服务都有⼀个领域模型。
微服务间的集成可以通过Context Map来完成,⽐如ACL(Anticorruption Layer,防腐层)。
微服务间最好采⽤Domain Event(领域事件)来进⾏交互,使得微服务可以保持松耦合。
……
分层架构
分层架构的好处是显⽽易见的。⾸先,由于层间松散的耦合关系,使得我们可以专注于本层的设计,
⽽不必关⼼其他层的设计,也不必担⼼⾃⼰的设计会影响其它层,对提⾼软件质量⼤有裨益。其次,分层架构使得程序结构清晰,升级和维护都变得⼗分容易,更改某层的具体实现代码,只要本层的接⼝保持稳定,其他层可以不必修改。即使本层的接⼝发⽣变化,也只影响相邻的上层,修改⼯作量⼩且错误可以控制,不会带来意外的风险。
要保持程序分层架构的优点,就必须坚持层间的松散耦合关系。设计程序时,应先划分出可能的层次,以及此层次提供的接⼝和需要的接⼝。设计某层时,应尽量保持层间的隔离,仅使⽤下层提供的接⼝。
关于分层架构的优点,Martin Fowler在《Patterns of Enterprise Application Architecture》⼀书中给出了答案:
开发⼈员可以只关注整个结构中的某⼀层。
可以很容易的⽤新的实现来替换原有层次的实现。
可以降低层与层之间的依赖。
有利于标准化。
利于各层逻辑的复⽤。
微服务在哪里“⾦⽆⾜⾚,⼈⽆完⼈”,分层架构也不可避免具有⼀些缺陷:
降低了系统的性能。这是显然的,因为增加了中间层,不过可以通过缓存机制来改善。
可能会导致级联的修改。这种修改尤其体现在⾃上⽽下的⽅向,不过可以通过依赖倒置来改善。
在每个BC中为了凸显领域模型,DDD中提出了分层架构模式。最近⼏年,笔者在实践DDD的过程中,也经常使⽤分层架构模式,本⽂主要分享DDD分层架构中⽐较经典的三种模式。
模式⼀:四层架构
Eric Evans在《领域驱动设计-软件核⼼复杂性应对之道》这本书中提出了传统的四层架构模式,如下图所⽰:
User Interface为⽤户界⾯层(或表⽰层),负责向⽤户显⽰信息和解释⽤户命令。这⾥指的⽤户可以是另⼀个计算机系统,不⼀定是使⽤⽤户界⾯的⼈。
Application为应⽤层,定义软件要完成的任务,并且指挥表达领域概念的对象来解决问题。这⼀层所
负责的⼯作对业务来说意义重⼤,也是与其它系统的应⽤层进⾏交互的必要渠道。应⽤层要尽量简单,不包含业务规则或者知识,⽽只为下⼀层中的领域对象协调任务,分配⼯作,使它们互相协作。它没有反映业务情况的状态,但是却可以具有另外⼀种状态,为⽤户或程序显⽰某个任务的进度。
Domain为领域层(或模型层),负责表达业务概念,业务状态信息以及业务规则。尽管保存业务状态的技术细节是由基础设施层实现的,但是反映业务情况的状态是由本层控制并且使⽤的。领域层是业务软件的核⼼,领域模型位于这⼀层。
Infrastructure层为基础实施层,向其他层提供通⽤的技术能⼒:为应⽤层传递消息,为领域层提供持久化机制,为⽤户界⾯层绘制屏幕组件,等等。基础设施层还能够通过架构框架来⽀持四个层次间的交互模式。
传统的四层架构都是限定型松散分层架构,即Infrastructure层的任意上层都可以访问该层(“L”型),⽽其它层遵守严格分层架构。
笔者在四层架构模式的实践中,对于分层的本地化定义主要为:
User Interface层主要是Restful消息处理,配置⽂件解析,等等。
Application层主要是多进程管理及调度,多线程管理及调度,多协程调度和状态机管理,等等。
Domain层主要是领域模型的实现,包括领域对象的确⽴,这些对象的⽣命周期管理及关系,领域服务的定义,领域事件的发布,等等。
Infrastructure层主要是业务平台,编程框架,第三⽅库的封装,基础算法,等等。
说明:严格意义上来说,User Interface指的是⽤户界⾯,Restful消息和配置⽂件解析等处理应该放在Application层,User Interface层没有的话就空缺。但User Interface也可以理解为⽤户接⼝,所以将Restful消息和配置⽂件解析等处理放在User Interface层也⾏。
模式⼆:五层架构
James O. Coplien和Trygve Reenskaug在2009年发表了⼀篇论⽂《DCI架构:⾯向对象编程的新构想》,标志着DCI架构模式的诞⽣。有趣的是James O. Coplien也是MVC架构模式的创造者,这个⼤叔⼀辈⼦就⼲了两件事,即年轻时创造了MVC和年⽼时创造了DCI,其他时间都在思考,让我辈望尘莫及。
⾯向对象编程的本意是将程序员与⽤户的视⾓统⼀于计算机代码之中:对提⾼可⽤性和降低程序的理解难度来说,都是⼀种恩赐。可是虽然对象很好地反映了结构,但在反映系统的动作⽅⾯却失败了,DCI的构想是期望反映出最终⽤户的认知模型中的⾓⾊以及⾓⾊之间的交互。
传统上,⾯向对象编程语⾔拿不出办法去捕捉对象之间的协作,反映不了协作中往来的算法。就像对象的实例反映出领域结构⼀样,对象的协作与交互同样是有结构的。协作与交互也是最终⽤户⼼智模型的组成部分,但你在代码中不到⼀个内聚的表现形式去代表它们。在本质上,⾓⾊体现的是⼀般化的、抽象的算法。⾓⾊没有⾎⾁,并不能做实际的事情,归根结底⼯作还是落在对象的头上,⽽对象本⾝还担负着体现领域模型的责任。
⼈们⼼⽬中对“对象”这个统⼀的整体却有两种不同的模型,即“系统是什么”和“系统做什么”,这就是DCI要解决的根本问题。⽤户认知⼀个个对象和它们所代表的领域,⽽每个对象还必须按照⽤户⼼⽬中的交互模型去实现⼀些⾏为,通过它在⽤例中所扮演的⾓⾊与其他对象联结在⼀起。正因为最终⽤户能把两种视⾓合为⼀体,类的对象除了⽀持所属类的成员函数,还可以执⾏所扮演⾓⾊的成员函数,就好像那些函数属于对象本⾝⼀样。换句话说,我们希望把⾓⾊的逻辑注⼊到对象,让这些逻辑成为对象的⼀部分,⽽其地位却丝毫不弱于对象初始化时从类所得到的⽅法。我们在编译时就为对象安排好了扮演⾓⾊时可能需要的所有逻辑。如果我们再聪明⼀点,在运⾏时才知道了被分配的⾓⾊,然后注⼊刚好要⽤到的逻辑,也是可以做到的。
算法及⾓⾊-对象映射由Context拥有。Context“知道”在当前⽤例中应该哪个对象去充当实际的演员,然后负责把对象“cast”成场景中的相应⾓⾊(cast 这个词在戏剧界是选⾓的意思,此处的⽤词⾄少符合该词义,另⼀⽅⾯的⽤意是联想到cast 在某些编程语⾔类型系统中的含义)。在典型的实现⾥,每
个⽤例都有其对应的⼀个Context 对象,⽽⽤例涉及到的每个⾓⾊在对应的Context ⾥也都有⼀个标识符。Context 要做的只是将⾓⾊标识符与正确的对象绑定到⼀起。然后我们只要触发Context⾥的“开场”⾓⾊,代码就会运⾏下去。
于是我们有了完整的DCI架构(Data、Context和Interactive三层架构):
Data层描述系统有哪些领域概念及其之间的关系,该层专注于领域对象的确⽴和这些对象的⽣命周期管理及关系,让程序员站在对象的⾓度思考系统,从⽽让“系统是什么”更容易被理解。
Context层:是尽可能薄的⼀层。Context往往被实现得⽆状态,只是到合适的role,让role交互起来完成业务逻辑即可。但是简单并不代表不重要,显⽰化context层正是为⼈去理解软件业务流程提供切⼊点和主线。
Interactive层主要体现在对role的建模,role是每个context中复杂的业务逻辑的真正执⾏者,体现“系统做什么”。role所做的是对⾏为进⾏建模,它联接了context和领域对象。由于系统的⾏为是复杂且多变的,role使得系统将稳定的领域模型层和多变的系统⾏为层进⾏了分离,由role专注于对系统⾏为进⾏建模。该层往往关注于系统的可扩展性,更加贴近于软件⼯程实践,在⾯向对象中更多的是以类的视⾓进⾏思考设计。
DCI⽬前⼴泛被看作是对DDD的⼀种发展和补充,⽤在基于⾯向对象的领域建模上。显式的对role进⾏建模,解决了⾯向对象建模中的充⾎模型和贫⾎模型之争。DCI通过显式的⽤role对⾏为进⾏建模,同时让role在context中可以和对应的领域对象进⾏绑定(cast),从⽽既解决了数据边界和⾏为边界不⼀致的问题,也解决了领域对象中数据和⾏为⾼内聚低耦合的问题。
⾯向对象建模⾯临的⼀个棘⼿问题是数据边界和⾏为边界往往不⼀致。遵循模块化的思想,我们通过类将⾏为和其紧密耦合的数据封装在⼀起。但是在复杂的业务场景下,⾏为往往跨越多个领域对象,这样的⾏为如果放在某⼀个对象中必然会导致别的对象需要向该对象暴漏其内部状态。所以⾯向对象发展的后来,领域建模出现两种派别之争,⼀种倾向于将跨越多个领域对象的⾏为建模在领域服务中。如果这种做法使⽤过度,则会导致领域对象变成只提供⼀堆get⽅法的哑对象,这种建模结果被称之为贫⾎模型。⽽另⼀派则坚定的认为⽅法应该属于领域对象,所以所有的业务⾏为仍然被放在领域对象中,这样导致领域对象随着⽀持的业务场景变多⽽变成上帝类,⽽且类内部⽅法的抽象层次很难⼀致。另外由于⾏为边界很难恰当,导致对象之间数据访问关系也⽐较复杂,这种建模结果被称之为充⾎模型。
关于多⾓⾊对象,举个⽣活中的例⼦:
⼈有多重⾓⾊,不同的⾓⾊履⾏的职责不同:
作为⽗母:我们要给孩⼦讲故事,陪他们玩游戏,哄它们睡觉。
作为⼦⼥:我们要孝敬⽗母,听取他们的⼈⽣建议。
作为下属:我们要服从上司的⼯作安排,并⾼质量完成任务。
作为上司:我们要安排下属的⼯作,并进⾏培养和激励。
……
这⾥⼈(⼤对象)聚合了多个⾓⾊(⼩类),⼈在某种场景下,只能扮演特定的⾓⾊:
在孩⼦⾯前,我们是⽗母。
在⽗母⾯前,我们是⼦⼥。
在上司⾯前,我们是下属。
在下属⾯前,我们是上司。
……
引⼊DCI后,DDD四层架构模式中的Domain层变薄了,以前Domain层对应DCI中的三层,⽽现在:
Domain层只保留了DCI中的Data层和Interaction层,我们在实践中通常将这两层使⽤⽬录隔离,即通过两个⽬录object和role来分离层Data和Interaction。
DCI中的Context层从Domain层上移变成Context层。
因此,DDD分层架构模式就变成了五层,如下图所⽰:
笔者在实践中,将这五层的本地化定义为:
User Interface是⽤户接⼝层,主要⽤于处理⽤户发送的Restful请求和解析⽤户输⼊的配置⽂件等,并将信息传递给Application层的接⼝。
Application层是应⽤层,负责多进程管理及调度、多线程管理及调度、多协程调度和维护业务实例的状态模型。当调度层收到⽤户接⼝层的请求后,委托Context层与本次业务相关的上下⽂进⾏处理。
Context是环境层,以上下⽂为单位,将Domain层的领域对象cast成合适的role,让role交互起来完成业务逻辑。
Domain层是领域层,定义领域模型,不仅包括领域对象及其之间关系的建模,还包括对象的⾓⾊role的显式建模。
Infrastructure层是基础实施层,为其他层提供通⽤的技术能⼒:业务平台,编程框架,持久化机制,消息机制,第三⽅库的封装,通⽤算法,等等。
DDD五层架构模式讨论完了吗?故事还没有结束……
笔者参与的很多DDD落地实践,都是⾯向控制⾯或管理⾯且消息交互⽐较多的系统。这类系统的⼀次
业务,包含⼀组同步消息或异步消息构成的序列,如果都放在Context层,会导致该层的代码⽐较复杂,于是我们考虑:
Context层在⾯向控制⾯或管理⾯且消息交互⽐较多的系统中⼜分裂成两层,即Context层和⼤Context层。
Context层处理单位为Action,对应⼀条同步消息或异步消息。
⼤Context层对应⼀个事务处理,由⼀个Action序列组成,⼀般通过Transaction DSL实现,所以我们习惯把⼤Context层叫做Transaction DSL层。
Application层在⾯向控制⾯或管理⾯且消息交互⽐较多的系统中经常会做⼀些调度相关的⼯作,所以我们习惯把Application层叫做Scheduler层。
因此,在⾯向控制⾯或管理⾯且消息交互⽐较多的系统中,DDD分层架构模式就变成了六层,如下图所⽰:
笔者在实践中,将这六层的本地化定义为:
User Interface是⽤户接⼝层,主要⽤于处理⽤户发送的Restful请求和解析⽤户输⼊的配置⽂件等,并将信息传递给Scheduler层的接⼝。
Scheduler是调度层,负责多进程管理及调度、多线程管理及调度、多协程调度和维护业务实例的状态模型。当调度层收到⽤户接⼝层的请求后,委托Transaction层与本次操作相关的事务进⾏处理。
Transaction是事务层,对应⼀个业务流程,⽐如UE Attach,将多个同步消息或异步消息的处理序列组合成⼀个事务,⽽且在⼤多场景下,都有选择结构。万⼀事务执⾏失败,则⽴即进⾏回滚。当事务层收到调度层的请求后,委托Context层的Action进⾏处理,常常还伴随使⽤Context层的Specification(谓词)进⾏Action的选择。
Context是环境层,以Action为单位,处理⼀条同步消息或异步消息,将Domain层的领域对象cast成合适的role,让role交互起来完成业务逻辑。环境层通常也包括Specification的实现,即通过Domain层的知识去完成⼀个条件判断。
Domain层是领域层,定义领域模型,不仅包括领域对象及其之间关系的建模,还包括对象的⾓⾊role的显式建模。
Infrastructure层是基础实施层,为其他层提供通⽤的技术能⼒:业务平台,编程框架,持久化机制,消息机制,第三⽅库的封装,通⽤算法,等等。
事务层的核⼼是事务模型,事务模型的框架代码⼀般放在基础设施层。关于事务模型,笔者以前分享过⼀篇⽂章——《Golang事务模型[1]》,感兴趣的同学可以看看。
综上所述,DDD六层架构可以看做是DDD五层架构在特定领域的变体,我们统称为DDD五层架构,⽽DDD五层架构与传统的四层架构类似,都是限定型松散分层架构。
模式三:六边形架构
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论