作者:张晓龙 \
起源:https://www.jianshu.com/p/a77…
引言
在探讨 DDD 分层架构的模式之前,咱们先一起回顾一下 DDD 和分层架构的相干常识。
DDD
DDD(Domain DrivenDesign,畛域驱动设计)作为一种软件开发办法,它能够帮忙咱们设计高质量的软件模型。在正确实现的状况下,咱们通过 DDD 实现的设计恰好就是软件的工作形式。
UL(Ubiquitous Language,通用语言)是团队共享的语言,是 DDD 中最具威力的个性之一。不论你在团队中的角色如何,只有你是团队的一员,你都将应用 UL。因为 UL 的重要性,所以须要让每个概念在各自的上下文中是清晰无歧义的,于是 DDD 在策略设计上提出了模式 BC(BoundedContext,限界上下文)。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 事务模型》,感兴趣的同学能够看看。
综上所述,DDD 六层架构能够看做是 DDD 五层架构在特定畛域的变体,咱们统称为 DDD 五层架构,而 DDD 五层架构与传统的四层架构相似,都是 限定型涣散分层架构。
模式三:六边形架构
有一种办法能够改良分层架构,即依赖倒置准则(Dependency Inversion Principle,DIP),它通过扭转不同层之间的依赖关系达到改良目标。
依赖倒置准则由 Robert C. Martin 提出,正式定义为:
高层模块不应该依赖于底层模块,两者都应该依赖于形象。
形象不应该依赖于细节,细节应该依赖于形象。
依据该定义,DDD 分层架构中的低层组件应该依赖于高层组件提供的接口,即无论高层还是低层都依赖于形象,整个分层架构如同被推平了。如果咱们把分层架构推平,再向其中退出一些对称性,就会呈现一种具备对称性特色的架构格调,即六边形架构。六边形架构是 AlistairCockburn 在 2005 年提出的,在这种架构中,不同的客户通过“平等”的形式与零碎交互。须要新的客户吗?不是问题。只须要增加一个新的适配器将客户输出转化成能被零碎 API 所了解的参数就行。同时,对于每种特定的输入,都有一个新建的适配器负责实现相应的转化性能。
六边形架构也称为端口与适配器,如下图所示:
六边形每条不同的边代表了不同类型的端口,端口要么解决输出,要么解决输入。对于每种外界类型,都有一个适配器与之对应,外界通过应用层 API 与外部进行交互。上图中有 3 个客户申请均到达雷同的输出端口(适配器 A、B 和 C),另一个客户申请应用了适配器 D。假如前 3 个申请应用了 HTTP 协定(浏览器、REST 和 SOAP 等),而后一个申请应用了 AMQP 协定(比方 RabbitMQ)。端口并没有明确的定义,它是一个非常灵活的概念。无论采纳哪种形式对端口进行划分,当客户申请达到时,都应该有相应的适配器对输出进行转化,而后端口将调用应用程序的某个操作或者向应用程序发送一个事件,控制权由此交给外部区域。
应用程序通过公共 API 接管客户申请,应用畛域模型来解决申请。咱们能够将 DDD 战术设计的建模元素 Repository 的实现看作是长久化适配器,该适配器用于拜访先前存储的聚合实例或者保留新的聚合实例。正如图中的适配器 E、F 和 G 所展现的,咱们能够通过不同的形式实现资源库,比方关系型数据库、基于文档的存储、分布式缓存或内存存储等。如果应用程序向外界发送畛域事件音讯,咱们将应用适配器 H 进行解决。该适配器解决音讯输入,而下面提到的解决 AMQP 音讯的适配器则是解决音讯输出的,因而应该应用不同的端口。
咱们在理论的我的项目开发中,不同层的组件能够同时开发。当一个组件的性能明确后,就能够立刻启动开发。因为该组件的用户有多个,并且这些用户的侧重点不同,所以须要提供多个不同的接口。同时,这些用户的意识也是不断深入的,可能会屡次重构相干的接口。于是,组件的多个用户常常会找组件的开发者探讨这些问题,无形中升高了组件的开发效率。
咱们换一种形式,组件的开发者在明确了组件的性能后就专一于性能的开发,确保性能稳固和高效。组件的用户本人定义组件的接口(端口),而后基于接口写测试,并一直演进接口。在跨层集成测试时,由组件开发者或用户再开发一个适配器就能够了。
六边形架构模式的演变
只管六边形架构模式曾经很好,然而没有最好只有更好,演变没有止境。在六边形架构模式提出后的这些年,又顺次衍生出三种六边形架构模式的变体,感兴趣的读者能够点击链接自行学习:
- Jeffrey Palermo 在 2008 年提出了 洋葱架构,六边形架构是洋葱架构的一个超集。
- Robert C. Martin 在 2012 年提出了 洁净架构(Clean Architecture),这是六边形架构的一个变体。
- Russ Miles 在 2013 年提出了 Life Preserver 设计,这是一种基于六边形架构的设计。
小结
本文先和读者一起回顾了 DDD 和分层架构的相干常识,而后将 DDD 分层架构中罕用的三种模式(四层架构、五层架构和六边形架构)联合实践经验别离进行具体论述,使得读者深刻理解 DDD 分层架构模式,以便在微服务的开发实际中依据具体情况抉择最合适的 DDD 分层架构模式,从而交付构造清晰且易保护的软件产品。
近期热文举荐:
1.600+ 道 Java 面试题及答案整顿(2021 最新版)
2. 终于靠开源我的项目弄到 IntelliJ IDEA 激活码了,真香!
3. 阿里 Mock 工具正式开源,干掉市面上所有 Mock 工具!
4.Spring Cloud 2020.0.0 正式公布,全新颠覆性版本!
5.《Java 开发手册(嵩山版)》最新公布,速速下载!
感觉不错,别忘了顺手点赞 + 转发哦!