如何设计一个复杂的业务系统

news2024/11/17 11:27:08

一、设计要干啥

作为一个企业级应用架构,自然会把专注点转移到业务应用功能性设计本身上来。现在来说对于一个复杂业务架构进行设计,我们要想做到又快又好,无非是两种情况:一是架构师本身对业务理解很深、能力超强、炉火纯青;二是原有的业务系统本身模型清晰,足够的“高内聚低耦合”,可以快速在其基础之上分析业务变化形成新的业务架构设计。

为什么我们要在代码实现前做设计?软件架构设计的实质是通过核心问题的分离降低复杂度,并让系统能够更快地响应外界业务的变化,并且使得系统能够持续演进。在遇到变化时不需要从头开始,保证实现成本得到有效控制。

从架构设计角度,以下三点是最为关键的:

  • 让我们的模型、组件和业务划分尽量靠近变化的本质,比如对于一般电商系统来说,就是用户、商品、交易、支付等,这样的划分能够让我们将变化“隔离”在一定的范围(业务模块)内,从而帮助我们有效减少改变点。

  • 设计上,业务模型内部是高内聚,模型之间是低耦合,即各自完成的业务是相对独立的,不会因为一方掉线而牵连另外一方,比如商品推荐功能挂掉了,但是交易和支付业务应该继续正常提供服务,可能提示用户暂时无法提供推荐服务,或者干脆降级为兜底策略。

  • 模型、组件在业务上尽可能是复用的,正是这样的复用才成就了今天的互联网级架构,我们不会每做一个电商系统都从零做起。而被“复用”最多的业务模块显然会重点设计和运营,成为核心业务模块。当然架构上这样的电商系统必然也会比较健壮。

二、设计怎么做

业务软件开发的常见病:从一个小的项目不断开发演化变成一个大型业务系统,但随着新需求的不断增加,最终演变成了开发团队的噩梦。而这些噩梦大部分是源于软件的概念完整性(“概念完整性”一词来源于软件工程的经典著作《人月神话》)遭到了破坏。这些业务代码可能是一代又一代的开发人员各行其道的堆叠起来的(我们又称之为“屎山”),而这个过程中没人有意识的去维护软件的概念完整性。而 DDD 领域设计,特别是 DDD 提供的战略建模层面的概念,是维护软件概念完整性的良药。

“技术服务于业务、业务驱动技术”是目前大部分人的共识,尤其是对商业公司而言。而 DDD 领域设计主张在软件设计中把业务领域本身作为关注的焦点(换句话说就是软件开发人员要懂业务)非常符合这种思想;并且,DDD 提供了切实可行的面对复杂业务软件设计的解决方法,这也是我非常提倡作为一个架构师去深入学习和讨论 DDD 领域设计的相关知识。

2.1战略建模

在战略层面,DDD 非常强调对业务问题的分析和分解,通过识别核心问题来降低问题的复杂度。DDD 在战略层面维护模型的概念完整性的方法,最重要的两个概念就是界限上下文(Bounded Context)和防腐层(Anti-Corruption Layer)。

  • 定义好界限上下文

关于界限上下文的定义,随便一本讲 DDD 的书上都会详细讲解,这里我只想分享一下自己的一些理解。这时,有人会问:界限上下文多大才能合适呢,划分上下文有没有可以遵循的规则呢?

划分上下文的规则,无非就是放之四海而皆准的“高内聚、低耦合”,这么说可能还是太虚。其实真正让大家感到纠结的是,不知如何切分的那些东西之间所存在的关联,有的甚至干脆都纳入到一个上下文里。其实,我认为与其关注上下文的“大小”,不如关注模型的“质量”,关注概念的完整性是不是容易被破坏。我觉得,判断大小是不是合适,要结合应用开发团队的能力,看开发团队能在多大的一个范围内掌控软件的概念完整性。只要是开发团队没有问题,这个范围就算再大也都是可以的。

如果开发团队的水平在业界属于上游,那么维护上下文的范围往往是很大的;一些公司开发团队的水平参差不齐,所以在项目的实施过程中,可能需要划分相对小的上下文,尽可能减少“屎山”的不断堆积。 

  • 做好防腐层

界限上下文需要时刻保护好自己所维护的边界,以及边界内概念的完整性,这时需要将某个上下文的概念转化为另一个上下文概念的地方就叫做“防腐层”。防腐层的实现有很多种,典型的比如作为适配器 Adaptor 来实现,另外广义上讲,Gateway 也是一个典型性的防腐层组件,当然,防腐层的代码和其他内部业务模型之间要存在明显的物理边界(当然不一定说要把防腐层作为一个个独立部署的进程),至少我们可以考虑把防腐层作为一个独立的类库来进行构建和维护,阿里内部的比如星环、其实就是这个思路。

