【DDD】学习笔记-代码模型的架构决策

news2024/11/13 1:21:34

代码模型属于软件架构的一部分,它是设计模型的进化与实现,体现出了代码模块(包)的结构层次。在架构视图中,代码模型甚至会作为其中的一个视图,通过它来展现模块的划分,并定义运行时实体与执行视图建立联系,如下图所示:

enter image description here

确定软件系统的代码模型属于架构决策的一部分。在领域驱动设计背景下,代码模型的设计可以分为两个层次,具体如下。

  • 系统层次:设计整个软件系统的代码模型。
  • 限界上下文层次:设计限界上下文内部的代码模型。

在领域驱动设计中,限界上下文对整个系统进行了划分,以便于实现“分而治之”的架构设计思想。正如前面几课所述,我们可以将每个限界上下文视为一个自治单元,这个自治单元就像一个独立的子系统,可以有自己的架构。尤其是当我们将一个限界上下文视为一个微服务时,这种控制在边界内的独立架构就显得更加明显。上一课介绍的代码模型,其实是这样的模型设计层次。

针对整个软件系统,我们可以将这些限界上下文视为一个黑盒子。我们仅需关心限界上下文暴露的接口以及它们之间的协作关系。而对于整个软件系统,则需要保证其在架构风格上的一致性。所谓“风格”,可以参考 Roy Fielding 的定义:

风格是一种用来对架构进行分类和定义它们的公共特征的机制。每一种风格都为组件的交互提供了一种抽象,并且通过忽略架构中其余部分的偶然性细节,来捕获一种交互模式(pattern of interation)的本质特征。

Roy Fielding 对“风格”的定义突出了对架构的分类和公共特征的定义。无论是分类,还是识别公共特征,都是一种抽象。同时,定义中明确指出风格为组件的协作提供了抽象,这说明架构风格并不涉及实现细节。在架构设计时,需要找出那些稳定不变的本质特征,且这个特征与系统的目标还有需求是相匹配的。结合领域驱动设计,限界上下文以及上下文映射就是这样的一种抽象:

  • 如果我们将限界上下文视为微服务,则该系统的架构风格就是微服务架构风格
  • 如果我们将上下文协作模式抽象为发布/订阅事件,则该系统的架构风格就是事件驱动架构风格
  • 如果在限界上下文层面将查询与命令分为两种不同的实现模式,则该系统的架构风格就是命令查询职责分离(CQRS)架构风格

显然,这些架构风格适应于不同的应用场景,即这些风格的选择应与系统要解决的问题域相关。为了保证整个软件系统架构设计的一致性,我们可以结合 Simon Brown 提出的 C4 模型来考虑设计元素的粒度和层次:

enter image description here

自上而下,Simon Brown 将整个软件系统分为了四个层次,分别为系统上下文(System Context)、容器(Containers)、组件(Components)以及类(Classes),这些层次的说明如下所示。

  • 系统上下文:是最高的抽象层次,代表了能够提供价值的东西。一个系统由多个独立的容器构成。
  • 容器:是指一个在其内部可以执行组件或驻留数据的东西。作为整个系统的一部分,容器通常是可执行文件,但未必是各自独立的进程。从容器的角度理解一个软件系统的关键在于,任何容器间的通信可能都需要一个远程接口。
  • 组件:可以想象成一个或多个类组成的逻辑群组。组件通常由多个类在更高层次的约束下组合而成。
  • 类:在一个面向对象的世界里,类是软件系统的最小结构单元。

在系统上下文层次,我们需要考虑的架构因素是将系统作为一个完整单元,然后思考它和用户之间的交互方式是怎样的,需要集成的外部系统有哪些,采用怎样的通信协议?若在这个层次考虑架构风格,就更容易建立架构的抽象体系。例如:

  • 倘若我们采用微服务的架构风格,意味着包括用户和外部系统在内的客户端都需要通过 API Gateway 实现微服务的调用;
  • 倘若采用事件驱动架构风格,意味着系统与外部系统之间需要引入消息中间件,以便于事件消息的传递;
  • 倘若采用 CQRS 架构风格,意味着系统暴露的 API 接口需要分解为命令接口和查询接口,接口类型不同,处理模式和执行方式都不相同。

