DDD Layers & Clean Architecture DDD分层和简洁架构
https://blog.csdn.net/u011385940/article/details/122517580
There are four fundamental layers of a Domain Driven Based Solution;
一个基于领域驱动的解决方案有四层,如下图所示:
Business Logic places into two layers, the Domain layer and the Application Layer, while they contain different kinds of business logic;
业务逻辑分为两层,即领域层和应用层,然而它们包含不同种类的业务逻辑。
Domain Layer implements the core, use-case independent business logic of the domain/system.
领域层实现核心,用例独立于领域/系统的的业务逻辑。
Application Layer implements the use cases of the application based on the domain. A use case can be thought as a user interaction on the User Interface (UI).
应用层实现基于领域的应用程序的用例。一个用例可以被认为是用户在用户界面(UI)上的交互。
Presentation Layer contains the UI elements (pages,components) of the application.
表现层包含应用程序的用户界面元素(页面、组件)。
Infrastructure Layer supports other layer by implementing the abstractions and integrations to 3rd-party library and systems.
基础设施层通过实现抽象、集成第三方库和其它系统来支持其他层。
The same layering can be shown as the diagram below and known as the Clean Architecture, or sometimes the Onion Architecture:
同样的分层被称为简洁架构,有时被称为洋葱架构,如下图所示:
In the Clean Architecture, each layer only depends on the layer directly inside it. The most independent layer is shown in the most inner circle and it is the Domain Layer.
在简洁架构中,每一层仅直接依赖于其内部的层。最独立的一层显示在最内圈,它就是领域层。
好的应用架构,都遵循一些共同模式,不管是六边形架构、洋葱圈架构、整洁架构、还是COLA架构,都提倡以业务为核心,解耦外部依赖,分离业务复杂度和技术复杂度。
分层架构(Layered Architecture)
分层架构就是将业务应用划分为对应的层级模块。每个层职责不同。
四层结构定义:
接口层: 统一处理系统对外的服务接口,可以是直接查询,也可以是三方系统对接。
应用层: 调用各个领域完成一个具体的业务流程应用,不需要关心具体的业务实现。
领域层: 实际完成业务逻辑处理的地方,包含领域模型和领域服务,无须关心显示及存储等相关问题。
基础设施层: 提供底层支撑功能,包括持久化、序列化与反序列化、消息中间件等。
优点:
可降低层级之间依赖;
利于层级模块复用;
安全性高,外部系统只从接口层访问,统一入口点;
项目结构清晰,简单。
常见两种分层架构图:
图一:四层
图二:五层
六边形架构(Hexagonal Architecture)
六边形架构可以理解为“去结构化”的架构,系统内部与外界的交互过程通过适配器来完成,没有层次,所有交互双方只知道适配器的存在,不用知晓对方的存在。
六边形又称为端口适配器模式。六边形架构将系统分为内部和外部,内部代表具体的业务逻辑,也就是DDD中强调的领域模型(其中包含领域服务,对业务概念建立的模型等);外部代表基础设施和应用等,类似RESTful API, SOAP, AMQP, 或者数据库,内存,文件系统,以及自动化测试。 内部和外部之间通过端口通信。端口代表协议,通常是以API呈现。端口的具体实现是适配器,负责对接具体的外部系统或内部逻辑。该架构可以非常容易的实现外部替换、依赖倒置、自动测试等功能。
一个端口对应多个适配器,是对一类外部系统的归纳。
适配器分为主适配器和次适配器:
主适配器(Driving Adapter):代表接收用户输入,调用端口并返回数据。
次适配器(Driven Adapter):实现应用的出口端口,向外部工具执行操作。
六边形架构优点:
业务领域的边界更加清晰。
更好的可扩展性。(比如需要新增一种协议(数据库、MQ...)的支持,那么只需要定义一组端口-适配器即可。对原有端口-适配器不影响)
对测试的友好支持。
更容易实施DDD。
整洁架构(Clean Architecture)
整洁架构(又名洋葱架构 Onion Architecture),六边形架构的变种。
整洁架构是Robot C.Martin在《整洁架构之道》一书中提出来的架构设计思想。它以圆环的形式把系统分成了几个不同的层次,因此又被称为洋葱架构。
在整洁架构里,同心圆代表应用软件的不同部分,从里到外依次是领域模型、领域服务、应用服务、最外围是容易变化的内容,如界面和基础设施(如数据存储等)。整洁架构是以领域为中心,不是以数据为中心。
整洁架构最主要原则是依赖原则,它定义了各层的依赖关系,越往里,依赖越低,代码级别越高。外圆代码依赖只能指向内圆,内圆不知道外圆的任何事情。一般来说,外圆的声明(包括方法、类、变量)不能被内圆引用。同样的,外圆使用的数据格式也不能被内圆使用。
各层职责:
Domain Model:业务模型,对应DDD中的Entity、值对象等。
Domain Services:核心业务逻辑。
Application Services:应用的输入输出层。
User Interface/Tests/Infrastructure:适配器层(例如数据库、用户界面及外部服务)。
优点:
各层职责清晰,提高了大型复杂项目的可维护性。
结合DDD,使项目以领域模型为主。
保证内部核心领域的独立和无依赖,外部技术细节可以通过接口和适配器随时更换,在不丢失任何业务逻辑的情况下替换掉整个技术实现,从而增加系统的灵活性和可测性。
DDD架构(Domain Driven Design Architecture)
准确地说,DDD不是架构,而是一种开发思想。 DDD带来的最大改变是让我们得以从 “数据驱动”转向“领域驱动”,让我们知道领域是应用的核心,其它都是技术细节,随时可以被替换。
DDD四层架构见分层架构图一。
DDD采用统一语言,避免组件划分过程中的边界错位。 让业务架构和系统架构形成绑定关系,从而建立针对业务变化的高响应力架构。
战略层面:针对业务问题分析和分解,通过识别核心问题域来降低分析的复杂度。
战术层面:识别问题域里的不同业务上下文来进行面向业务需求的组件化。
DDD基本概念:
通用语言:通过画领域模型图、类图来建立一种沟通关系。
实体 Entity:从属于某个聚合根。实体具有id,有生命周期。
值对象 Value Object:无生命周期,归属具体的实体。
聚合 Aggregate:领域模型最底层的边界。逻辑边界。一个聚合只有一个聚合根。
聚合根 Aggregate Root:最抽象、最普遍的特征。也叫做根实体。
工厂 Factory:隐藏对象的复杂创建逻辑。
仓库 Repository:封装获取对象的逻辑。
限界上下文 Bounded Context:定义了每个模型的应用范围,可理解为一个子域对应一个上下文。一个上下文可能包含多个聚合,每个聚合都有一个根实体,叫做聚合根。
COLA架构 (Clean Object-oriented & Layered Architecture)
一方面COLA是一种架构思想,是整合了洋葱圈架构、适配器架构、DDD、整洁架构、TMF等架构思想的一种应用架构。
基于扩展点+元数据+CQRS+DDD的应用架构:扩展性好,贯彻了OO思想,有一套完整的规范标准,并采用CQRS和领域建模技术,很大程度可降低应用的复杂度。
层级职责:
cola-adapter:负责对前端展示的路由和适配。
cola-app:负责获取输入,组装context,做输入校验,调用领域层做业务处理。
cola-client:二方库组件,提供应用对外的接口。
cola-domain:领域层,封装核心业务逻辑。
cola-infrastructure:基础设施层,处理技术细节加领域防腐。
start:spring boot启动层。
图来源:https://blog.csdn.net/significantfrank/article/details/110934799
CQRS架构
命令查询职责分离(CQRS)是指读取和写入分别拥有单独的数据结构。 使用CQRS理由是,在复杂领域中,使用单一模型处理读取和写入过于复杂,我们可以通过分离模型来简化设计和实现。
其基本思想在于任何一个对象的方法可以分为以下两类:
命令(Command):不返回任何结果(void),但会改变对象的状态。
查询(Query):返回结果,但是不会改变对象的状态,对系统没有副作用。
CQRS查询和更新数据(命令)模型不一样,CQRS强调的是command和query访问的数据模型不同,分别根据command与query需求的不同特性设计数据模型。 命令模型数据变更后,需同步给查询模型。
在很多系统业务设计上,数据库里数据模型很难和业务领域模型一致,所以DDD中有领域模型(Domain model)的概念,在领域层转换成对应的数据库DO(Data Object)实体。
优点:
读写逻辑高度解耦,符合单一职责(模型分离,可独立设计)
独立部署有更好的伸缩性
缺点:
带来了复杂性,传统过程式编程直接用一个模型搞定,现在变成两个模型,还要结合一些DDD的思想才能更好的实现;
数据非强一致性,是最终一致性。(个人理解:如果同步实现保证强一致性的话,命令端写成功之后还需要往查询端写,会降低系统可用性)
帮助工程团队将函数编程原理应用到高级设计和体系结构与架构的通俗易懂的思想和最佳实践。
关于函数式编程或FP的许多文章都专注于低级编码实践(例如避免副作用)和FP特定模式(例如可怕的monad)。但是,它们不涉及高级设计和体系结构。然而,FP原则可以大规模应用。实际上,从后端的无服务器到前端的Redux / Elm风格的框架,许多流行的框架和架构样式都源于函数式编程。
如果使用得当,FP原理可以降低复杂性,同时提高应用程序的可测试性和可维护性。这是函数架构。
FP原理适用于软件架构
函数式编程的三个原理与软件架构特别相关。首先是函数是独立的值。也就是说,可以像对待其他独立值一样对待它们,例如整数和字符串。可以将它们分配给变量,存储在列表中,作为参数传递,作为结果返回等等。
在函数架构中,基本单元也是函数,但是我喜欢称其为工作流workflow,工作流是函数的基本单元。它可以称为一个功能特性、用例、场景、故事或任何您想调用的。就像函数一样在编码级别是“某个事物”,这些工作流是架构级别的“事物”,是架构的基本构建块。
其次,组合是构建系统的主要方式。只需将一个输出连接到另一个输入即可构成两个简单功能。结果是可以用作更多合成的起点的另一个功能。
组合是一个非常重要的概念,函数式程序员拥有一套标准工具,例如monads,即使输入和输出不完全匹配也可以进行组合。
从体系结构和架构的角度来看,由较小的函数组成较大的函数,其结果最明显是,函数系统看起来像带有输入和输出的管道,而不是面向消息的请求/响应模型。
[图片上传失败...(image-f41b4d-1644728141682)]
每个工作流程函数通常具有相同的结构:读取数据、制定业务决策并根据需要转换数据,最后,在另一端输出任何新数据或事件。这些步骤中的每一个都可以依次视为较小的函数。分支和其他类型的复杂性可能会发挥作用,但是即使工作流程变得越来越大和越来越复杂,数据也始终会朝一个方向流动。
这种组合方法意味着我们仅结合了特定业务工作流所需的特定组件。不需要传统的分层体系结构。当我们向系统中添加新功能时,每个新工作流程所需的函数都是独立定义的,而不是分组为数据库或服务层。
如果我们确实需要在不同的工作流中使用完全相同的函数,则可以将该函数一次定义为子函数,然后在需要它的工作流中将其重新用作共享步骤。这就是组合方法如此吸引人的原因:工作流作为独立的单元进行设计和构建,仅包含其所需的功能,但是当需要时,我们仍然可以利用重用和组件化的所有好处。
最后,函数式程序员尝试尽可能多地使用纯函数。纯函数是确定性的(给定的输入始终会导致相同的输出),并且没有副作用(例如突变或I / O)。它们非常易于测试(确定性!),并且易于理解而无需深入研究其实现(无副作用!)。
与外界互动
您不需要鼓励开发人员使用洋葱架构,作为FP 方法的副作用它会自动发生。
当然,在某些时候,我们将需要进行I / O操作-读写文件,访问数据库等等。函数式程序员试图将这种不确定性尽可能地保持在管道的边缘。某些语言(例如Elm和Haskell)对此非常严格,不允许有任何偏差,而其他语言则将其更多地视为准则而不是规则。
该函数模型与众所周知的方法非常相似,例如洋葱架构,六边形架构(也称为端口和适配器架构)以及函数核心,命令式外壳。在所有情况下,核心域(纯业务逻辑)都与基础架构隔离。基础结构代码了解核心域,但并非相反。依赖关系是单向的,I / O保持在边缘。
[图片上传失败...(image-78e868-1644728141682)]
仅将纯代码用于业务逻辑意味着单元测试和集成测试之间存在明显的区别。单元测试是针对核心领域的,它是确定性和快速的,而集成测试则是从头到尾对工作流进行的。
函数式编程的优点之一是,业务域与基础结构的这种隔离是自然发生的。您不需要鼓励开发人员使用洋葱体系结构。作为FP 方法的副作用它会自动发生。
边界和背景
作为软件设计师和架构师,我们的下一个挑战是决定如何将这些工作流或管道分组为逻辑单元。与往常一样,这更多的是艺术而不是科学。
有许多准则可以提供帮助。低耦合和高内聚的经典原理不仅适用于函数代码,而且适用于面向对象的代码。或者,重述通用封闭原则:一起变化的代码应该一起生活。最近,领域驱动设计(DDD)社区非常重视组件边界以及在何处绘制边界。
[图片上传失败...(image-2be33b-1644728141682)]
在DDD术语中,相关函数的分组称为有界上下文,每个有界上下文在其自身权限中都被视为一个微型域。它通常对应于特定业务功能的逻辑封装。我们用“有界上下文”而不是像之前称为“子系统”,是因为它使我们能够专注于设计解决方案时最重要的事情:了解上下文和边界。(如何定义这些有界上下文正是另一篇文章的主题。)
为什么要上下文?因为每个上下文代表一些专业知识或能力。在上下文中,我们共享一种通用语言,并且设计是连贯一致的。但是,就像在现实世界中一样,从上下文中提取信息可能会造成混乱或无法使用。
太宽或太模糊的边界根本就没有边界。
为什么要有边界?在现实世界中,领域可能具有模糊边界。但是在软件领域,我们希望减少子系统之间的耦合,以便它们可以独立发展。边界是确保子系统保持独立的关键,可以使用标准软件实践(例如使用显式API)和避免依赖项(例如共享代码)来维护子系统。在需求不断变化的复杂项目中,我们必须毫不留情地维护有界上下文的“有界”部分。太宽或太模糊的边界根本就没有边界。
自治是有界上下文的关键方面。具有自主权意味着有界上下文可以做出决策,而不必等待来自其他有界上下文的决策或信息。也就是说,如果一个有界上下文不可用,则其他有界上下文可以继续独立运行,这是重要的分离。
自治也可以应用于开发过程。通常,一个有界上下文最好由一个团队拥有。想想三腿比赛:绑在腿上的两个跑步者比自由地独立跑步的两个跑步者慢得多。软件组件也是如此。如果两个团队在相同的受限环境中做出贡献,那么他们可能最终会随着设计的发展而朝着不同的方向拉动设计。
如果将有界上下文的概念应用于函数体系结构,我们最终会遇到许多小型的、集中的域,每个域都支持许多业务工作流。这些边界的定义方式应使其内部的工作流具有自主性,并且能够在不依赖其他系统的情况下完成其工作。
在某些情况下,长期运行的用例或场景需要多个工作流。在这种情况下,处于不同上下文中的工作流将需要使用事件和其他方法相互通信。但是,重要的是将单个工作流保持在一个有界上下文中,并且不要尝试实现方案在多种情况下“端到端”。允许工作流到达多个服务内部最终将导致我们很好解耦的体系结构演变成无法维护的依赖关系的纠结- “ 一个大麻烦” 。”
实体服务反模式
定义边界可能没有正确的方法,但是肯定有许多错误的方法。
定义边界可能没有一种正确的方法,但是肯定有许多错误的方法。分组功能的常见反模式是“ 实体服务”方法,其中围绕实体而不是工作流构建服务。也就是说,有一个“订单”服务,一个“产品”服务,等等。这通常是由于天真地将面向对象的设计直接转移到面向工作流程的体系结构而导致的。此设计的主要问题是,单个业务工作流通常将需要所有这些服务进行协作。如果其中任何一个不可用,则整个工作流程将失败。而且,如果工作流程需要发展,我们可能需要同时触摸和更新许多服务中的代码,从而破坏了“一起改变的代码应该一起生活”的规则。
此外,仅因为业务工作流程涉及实体,例如“订单”并不意味着它与使用该实体的其他工作流程有任何共同点。例如,“支付订单”工作流和“删除订单”工作流都涉及订单,但是具有完全不同的业务逻辑。不需要他们都依赖于将不同函数集合在一起的“订购”服务。当其他要求(例如安全性,可伸缩性等)开始发挥作用时,我们可能会发现必须以非常不同的方式来管理不同的工作流程。耦合它们只会引起痛苦!
事件
现在,我们的工作流函数已分组到有界上下文中,可以使用了。但是什么触发了这些业务工作流?是什么导致员工,用户或自动化流程启动工作流?
是一个事件。也就是说,在外面的世界有新的变化-客户在点击一个按钮,邮件到达时,警报弹出。这是在一个商业活动的形式捕获-例如,“下订单”或“已收到电子邮件。” 在FP架构中,像这样的业务事件会触发工作流。
此外,使用这种方法,工作流的输出也是一个事件:一个通知,通知所有下游工作流世界上发生了重要变化。特定于特定工作流程且未共享的更改(例如数据库更新)不会作为事件从工作流程中发出。
这就是我们可以从这些较小的工作流程中组装较大的流程的方式。每个工作流程都由一个事件触发,并且该工作流程又会生成更多事件供下游流程使用。但是事件如何在工作流之间传递?这取决于项目的特定要求。如果所有工作流程都可以生活在同一个流程中,那么它可以是一个简单的内存队列。但是,如果需要分别和独立地部署工作流,则首选外部队列,服务总线或Kafka风格的事件日志。
请注意,在所有情况下,工作流都是异步交互的。这使他们在时间和空间上保持独立和分离。很少使用在工作流中直接调用另一个工作流的命令和控制方法。
如果您熟悉事件驱动的体系结构,则将很熟悉这种基于事件的方法。而且,确实,具有单独管道的FP方法非常适合于这种体系结构样式。
逻辑架构与物理架构
对事件触发的单独工作流的描述是逻辑视图,而不是物理视图。
到目前为止,设计目标已经围绕业务需求进行了调整,但是我们还需要考虑技术需求。赞成使用工作流作为设计的单元的一个观点是:它们可以在物理上部署在多种方式-例如作为微服务,独立无服务器函数,或单体架构的组件模块化,或甚至作为在基于代理的系统Erlang或Akka的风格。技术实现的选择取决于开发团队的规模和数量,安全性,可伸缩性需求等。
在某些情况下,逻辑工作流也可能在物理上分为单独的部分。例如,工作流可以通过从前端开始同步API 调用,然后继续在后端执行。
那么,应该清楚的是,系统的逻辑和物理组成是不相同的,不应混为一谈。但这并不意味着我们可以避免就架构的物理和实现方面做出决策。单体或无服务器是在项目早期应考虑的重要决定。同样,编程语言的选择和数据库的选择也没有反映在像这样的逻辑模型中,但它们是至关重要的体系结构决策。
前端函数架构
随着SPA的兴起以及完全写在前端的严肃应用程序,前端软件体系结构变得越来越重要。随着FP强调不变性、单向数据流和边缘处的I / O已被证明对于降低复杂性很有价值,用于前端架构的函数性方法也变得越来越流行。
最常见的功能前端体系结构是Model-View-Update体系结构,也称为Elm体系结构。在此设计中,应用程序包含一个不变的模型(代表应用程序状态)和两个关键功能:一个update功能是在发生来自浏览器的消息或事件(例如单击按钮)时更新模型的view功能,以及一个呈现查看该模型(通常只是HTML)。呈现的视图可以将浏览器事件与域模型中定义的消息相关联,以便随后在浏览器中单击按钮(例如,在浏览器中)将触发域消息,从而触发更新功能,该函数最终将呈现新视图,即再次传递给浏览器。以此类推。
函数式编程原理的力量
从后端的微服务和无服务器到前端的MVU,函数式编程的知识对于理解许多现代架构样式至关重要。
软件体系结构的许多良好做法:凝聚力,去耦,I / O的隔离等等,从应用编程函数的原则自然会出现。例如,我们已经看到,在典型的函数设计中,每个工作流都是独立构建的,仅包含其所需的功能(最大限度地提高了凝聚力),并且在各个层次上都强调了自治性,从单个函数到有界上下文(去耦) 。此外,确保可测试性和可维护性的一种确保方法是将业务逻辑保持在纯确定性的函数中,并使用不可变的数据模型来迫使数据更改变得明确。
有许多方法可以进行软件体系结构,并且没有一种千篇一律的方法。但是,使用函数式编程原理的潜在好处是多方面的,我鼓励您进一步研究这些原理-甚至可以将其应用于下一个项目。祝好运!