2.2 战术建模

DDD 在战术上最核心的概念就是实体和聚合,为了更好的理解什么是聚合、聚合根、聚合内部实体,下面举例说明一下。想象一下一个电商系统的订单相关的模型,我们可能会得到订单 Order、订单头 OrderHeader、订单行项 OrderItem 三个相互关联的概念:

  • 一个叫做 Order 的聚合。

  • 这个订单聚合的聚合根是一个叫做 OrderHeader 的实体,实体 OrderHeader 的 ID 叫做 OrderId(订单号)。

  • 通过 OrderHeader 实体,我们可以访问 OrderItem 实体的一个聚合。OrderItem 这个实体的局部 ID 叫做 ProductId(产品 ID)。因为业务善变不允许在同一个订单的不同订单项内出现同一个产品,所以我们可以选择产品 ID 作为订单项的局部 ID。

“聚合是数据修改的单元”,基于这个原则,我们可以做到“聚合内强一致、聚合外最终一致”,比如,我们可以不能接受一个订单内的所有订单项的金额之和不等于订单头的总金额,我们就必须把订单头和订单行项这两个实体划分到同一个聚合内。

  • 设计聚合的原则

我们不妨先看一下《实现领域驱动设计》一书中对聚合设计原则的描述,原文是有点不太好理解的,我来稍微解释一下:

  1. 在一致性边界内建模真正的不变条件。聚合用来封装真正的不变性,而不是简单地将对象组合在一起。聚合内有一套不变的业务规则,各实体和值对象按照统一的业务规则运行,实现对象数据的一致性,边界之外的任何东西都与该聚合无关,这就是聚合能实现业务高内聚的原因。

  2. 尽量设计小的聚合。如果聚合设计得过大,聚合会因为包含过多的实体,导致实体之间的管理过于复杂,高频操作时会出现并发冲突或者数据库锁,最终导致系统可用性变差。而小聚合设计则可以降低由于业务过大导致聚合重构的可能性,让领域模型更能适应业务的变化。

  3. 通过唯一标识引用其它聚合。聚合之间是通过关联外部聚合根 ID 的方式引用,而不是直接对象引用的方式。外部聚合的对象放在聚合边界内管理,容易导致聚合的边界不清晰,也会增加聚合之间的耦合度。

  4. 在边界之外使用最终一致性。聚合内数据强一致性,而聚合之间数据最终一致性。在一次事务中,最多只能更改一个聚合的状态。如果一次业务操作涉及多个聚合状态的更改,应采用领域事件的方式异步修改相关的聚合,实现聚合之间的解耦(相关内容我会在领域事件部分详解)。

  5. 通过应用层实现跨聚合的服务调用。为实现微服务内聚合之间的解耦,以及未来以聚合为单位的微服务组合和拆分,应避免跨聚合的领域服务调用和跨聚合的数据库表关联。

上面的这些原则是 DDD 的一些通用的设计原则,还是那句话:“适合自己的才是最好的。”在系统设计过程时,你一定要考虑项目的具体情况,如果面临使用的便利性、高性能要求、技术能力缺失和全局事务管理等影响因素,这些原则也并不是不能突破的,总之一切以解决实际问题为出发点。

  • 设计聚合的步骤

DDD 领域建模通常采用类似事件风暴,一般通过用例分析、场景分析和用户旅程分析等方法,通过头脑风暴列出所有可能的业务行为和事件,然后找出产生这些行为的领域对象,并梳理领域对象之间的关系,找出聚合根,找出与聚合根业务紧密关联的实体和值对象,再将聚合根、实体和值对象组合,构建聚合。

  • 第一步:采用用例分析或事件风暴等方法,根据业务行为,梳理出在过程中发生这些行为的所有的实体和值对象,比如投保单、标的、客户、被保人等等。

  • 第二步:从众多实体中选出适合作为对象管理者的根实体,也就是聚合根。判断一个实体是否是聚合根,上一章也说过,你可以结合以下场景分析:是否有独立的生命周期?是否有全局唯一 ID?是否可以创建或修改其它对象?是否有专门的模块来管这个实体。图中的聚合根分别是投保单和客户实体。

  • 第三步:根据上一章说的设计聚合的原则,找出与聚合根关联的所有紧密依赖的实体和值对象。构建出一个包含一个聚合根、多个实体和值对象的对象集合,这个集合就是聚合。在图中我们构建了客户和投保这两个聚合。

  • 第四步:在聚合内根据聚合根、实体和值对象的依赖关系,画出对象的引用和依赖模型。这里需要说明一下:投保人和被保人的数据,是通过关联客户 ID 从客户聚合中获取的,在投保聚合里它们是投保单的值对象,这些值对象的数据是客户的冗余数据,即使未来客户聚合的数据发生了变更,也不会影响投保单的值对象数据。从图中我们还可以看出实体之间的引用关系,比如在投保聚合里投保单聚合根引用了报价单实体,报价单实体则引用了报价规则子实体。

  • 第五步:多个聚合根据业务语义和上下文一起划分到同一个限界上下文内。

  那以上就是一个聚合诞生的完整过程了。