C4 模型中的容器基本等同于微服务的概念,推而广之也就代表了限界上下文的概念。其实,容器与组件之间的边界很模糊,这取决于我们对限界上下文之间通信机制的决策。不仅限于此,即使采用了微服务架构风格,我们识别出来的限界上下文亦未必一定要部署为一个微服务。它们可能为整个系统提供公共的基础功能,因而在微服务架构中实际是以公共组件的形式而存在的。

在代码模型上,这些公共组件又分为两种。

一种公共组件具有业务价值,因而对应于一个限界上下文,可以视为是支撑子域(Supporting SubDomain)或通用子域(Generic SubDomain)在解决方案上的体现,如规则引擎、消息验证、分析算法等。那么,在微服务架构风格中,为何不将这样的限界上下文部署为微服务呢?这实际上是基于微服务的优势与不足来做出的设计决策。

enter image description here

如上图所示,微服务保证了技术选择的自由、发布节奏的自由、独立升级的自由以及自由扩展硬件配置资源的自由。为了获得这些自由,付出的代价自然也不少,其中就包括分布式系统固有的复杂度、数据的一致性问题以及在部署和运维时带来的挑战。除此之外,我们还需要考虑微服务协作时带来的网络传输成本。如果我们能结合具体的业务场景考虑这些优势与不足,就可以在微服务与公共组件之间做出设计权衡。

基于我个人的经验,我认为当满足以下条件时,应优先考虑设计为微服务:

  • 实现经常变更,导致功能需要频繁升级;
  • 采用了完全不一样的技术栈;
  • 对高并发与低延迟敏感,需要单独进行水平扩展;
  • 是一种端对端的垂直业务体现(即需要与外部环境或资源协作)。

当满足以下条件时,应优先考虑设计为公共组件:

  • 需求几乎不会变化,提供了内聚而又稳定的实现;
  • 在与其进行协作时,需要传输大量的数据;
  • 无需访问外部资源。

如果找不到支持微服务的绝对理由,我们应优先考虑将其设计为公共组件。

另一种公共组件并非领域的一部分,仅仅提供了公共的基础设施功能,如文件读写、FTP 传输、Telnet 通信等。本质上,这些公共组件属于基础设施层的模块。如果是多个限界上下文都需要重用的公共模块,甚至可以将该公共组件视为一种基础的平台或框架。这时,它不再属于限界上下文中的代码模型,而是作为整个系统的代码模型。当然,倘若我们将这种公共组件视为基础平台或框架,还可以为其建立单独的模块(或项目),放在专有的代码库中,并以二进制依赖的形式被当前软件系统所使用。

整体而言,一个典型的微服务架构通常如下图所示:

enter image description here

采用微服务架构风格时,诸如 Spring Cloud 之类的微服务框架事实上间接地帮我们完成了整个系统架构的代码模型。例如:

  • API Gateway 作为所有微服务的访问入口,Euraka 或 Consul 提供的服务注册与发现,帮我们解决了微服务协作的访问功能;
  • Feign 提供的声明式服务调用组件让我们省去了编写防腐层中 Client 的代码实现;
  • Spring Cloud Config 提供了分布式的配置中心等。

这些组件在上面所示的架构中,作为微服务架构的基础设施而存在。当我们使用这样的微服务框架时,就可以让设计与开发人员只需要专注于微服务内部的设计,即领域逻辑的实现,实际上就是对软件复杂度的应对。通过限界上下文(微服务)实现对业务的分而治之,从而降低业务复杂度;而微服务架构自身虽然会带来技术复杂度的增加,但技术复杂度已经转移到了微服务框架来完成,从而整体降低了应用开发人员的开发难度。