三、非功能需求

云原生在技术上能够最大程度的解决众多非功能性质量和技术需求

在云原生时代,我们需要将弹性作为首要考虑的因素,纳入建模的考量。那么弹性边界,就是我们划分系统的重要依据。而且,我们还需要考虑弹性边界间的依赖关系,尽量避免弹性耦合。对于业务建模来说,为了配合云原生时代的架构,我觉得要做到如下几点:

  • 确立一种模型结构能够反映弹性边界,而这时候需要考虑不同弹性边界的原则来划分界限上下文;如果两个上下文明显具有不同的弹性诉求,那就应该拆分。而如果具有一致的弹性诉求,可以考虑先不拆。那这个时候拆分微服务到底能多“微”呢?简单说就是“微”到能够更好的利用弹性来控制成本的大小。

  • 从异步模型的视角,去优化业务逻辑;典型就是 MQ 消息队列系统,由于有 broker,所以生产者和消费者不必在同一时间都保持可用性以及相同的吞吐量,而且生产者也不需要马上等到回复。

  • 位置的松耦合:典型就是服务注册中心,消费端完全不需要直接知道提供端的具体位置,而都通过注册中心来查找服务来访问。

  • 在弹性边界切分业务上下文时,同一个弹性边界内部维护业务强一致性。

  • 在异步调用产生中间态异常时,需要维护业务最终一致性。

四、SOA、微服务、中台

多年前,这些传统的大型 ERP 业务软件,其实都是在一个很大的范围内维护业务概念的完整性。一个 ERP 安装完毕后,数据库有七八百张表(也就是七八百个实体)处于同一个界限上下文之内。但是这些 ERP 在这样一个巨大的界限上下文内仍然很好的维护了业务概念的完整性,实在令人敬佩。

然而实现它非常困难,但是破坏它却非常容易。一套 ERP 定制项目实施下来,数据库里可能又多了几百张表,更不用说不规范的命名看起来千奇百怪。这些厂商的 ERP 实施顾问和开发人员,夜以继日的维护这个庞大的“屎山”。我们不能让这些庞大的“单体应用”无限制的增长,于是我们又一次祭起了“分而治之”的大旗。想 SOA 这样的软件组件化技术给我们提供了拆分的工具。我们把一个大的界限上下文按照利于拆分成几个相对来说小一些的界限上下文;在物理上,我们把一个大的单体应用拆分成若干服务。

一般来说,我们会让服务的物理边界和界限上下文的领域边界基本堆砌,一个界限上下文对一个或多个可以独立部署的服务应用,服务应用包含了界限上下文的核心业务逻辑的实现。SOA 的服务组件的物理边界给服务间的调用增加了一些困难,这就使得开饭人员简化对象之间的关系,编写更加“高内聚、低耦合”的代码。当服务组件不多的时候,构建防腐层的工作量也不会很大,我们只要处理好组件之间的代码即成就好了。

但是,我们的架构师和开发人员太喜欢“分而治之”了,微服务的广泛使用甚至说是滥用,让我们看很多微服务真的是很“微”,几乎是一个 DDD 的聚合就可以对应一个可以独立部署的微服务。这样的微服务单单靠本身做不了太多的业务,这就需要更多更多的微服务“聚合”起来一起才能对外提供业务服务。

当然,微服务技术基础设施的发展也为服务之间的调用提供了更多的便利,跨越微服务的边界成为了常态;这个时候,业务开发人员区分“同一个上下文内的服务调用”和“上下文之间的防腐层”就要时刻保持头脑清醒,这时候的界限上下文和微服务的物理边界往往很难对齐,这就必然增加了维护每个界限上下文概念完整性的难度。

既然维护一个个“微小”的独立的界限上下文概念完整性越来愈难,那么我们干脆将它们再聚合起来吧?将它们融合到一个大小适度的界限上下文,那这就是所谓的企业级业务架构,也就是我们现在说的业务中台,最终目的可以说想要获得“企业级”的大和谐。

所以在一定程度上讲,软件工程就是妥协的艺术,是“中庸之道”。我们要不要中台,要大的中台,不管企业的大小,都应该结合自身的业务目标以及拥有的资源,在“维护更大范围的概念完整性”和“维护更多的防腐层代码”之间做出平衡,那这也是一个企业级架构师所要做的最核心的事情之一。

五、结语

做软件架构的其实很羡慕做建筑架构的,因为建筑架构有严谨的力学基础作为基座,有很多可以精确计算的东西,而软件架构却没有多少可以精确计算出来的成分,所以,前面说的“不断的妥协”不失是一种可行的设计思路和设计艺术;其实这也应验了那句“没有银弹”。

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

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

相关文章

QT QMdiArea控件 使用详解

本文详细的介绍了QMdiArea控件的各种操作,例如:新建界面、源代码、添加界面、移除一个子窗口、设置活动子窗口、子窗口级联排列、子窗口平铺排列、关闭当前子窗口、关闭当前子窗口、返回当前子窗口、返回当前子窗口、返回子窗口列表、信号槽、单击信号、…

使用python-docx对doc文档修改页眉时,遇到的一点小问题

之前在百度和google搜到的也修改页眉的方式,代码如下 import docx # 打开 Word 文档 doc docx.Document(sample.docx) # 遍历每个节 for section in doc.sections: # 获取节的页眉 header section.header # 获取页眉中的段落 p header.paragraphs[0] # 替换段落…

冒泡排序、选择排序、插入排序、希尔排序

冒泡排序 基本思想 代码实现 # 冒泡排序 def bubble_sort(arr):length len(arr) - 1for i in range(length):flag Truefor j in range(length - i):if arr[j] > arr[j 1]:temp arr[j]arr[j] arr[j 1]arr[j 1] tempflag Falseprint(f第{i 1}趟的排序结果为&#…

基于51单片机+DS1302时钟模块+4位数码管显示

一、DS1302时钟模块简介 二、绘制Proteus 仿真电路图 三、编写51单片机代码 #include "DS1302.h"// 位定义 sbit DS1302_DATA P3^3; sbit SCLK P3^2; sbit RST P3^1;// 向DS1302写一个字节 void DS1302_Write_Byte(unsigned char addrOrData) {unsigned char i;f…

RocketMQ的架构及概念

RocketMQ就是一个消息中间键用于实现异步传输与解耦 那什么是消息中间键呢? 消息中间件利用高效可靠的消息传递机制进行平台无关的数据交流,并基于数据通信来进行分布式系统的集成。通过提供消息传递和消息排队模型,它可以在分布式环境下扩展…

CSP 201312-1 出现次数最多的数

答题 用两个map&#xff0c;一个map记录每个数出现的次数并降序排序&#xff0c;另一个map将次数作为键&#xff0c;数本身作为值&#xff0c;降序排序&#xff0c;搞定 #include<iostream> #include<map> using namespace std; int main(){map<int,int,great…

arm栈推导

按照栈生长方向分&#xff1a;可以分为递增栈&#xff08;向高地址生长&#xff09;&#xff1b;递减栈&#xff08;向低地址生长&#xff09; 按照sp执行位置来分&#xff1a;满栈&#xff08;sp指向栈顶元素的位置&#xff09;&#xff1b;空栈&#xff08;sp指向即将入栈的…

ChatGPT 和 Elasticsearch:APM 工具、性能和成本分析

作者&#xff1a;LUCA WINTERGERST 在本博客中&#xff0c;我们将测试一个使用 OpenAI 的 Python 应用程序并分析其性能以及运行该应用程序的成本。 使用从应用程序收集的数据&#xff0c;我们还将展示如何将 LLMs 成到你的应用程序中。 在之前的博客文章中&#xff0c;我们构建…

SpringBoot+Vue 整合websocket实现简单聊天窗口

效果图 1 输入临时名字充当账号使用 2 进入聊天窗口 3 发送消息 &#xff08;复制一个页面&#xff0c;输入其他名字&#xff0c;方便展示效果&#xff09; 4 其他窗口效果 代码实现 后端SpringBoot项目&#xff0c;自行创建 pom依赖 <dependency><groupId…