倘若采用单体架构,我们也需保证其向微服务演化的可能。因此,这两种风格的选择对于限界上下文内部的代码模型并无影响。但我们还需要为整个系统建立一个一致的系统架构。为了保证关注点分离,整个系统的架构同样需要进行分层,并遵循整洁架构的思想。

在对整个系统架构进行分层架构设计时,需要考虑用户展现层、应用层和基础设施层与限界上下文内各层次之间的关系。我认为,限界上下文的范围包含了除用户展现层在外的其他各层。其中,应用层包含了应用服务,由它来完成领域对象的调用以及对其他横切关注点的调用。基础设施层的北向网关提供了对外公开的开放主机服务,通常被定义为 RESTful 服务。那么,对于整个系统架构而言,还需要定义系统层次的应用服务与 RESTful 服务么?如下图所示:

enter image description here

如果我们参考微服务架构风格,就会发现上图的控制器层暴露了所有的服务接口,相当于 API Gateway 的功能,上图的应用层用于管理各个限界上下文应用服务的协作,相当于服务注册与发现组件的功能。微服务框架提供的 API Gateway 和服务注册与发现组件仅仅是一个外观(Facade),内部并没有包含任何应用逻辑和领域逻辑。而在单体架构中,由于所有的限界上下文都部署在一个服务中,因而并不需要服务的注册与发现功能;每个限界上下文都有控制器定义了对外公开的 RESTful 服务,且所有的这些 RESTful 服务都绑定到唯一的入口(如 Spring Boot 要求定义的 Application 类)上,区别仅仅是代码模型的隔离罢了,自然也就不需要 API Gateway。故而在系统架构层次,保留二者并没有任何意义。

当然,也有例外。譬如说我们需要为整个单体架构提供一些与业务无关的 RESTful 服务,如健康检查、监控等。另外,倘若需要组合多个限界上下文的领域模型,似乎也有了保留应用层的必要。Vernon 在《实现领域驱动设计》一书中就提到了这种“组合多个限界上下文”的场景,如下图所示:

enter image description here

Vernon 认为:

……应用层不是成了一个拥有内建防腐层的新领域模型吗?是的,它是一个新的、廉价的限界上下文。在该上下文中,应用服务对多个 DTO 进行合并,产生的结果有点像贫血领域模型。

实际上,Vernon 在这里谈到的组合功能,目的是为了组装一个满足客户端调用的服务对象。但我认为定义这样专属的应用服务并非必须。归根结底,这个应用服务要做的事情就是对多个限界上下文领域模型的协调与组装。这种需求必然要结合具体的业务场景,例如订单对象需要组合来自不同限界上下文的商品信息、客户信息、店铺信息等。该业务场景虽然牵涉到多个限界上下文领域模型的协调,但必然存在一个主领域对应的限界上下文。这个限界上下文提供的应用服务才是该业务场景需要实现的业务价值,因此就应该将这个应用服务定义在当前限界上下文,而非整个系统架构的应用层,又或者为其建立一个新的廉价的限界上下文。而在该限界上下文内部,应用层或领域层可以通过防腐层与其他限界上下文协作,共同为这个业务提供支持。除非,这个业务场景要完成的业务目标不属于之前识别的任何一个限界上下文。

再来考虑用户展现层的场景。假设需要支持多种前端应用,且不同前端应用需要不同的视图模型和交互逻辑。考虑到前端资源有限,同时保证前端代码的业务无关性,我们可以在系统架构层面上,定义一个统一的接口层。这个接口层位于服务端,提供了与前端界面对应的服务端组件,并成为组成用户界面的一部分。在这个接口层中,我们可以为不同的前端提供不同的服务端组件。由于引入的这一接口层具有后端服务的特征,却又为前端提供服务,因而被称之为 BFF(Backends For Frontends,为前端提供的后端)