docker安装xxl-job连接数据库时显示无法连接问题

背景&#xff1a; 在项目中需要定时任务调度&#xff0c;需要在docker容器中安装xxl-job 遇到的问题 部署成功后&#xff0c;可以访问xxl-job登录界面&#xff0c;点登录没反应&#xff0c;但过一段时间就弹出数据库拒绝连接&#xff0c;说MyBatis连接用户失败 原因&#xf…

华为云API图像识别Image的趣味性—AI识别迈克尔·杰克逊

云服务、API、SDK&#xff0c;调试&#xff0c;查看&#xff0c;我都行 阅读短文您可以学习到&#xff1a;人工智能AI图像识别的图像识别、名人识别 1 IntelliJ IDEA 之API插件介绍 API插件支持 VS Code IDE、IntelliJ IDEA等平台、以及华为云自研 CodeArts IDE&#xff0c;基…

深度学习算法

深度学习算法 1. 各种网络框架及其联系1.1 两阶段与一阶段区别1.1.1 detectron算法框架套路&#xff1a;1.1.2 multi-stage1.1.3 two-stage 算法1.1.4 one-stage 算法 2. 常用算法2.1 SS(选择性搜索算法&#xff0c;Selective Search) 3. 神经元模型4. 神经网络分类4.1 前馈神经…

Linux内核分析与应用5-中断

本系列是对 陈莉君 老师 Linux 内核分析与应用[1] 的学习与记录。讲的非常之好&#xff0c;推荐观看 留此记录&#xff0c;蜻蜓点水,可作抛砖引玉 中断机制概述 中断是CPU对系统发生的某个事件作出的一种反应, 当中断发生时,CPU暂停正在执行的程序,保留现场后,自动转去执行相应…

一本快速入门Java的书

关于这本书 很高兴&#xff0c;我又一本书籍《Java编程动手学》上市了。记得早在2017年&#xff0c;在我跟人邮出版社的傅道坤编辑合作完《Tomcat内核设计剖析》这本书后&#xff0c;傅编就问我考不考虑写一本面向Java初学者的图书&#xff0c;当时我谢绝了傅编的邀请。一来是我…

总结986

时间记录&#xff1a; 7:10起床 8:00~下午2:00课程设计&#xff0c;偷学了3小时 2:17~3:55午觉 4:10~5:30计网 5:35~6:41数据结构 7:00~7:22继续数据结构课后习题重做 7:23~8:07考研政治&#xff0c;做题20道纠错 8:15~8:39每日长难句 8:39~10:21 14年tex2纠错标记 1…

Unity下如何实现RTMP或RTSP播放端录像?

好多开发者问我们&#xff0c;Unity环境下&#xff0c;除了RTSP或RTMP的播放&#xff0c;如果有录像诉求&#xff0c;怎么实现&#xff1f;实际上录像相对播放来说&#xff0c;更简单一些&#xff0c;因为不涉及到绘制&#xff0c;只要拉流下来数据&#xff0c;直接写mp4文件就…

pytorch代码实现之SAConv卷积

SAConv卷积 SAConv卷积模块是一种精度更高、速度更快的“即插即用”卷积&#xff0c;目前很多方法被提出用于降低模型冗余、加速模型推理速度&#xff0c;然而这些方法往往关注于消除不重要的滤波器或构建高效计算单元&#xff0c;反而忽略了特征内部的模式冗余。 原文地址&am…

BUUCTF Reverse/[羊城杯 2020]login(python程序)

查看信息,python文件 动调了一下&#xff0c;该程序创建了一个线程来读入数据&#xff0c;而这个线程的代码应该是放在内存中直接执行的&#xff0c;本地看不到代码&#xff0c;很蛋疼 查了下可以用PyInstaller Extractor工具来解包&#xff0c;可以参考这个Python解包及反编译…

华为云云服务器云耀L实例评测 | 在华为云耀L实例上搭建电商店铺管理系统:一次场景体验

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全专栏》 &#x1f995; 文章图文…

sqli第一关

1.在下使用火狐访问sqlilabs靶场并使用burpsuite代理火狐。左为sqlilabs第一关&#xff0c;右为burpsuite。 2.输入?id1 and 11 与?id1 and 12试试 可以看出没有变化哈&#xff0c;明显我们输入的语句被过滤了。在?id1后面尝试各种字符&#xff0c;发现单引号 包…