引入的 BFF 往往是为了协调前端开发人员与后端开发人员的沟通协作问题。前端开发人员理解用户界面的设计,后端开发人员却只为垂直领域(即特性)设计服务接口,就使得二者并不能很好地实现模型之间的匹配。既然 BFF 是为前端 UI 提供的,最佳的做法就是让前端开发人员自己来定义。这也是在项目实践中 Node.js 扮演的重要作用,如下图所示:

enter image description here

图中为浏览器 UI 调用提供的 UI Layer,即 BFF,它实则是在服务器与浏览器之间增加了一个 Node.js 中间层。各层的职责如下表所示:

JavaNode.jsJS + HTML + CSS
服务层跑在服务器上的 JS跑在浏览器上的 JS
提供数据接口转发数据,串接服务CSS、JS 加载与运行
维持数据稳定路由设计,控制逻辑DOM 操作
封装业务逻辑渲染页面,体验优化共用模板、路由

显然,BFF 的引入虽然是架构决策的一部分,但严格意义上讲,它并不属于后端架构。因而,BFF 的设计并不在领域驱动战略设计的考虑范围之内。

最后再来考虑基础设施层。除了限界上下文自身需要的基础设施之外,在系统架构层面仍然可能需要为这些限界上下文提供公共的基础设施组件,例如对 Excel 或 CSV 文件的导入导出,消息的发布与订阅、Telnet 通信等。这些组件往往是通用的,许多限界上下文都会使用它们,因而应该放在系统的基础设施层而被限界上下文重用,又或者定义为完全独立的与第三方框架同等级别的公共组件。理想状态下,这些公共组件的调用应由属于限界上下文自己的基础设施实现调用。倘若它们被限界上下文的领域对象或应用服务直接调用(即绕开自身的基础设施层),则应该遵循整洁架构思想,在系统架构层引入 interfaces 包,为这些具体实现定义抽象接口。

在运用领域驱动设计时,还需要提供遵照领域驱动设计尤其是战术设计要素提供的基本构造块(Building Block),例如对 Identity 的实现、值对象、实体以及领域事件的抽象、聚合根的构造型等。你可以理解为这些构造块组成了支持领域驱动设计的框架。如果没有单独剥离出这个框架,这些构造块也将作为系统代码模型的一部分。

综上所述,我们选择的架构风格会影响到系统的代码模型。假设我们要设计的系统为 ecommerce,选择单体架构风格,则系统架构与限界上下文的代码模型如下所示:

practiceddd
    - ecommerce
        - core
            - Identity
            - ValueObject
            - Entity
            - DomainEvent
            - AggregateRoot
        - controllers
            - HealthController
            - MonitorController
        - application(视具体情况而定)
        - interfaces
            - io
            - telnet
            - message
        - gateways
            - io
            - telnet
            - message
        - ordercontext
            - application
            - interfaces
            - domain
            - repositories
            - gateways
        - productcontext
            - application
            - interfaces
            - domain

如果选择微服务架构风格,通常不需要建立一个大一统的代码模型,而是按照内聚的职责将这些职责分别封装到各自的限界上下文中,又或者定义为公共组件以二进制依赖的方式被微服务调用。这些公共组件应该各自构建为单独的包,保证重用和变化的粒度。如果选择 CQRS 架构风格,就可以在限界上下文的代码模型中为 command 和 query 分别建立 module(领域驱动设计中的设计要素),使得它们的代码模型可以独自演化,毕竟命令和查询的领域模型是完全不同的。基于质量因素的考虑,我们甚至可以为同一个领域的 command 和 query 各自建立专有的限界上下文。在 command 上下文中,除了增加了 command 类和 event 类以及对应的 handler 之外,遵循前面讲述的限界上下文代码模型,而 query 上下文的领域模型就可以简化,例如直接运用事务脚本或表模块模式。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1429830.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Leetcode—203. 移除链表元素【简单】

2024每日刷题(一零九) Leetcode—203. 移除链表元素 实现代码 /*** Definition for singly-linked list.* struct ListNode {* int val;* ListNode *next;* ListNode() : val(0), next(nullptr) {}* ListNode(int x) : val(x), next(n…

ffmpeg 时间裁剪之-ss -t与滤镜中trim=start=*:duration=*的区别和联系

背景 工作中遇到的呗。记下来贡着。 滤镜重置时间戳:setptsPTS-STARTPTS 在FFmpeg中,setptsPTS-STARTPTS是一种用于调整视频时间戳(PTS)的滤镜表达式。这个表达式通常用于视频编辑和处理过程中,用于修改视频的时间轴…

H5 加密(MD5 Base64 sha1)

1. 说明 很多的时候是避免不了注册登录这一关的,但是一般的注册是没有任何的难度的,无非就是一些简单的获取用户输入的数据,然后进行简单的校验以后调用接口,将数据发送到后端,完成一个简单的注册的流程,那…

代码随想录 Leetcode93. 复原 IP 地址

题目&#xff1a; 代码(首刷看解析 2024年2月3日&#xff09;&#xff1a; class Solution { public:vector<string> res;bool Enligible(string& s, int left, int right) {if (left > right) return false;if (s[left] 0 && left ! right) return fal…

类银河恶魔城学习记录1-1 Player状态机的搭建 P28

对状态机的介绍 什么是状态机&#xff1f;一篇文章就够了 - 知乎 说实话&#xff0c;目前并不能深入理解状态机的奇妙之处&#xff08;当然&#xff0c;我觉得状态机作为教程的重要组成部分是不得不理解的&#xff0c;所以以下我会对游戏教程内的状态机做一些我认为的解释&am…

通过 ChatGPT 的 Function Call 查询数据库

用 Function Calling 的方式实现手机流量包智能客服的例子。 def get_sql_completion(messages, model"gpt-3.5-turbo"):response client.chat.completions.create(modelmodel,messagesmessages,temperature0,tools[{ # 摘自 OpenAI 官方示例 https://github.com/…

element-ui link 组件源码分享

link 组件的 api 涉及的内容不是很多&#xff0c;源码部分的内容也相对较简单&#xff0c;下面从以下这三个方面来讲解&#xff1a; 一、组件结构 1.1 组件结构如下图&#xff1a; 二、组件属性 2.1 组件主要有 type、underline、disabled、href、icon 这些属性&#xff0c;…

云服务器安全组、防火墙、端口问题,结合telnet解决项目部署无法访问

无论是运维还是后台亲自操刀在云服务器上部署项目&#xff0c;往往会遇到项目部署上去了&#xff0c;也确定项目正常运行&#xff0c;但还是没法访问的问题。 如果没有经验的小伙伴&#xff0c;很容易陷入疑惑的状态&#xff0c;无从下手解决。 其实这涉及到云平台安全组、服…

centos7安装oracle

1 安装虚拟机 设置4G内存&#xff0c;硬盘40G 2 配置网络环境 2.1配置主机名 # vi /etc/hostname 修改为 oracle2.2 配置IP地址 # vi /etc/sysconfig/network-scripts/ifcfg-ens33 修改 BOOTPROTO"static" ONBOOT"yes" IPADDR192.168.109.110 NETMAS…

如何过滤离线logcat日志文件?

1.需求&#xff1a; How did Android Studio Logcat to read the files which have save in logcat? I saved some logs and would like to open them with Android Studio - Logcat interface and be able to see the colours and apply some filters just as if the pho…

【PostgreSQL内核学习(二十五) —— (DBMS存储空间管理)】

DBMS存储空间管理 概述块&#xff08;或页面&#xff09;PageHeaderData 结构体HeapTupleHeaderData 结构 表空间表空间的作用&#xff1a;表空间和数据库关系表空间执行案例 补充 —— 模式&#xff08;Schema&#xff09; 声明&#xff1a;本文的部分内容参考了他人的文章。在…

小华和小为的聚餐地点 - 华为OD统一考试

OD统一考试&#xff08;C卷&#xff09; 分值&#xff1a; 200分 题解&#xff1a; Java / Python / C 题目描述 小华和小为是很要好的朋友&#xff0c;他们约定周末一起吃饭。 通过手机交流&#xff0c;他们在地图上选择了多个聚餐地点(由于自然地形等原因&#xff0c;部分聚…

支付宝直连商户处理支付交易投诉管理,支持多商户

大家好&#xff0c;我是小悟 1、问题背景 玩过支付宝生态的&#xff0c;或许就有这种感受&#xff0c;如果收到投诉单&#xff0c;不会通知到手机端&#xff0c;只会在支付宝商家后台-账号中心-安全中心-消费者投诉-支付交易投诉那里显示。那你能一直盯着电脑看吗&#xff1f;…

多维时序 | Matlab实现CNN-RVM卷积神经网络结合相关向量机多变量时间序列预测

多维时序 | Matlab实现CNN-RVM卷积神经网络结合相关向量机多变量时间序列预测 目录 多维时序 | Matlab实现CNN-RVM卷积神经网络结合相关向量机多变量时间序列预测效果一览基本介绍程序设计参考资料 效果一览 基本介绍 Matlab实现CNN-RVM卷积神经网络结合相关向量机多变量时间序…

梳理cuda算子编译与python调用的流程_以vllm为例

最近在使用vllm框架,部署大语言模型的时候。发现吞吐量提升比较明显。对里面用到的技术比较感兴趣。后来发现vllm使用了一些新的技术,如kv cache,page attention等。其中很多是用cuda编写加速的。并且对cuda算子如何应用到python服务中比较感兴趣,现在就自己的了解,用文章…

如何进行嵌入式系统的软件性能监测和优化

嵌入式系统的软件性能监测和优化是确保系统能够高效运行和性能稳定的关键步骤。嵌入式系统通常包含有限的计算资源和受限的能源消耗&#xff0c;因此对其进行有效的性能监测和优化是至关重要的。本文将介绍嵌入式系统软件性能监测和优化的基本概念、常见方法和技术&#xff0c;…

SQL报错注入

SQL注入报错注入 报错注入原理:报错注入是通过特殊函数错误使用并使其输出错误结果来获取信息的在遇到有报错回显的时候&#xff0c;但是没有数据回显的情况下可以利用报错注入的函数: 1.floor():向下取整 2.extractvalue(): 对XML文档进行查询的函数&#xff0c;当参数的格式…

代码随想录算法训练营第42天 | 01背包问题,你该了解这些! 01背包问题,你该了解这些! 滚动数组 416. 分割等和子集

目录 01背包问题&#xff0c;你该了解这些&#xff01; 01 背包 二维dp数组01背包 &#x1f4bb;实现代码 01背包问题&#xff0c;你该了解这些&#xff01; 滚动数组 一维dp数组&#xff08;滚动数组&#xff09; &#x1f4bb;实现代码 416. 分割等和子集 &#x1f…

位运算之妙用:识别独特数字(寻找单身狗)

目录 找单身狗1 图解&#xff1a; 代码如下&#xff1a; 找单身狗2 图解&#xff1a; 代码如下&#xff1a; 寻找单身狗1 从数组中 的1 2 3 4 5 1 2 3 4 中找出没有另一个相同的数与其匹配的数 这个问题的原理是利用异或运算的性质。异或运算&#xff08;XOR&#xff09…

游戏如何选择服务器

一个网络游戏要想长期运行下去&#xff0c;关键是用户体验&#xff0c;在初期阶段的游戏服务器租用环节就显得尤为重要。那么问题来了&#xff0c;游戏公司如何才能够在众多的服务器商中租用找到高性能、高性价比的游戏服务器租用呢&#xff1f;租用游戏服务器时需要考虑的因素…