架构整洁之道-设计原则
4 设计原则
通常来说,要想构建一个好的软件系统,应该从写整洁的代码开始做起。这就是SOLID设计原则所要解决的问题。
SOLID原则的主要作用就是告诉我们如何将数据和函数组织成为类,以及如何将这些类链接起来成为程序。请注意,这里的“类”并不意味着SOLID原则只适用于面向对象编程,这里的类仅仅代表了一种数据和函数的分组,每个软件都会有自己的分类系统,不管它们各自是不是将其称为“类”,事实上都是SOLID原则的适用领域。
一般情况下,我们为软件构建中层结构的主要目标如下:
(1) 使软件可容忍被改动;
(2) 使软件更容易被理解;
(3) 构建可在多个软件系统中复用的组件;
我们这里之所以会使用“中层”这个词,是因为这些设计原则主要适用于那些进行模块级编程的程序员。SOLID原则应该直接紧贴于具体的代码逻辑之上,这些原则是用来帮助我们定义软件架构中的组件和模块的。
SOLID原则是指:
(1) 单一职责原则(Single Responsibility Principle,SRP),每个类都应该只有一个引起它变化的原因,换句话说,一个类应该只有一个职责或功能,当需求改变时,会影响这个类的修改只应与该职责相关;
(2) 开闭原则(Open-Closed Principle,OCP),类模块应该是对扩展开放的(open for extension),对修改关闭的(closed for modification),也就是说,已有的代码在不改动的情况下可以增加新的行为,通过继承和多态来实现新功能的添加而不是修改已有的代码;
(3) 里氏替换原则(Liskov Substitution Principle,LSP),子类型必须能够替换掉它们的基类型,这意味着任何使用基类型的地方,子类型都可以无缝替代,并且不会导致程序的行为出现错误或异常;
(4) 接口隔离原则(Interface Segregation Principle,ISP),客户端不应该被迫依赖它们不需要的方法,接口应该小而具体,每个接口代表一种独立的责任或服务,这样可以使系统更松散耦合且更易复用;
(5) 依赖倒置原则(Dependency Inversion Principle,DIP),高层模块不应该依赖低层模块,二者都应当依赖其抽象,抽象不应依赖细节,细节应当依赖抽象,这个原则强调了依赖于抽象而非具体实现的重要性,通过依赖注入等方式实现,有助于提供代码的可扩展性和可测试性;
4.1 单一职责原则(Single Responsibility Principle)
SRP很容易被误解为“每个模块都应该只做一件事”,而实际上,这只是SRP的一部分,实际上SRP应该被描述成“任何一个软件模块都应该只对某一类行为负责”。这里的软件模块是指一组紧密相关的函数和数据结构。在这里,“相关”这个词实际上就隐含了SRP这一原则。代码与数据就是靠着与某一类行为者的相关性被组合在一起的。
我们来看一些反面案例。
4.1.1 重复的假象
某个工资管理程序中的Employee类有三个函数calculatePay()、reportHours()和save():
如上所述,这个类的三个函数分别对应的是三类非常不同的行为者,违反了SRP设计原则:
(1) calculatePay()函数是由财务部门制定的,他们负责向CFO汇报;
(2) reportHours()函数是由人力资源部门制定并使用的,他们负责向COO汇报;
(3) save()函数是由DBA制定的,他们负责向CTO汇报;
这三个函数被放在同一个源代码文件,即同一个Employee类中,程序员这样做实际上就等于使三类行为者的行为耦合在了一起,这有可能会导致CFO团队的命令影响到COO团队所依赖的功能。例如,calculatePay()函数和reportHours()函数使用同样的逻辑来计算正常工作时数,程序员为了避免重复编码,通常会将该算法单独实现为一个名为regularHours()的函数:
接下来,假设CFO团队需要修改正常工作时数的计算方法,而COO带领的HR团队不需要这个修改,因为他们对数据的用法是不同的,这时候,负责这项修改的程序员会注意到calculatePay()函数调用了regularHours()函数,但可能不会注意到该函数会同时被reportHours()调用。于是,该程序员就这样按照要求进行了修改,同时,CFO团队的成员验证了新算法正常工作,这项修改最终被成功部署上线了。但是,COO团队显然完全不知道这些事情的发生,HR仍然在使用reportHours()产生的报表,随后就会发现他们的数据出错了!
这类问题发生的根源就是因为我们将不同行为者所依赖的代码强凑到了一起,对此,SRP强调这类代码一定要被分开。
4.1.2 代码合并
一个拥有很多函数的源代码文件必然会经历很多次代码合并,该文件中的这些函数分别服务不同行为者的情况就更常见了。
例如,CTO团队的DBA决定要对Employee数据库表结构进行简单修改,与此同时,COO团队的HR需要修改工作时数报表的格式。这样一来,就很可能出现两个来自不同团队的程序员分别对Employee类进行修改的情况。不出意外的话,他们各自的修改一定会互相冲突,这就必须要进行代码合并。
在这个例子中,这次代码合并不仅有可能让CTO和COO要求的功能出错,甚至连CFO原本正常的功能也可能受到影响。
事实上,这样的案例还有很多,它们的一个共同点是,多人为了不同的目的修改了同一份源代码,这很容易造成问题的产生。而避免这种问题产生的方法就是将服务不同行为者的代码进行切分。
4.1.3 解决方案
我们有很多不同的方法可以用来解决上面的问题,每一种方法都需要将相关的函数划分成不同的类。其中最简单直接的办法是将数据与函数分享,设计三个类共同使用一个不包括函数、十分简单的EmployeeData类,每个类只包含与之相关的函数代码,互相不可见,这样就不存在互相依赖的情况了。
这种解决方案的坏处在于:程序员现在需要在程序里处理三个类。
另一种解决办法是使用Facade设计模式:
这样一来,EmployeeFacade类所需要的代码量就很少了,它仅仅包含了初始化和调用三个具体实现类的函数。
当然,也有些程序员更倾向于把最重要的业务逻辑与数据放在一起,那么我们可以选择将最重要的函数保留在Employee类中,同时用这个类来调用其他没那么重要的函数:
总而言之,每一个类都分别容纳了一组作用于相同作用域的函数,而在该作用域之外,它们各自的私有函数是互相不可见的。
单一职责原则主要讨论的是函数和类之间的关系——但是它在两个讨论层面上会以不同的形式出现:在组件层面,我们可以将其称为共同闭包原则(Common Closure Principle),在软件架构层面,它则是用于奠定架构边界的变更轴心(Axis of Change)。
4.2 开闭原则(Open-Closed Principle)
开闭原则(OCP)是Bertrand Meyer在1988年提出的,该设计原则认为:设计良好的计算机软件应该易于扩展,同时抗拒修改。换句话说,一个设计良好的计算机系统应该在不需要修改的前提下就可以轻易被扩展。
OCP是我们进行系统架构设计的主导原则,其主要目标是让系统易于扩展,同时限制其每次被修改所影响的范围。实现方式是通过将系统划分为一系列组件,并且将这些组件间的依赖关系按层次结构进行组织,使得高阶组件不会因低阶组件被修改而受到影响。
4.3 里氏替换原则(Liskov Substitution Principle)
里氏替换原则(LSP)由Barbara Liskov在1987年的“数据抽象和层级结构”研讨会上首次提出,并在后续论文《行为型模式的继承》中进一步明确。这一原则在软件工程领域具有深远影响,对于保证代码的可扩展性和维护性至关重要。
里氏替换原则的核心思想可以总结为:
(1) 子类兼容父类:在一个程序设计中,如果一个对象能够被其基类的实例所替代而不会引起程序的任何错误或异常,则称该子类符合里氏替换原则,即子类应当保持与父类相同的接口约定和行为特征;
(2) 扩展而非修改:子类可以在不破坏原有功能的基础上扩展功能,但不能覆盖或削弱父类原有的功能,这意味着子类可以添加新的方法或者增强已有的功能,但不应该重写或更改父类的方法以导致原有契约失效;
(3) 不变式条件保护:子类必须尊重并维持父类定义的不变式条件(即对状态的约束),且不得引入新的约束条件;
(4) 开放封闭原则的支持:通过遵循LSP,子类能够在不修改原有代码的基础上进行扩展,这是实现开闭原则的关键手段之一;
违反LSP会导致多态性失效、代码难以理解和维护,以及系统稳定性降低等问题。因此,在设计类层次结构时,开发者需要确保子类严格遵循父类的设计契约,避免出现不符合预期的行为变化。
4.4 接口隔离原则(Interface Segregation Principle)
接口隔离原则(ISP)由罗伯特.C.马丁(Robert C.Martin)在《敏捷软件开发:原则、模式与实践》一书中提出,该原则强调了接口设计应具有高度的内聚性,避免创建臃肿的大接口。接口隔离原则的主要内容和指导原则包括:
(1) 客户端特定需求:每个接口应该针对一组相关功能进行设计,而不是提供一个包含所有可能操作的庞大接口,这意味着接口的设计应当基于客户端的需求来划分职责,客户端只需要依赖它们实际使用的接口部分;
(2) 单一职责在接口层面的体现:类似于单一职责原则,ISP要求接口也只承担一种抽象职责,如果一个接口中包含了多个不相关的职责,则应当将其拆分为多个更小、更专注的接口;
(3) 降低耦合度:通过细粒度接口的使用,可以减少类之间的耦合度,一个类不需要实现它不使用的接口方法,这样当接口发生变化时,对依赖此接口的类的影响将被最小化;
(4) 可扩展性和灵活性增强:遵循接口隔离原则可以使系统更加灵活,易于扩展,新功能可以通过新增接口或修改现有接口而不影响其他已有的模块;
(5) 接口用户满意度:理想的接口设计应当使接口使用者仅需了解他们所关心的方法,不必关注无关细节,从而提高用户的满意度和系统的清晰度;
具体到实践中,接口隔离原则鼓励开发者为不同的服务或功能创建独立且专门的接口,并确保每个接口都足够小且具有明确的目的,使得任何实现这些接口的类都能够准确无误地完成所需的任务,而不会因为不必要的方法负担而导致设计复杂性增加。
4.5 依赖反转原则(Dependency Inversion Principle)
依赖反转原则(DIP)主要想告诉我们的是,如果想要设计一个灵活的系统,在源代码层次的依赖关系中就应该多引用抽象类型,而非具体实现。在应用DIP时,我们不必考虑稳定的操作系统或者平台设施,因为这些接口很少会变动。我们主要应关注的是软件系统内部那些会经常变动的(volatile)具体实现模块,这些模块是不停开发的,也就会经常出现变更。
我们每次修改抽象接口时,一定也会去修改对应的具体实现,反之,当我们修改具体实现时,却很少需要去修改相应的抽象接口,因此我们认为接口比实现更稳定。也就是说,如果想要在软件架构设计上追求稳定,就必须多使用稳定的抽象接口,少依赖多变的具体实现。
依赖反转原则可以归结为以下几条具体的编码守则:
(1) 应在代码中多使用抽象接口,尽量避免使用那些多变的具体实现类:这条守则适用于所有的编程语言,无论静态类型语言还是动态类型语言,同时,对象的创建过程也应该受到严格的限制,对此,我们通常选择用抽象工厂这个设计模式;
(2) 不要在具体实现类上创建衍生类;
(3) 不要覆盖(override)包含具体实现的函数:调用包含具体实现的函数通常就意味着引入了源代码级别的依赖,即使覆盖了这些函数,我们也无法消除这其中的依赖——这些函数继承了那些依赖关系,在这里,控制依赖关系的唯一办法,就是创建一个抽象函数,然后再为该函数提供多种具体实现;
(4) 应避免在代码中写入与任何具体实现相关的名字,或者是其他容易变动的事物的名字;
相关文章:

架构整洁之道-设计原则
4 设计原则 通常来说,要想构建一个好的软件系统,应该从写整洁的代码开始做起。这就是SOLID设计原则所要解决的问题。 SOLID原则的主要作用就是告诉我们如何将数据和函数组织成为类,以及如何将这些类链接起来成为程序。请注意,这里…...

数据结构(队列)
一.什么是队列 1.队列定义 队列是一种特殊的线性表,特殊之处在于他只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作。和栈一样,队列也是一种操作受限制的线性表。进行插入操作的一端称为队尾,进行删除操作的…...

docker容器启动后修改或添加端口 nacos容器 版本2.x需要额外开放9848、9849
1.输入docker ps -a查看需要修改的容器ID: 记录下、 docker ps -a 2.停止docker systemctl stop docker 3.进入docker 容器文件夹,找到对应容器的位置: docker的默认文件夹应该是/var/lib/docker 如果不是root用户查看的话,可能会出现权限…...

C语言实现归并排序算法(附带源代码)
归并排序 把数据分为两段,从两段中逐个选最小的元素移入新数据段的末尾。 可从上到下或从下到上进行。 动态效果过程演示: 归并排序(Merge Sort)是一种分治算法,它将一个数组分为两个子数组,分别对这两个…...

考研C语言刷题基础篇之分支循环结构基础(二)
目录 第一题分数求和 第二题:求10 个整数中最大值 第三题:在屏幕上输出9*9乘法口诀表 第四题:写一个代码:打印100~200之间的素数 第五题:求斐波那契数的第N个数 斐波那契数的概念:前两个数相加等于第三…...

Scala基础知识
scala 1、scala简介 scala是运行在JVM上的多范式编程语言,同时支持面向对象和面向函数式编程。 2、scala解释器 要启动scala解释器,只需要以下几步: 按住windows键 r输入scala即可 在scala命令提示窗口中执行:quit,即可退…...

数学建模-------误差来源以及误差分析
绝对误差:精确值-近似值; 举个例子:从A到B,应该有73千米,但是我们近似成了70千米;从C到D,应该是1373千米,我们近似成了1370千米,如果使用绝对误差,结果都是3…...
Arduino和MPLAB X 开发STM32F103和PIC16F15376
要点: 使用Arduino开发STM32F103(Blue Pill),MPLAB X 开发PIC16F15376(Curiosity Nano)C/C嵌入式开发ESP32(Arduino、ESP-IDF)和STM32实时操作系统FreeRTOS STM32使用FreeRTOS示例 在使用 FreeRTOS 时&a…...
手机操作系统Android
▶1.Android系统概述 Andaid(读[安卓)由Coosle公司和开放手机联盟共同开发,它是基于Lmx内核的开源操作系统。Andtoid主要用于移动设备,如智能手机和平板计算机。2008年发布了第一部Andtoid智能手机,以后Android逐渐扩展到平板计算机、电视、…...

2024年,你是否还在迷茫?
2024年,你是否还在迷茫? 别担心!鸿蒙来了,这个未来技术的制高点,为你提供了答案! 诸多大厂疯抢、24年预计鸿蒙相关的岗位需求将达到百万级、就业均薪达到19K,全国高校开课…… 种种现象都在表…...

ART: Automatic multi-step reasoning and tool-use for large language models 导读
ART: Automatic multi-step reasoning and tool-use for large language models 本文介绍了一种名为“自动推理和工具使用(ART)”的新框架,用于解决大型语言模型(LLM)在处理复杂任务时需要手动编写程序的问题。该框架可…...

Github 2024-01-26 开源项目日报Top10
根据Github Trendings的统计,今日(2024-01-26统计)共有10个项目上榜。根据开发语言中项目的数量,汇总情况如下: 开发语言项目数量Python项目4Jupyter Notebook项目2HTML项目1Shell项目1Dockerfile项目1非开发语言项目1Go项目1Rust项目1 高级…...

免费的 UI 设计资源网站 Top 8
今日与大家分享8个优秀的免费 UI 设计资源网站。这些网站的资源包括免费设计材料站、设计工具、字体和其他网站,尤其是一些材料站。它们是免费下载的,材料的风格目前很流行,适合不同的项目。非常适合平面设计WEB/UI设计师收藏,接下…...
人机协同对人工智能治理的影响
人机协同对人工智能治理的影响是多方面的。 首先,人机协同可以提供更有效的人工智能监管和治理机制。人工智能系统通常需要大量的数据来训练和运行,而人类在监管和治理方面有独特的能力。通过人机协同,人们可以利用他们的主观意见和专业知识来…...
Form.List的使用,设置某个字段的值
1.Form.Item的name <Form.Itemname{[base_range, company_base_range_start]}dependencies{[[base_range, company_base_range_end]]}rules{[{ required: true, message: 请输入下限 },{validator: (_, value) >validateMoneyRule(value,base_range?.company_base_range…...
React16源码: React中的updateHostComponent的源码实现
updateHostComponent 1 )概述 在 completeWork 阶段的 HostComponent 处理,继续前文所述在更新的逻辑里面,调用了 updateHostComponent进行前后props对应的dom的attributes变化的对比情况这个方法也是根据不同环境来定义的,我们这…...

uniapp导入uView组件库
目录 准备工作 1. 新建一个项目 2. 导入uview组件库 3. 关于SCSS 配置步骤 1. 引入uView主JS库 2. 在引入uView的全局SCSS 3. 引入uView基础样式 4. 配置easycom组件模式 添加效果实验运行即可成功 准备工作 1. 新建一个项目 2. 导入uview组件库 在进行配置之前&#x…...

防御保护----防火墙的安全策略、NAT策略实验
实验拓扑: 实验要求: 1.生产区在工作时间(9:00-18:00)内可以访问DMZ区,仅可以访问http服务器; 2.办公区全天可以访问DMZ区,其中10.0.2.10可以访问FTP服务器和HTTP服务器…...

# 安徽锐锋科技IDMS系统简介
IDMS 由安徽锐锋科技独立开发 该系统负责和海算以及UE\UNITY的无缝衔接并具备远程数据库访问、高速数据库的自动创建及数据存储、支持MQTT等多种物联网接口,支持多种算法。主要完成由于物料、人员、生产、故障、不良异常、订单异常带来的生产损失,通过海…...

Notepad在文件中查找多行相同内容的文字
Notepad在文件中查找多行相同的内容 查找:打开 Notepad软件, Ctrl F 查找 。输入关键词, 点击【在当前文件中查找】。 复制:直接在下方的【搜索结果】复制。 Notepad提取含有特定字符串的行 详情见: https://blog…...

大话软工笔记—需求分析概述
需求分析,就是要对需求调研收集到的资料信息逐个地进行拆分、研究,从大量的不确定“需求”中确定出哪些需求最终要转换为确定的“功能需求”。 需求分析的作用非常重要,后续设计的依据主要来自于需求分析的成果,包括: 项目的目的…...

从WWDC看苹果产品发展的规律
WWDC 是苹果公司一年一度面向全球开发者的盛会,其主题演讲展现了苹果在产品设计、技术路线、用户体验和生态系统构建上的核心理念与演进脉络。我们借助 ChatGPT Deep Research 工具,对过去十年 WWDC 主题演讲内容进行了系统化分析,形成了这份…...

PPT|230页| 制造集团企业供应链端到端的数字化解决方案:从需求到结算的全链路业务闭环构建
制造业采购供应链管理是企业运营的核心环节,供应链协同管理在供应链上下游企业之间建立紧密的合作关系,通过信息共享、资源整合、业务协同等方式,实现供应链的全面管理和优化,提高供应链的效率和透明度,降低供应链的成…...
FastAPI 教程:从入门到实践
FastAPI 是一个现代、快速(高性能)的 Web 框架,用于构建 API,支持 Python 3.6。它基于标准 Python 类型提示,易于学习且功能强大。以下是一个完整的 FastAPI 入门教程,涵盖从环境搭建到创建并运行一个简单的…...
vue3 定时器-定义全局方法 vue+ts
1.创建ts文件 路径:src/utils/timer.ts 完整代码: import { onUnmounted } from vuetype TimerCallback (...args: any[]) > voidexport function useGlobalTimer() {const timers: Map<number, NodeJS.Timeout> new Map()// 创建定时器con…...
C++八股 —— 单例模式
文章目录 1. 基本概念2. 设计要点3. 实现方式4. 详解懒汉模式 1. 基本概念 线程安全(Thread Safety) 线程安全是指在多线程环境下,某个函数、类或代码片段能够被多个线程同时调用时,仍能保证数据的一致性和逻辑的正确性…...

Qemu arm操作系统开发环境
使用qemu虚拟arm硬件比较合适。 步骤如下: 安装qemu apt install qemu-system安装aarch64-none-elf-gcc 需要手动下载,下载地址:https://developer.arm.com/-/media/Files/downloads/gnu/13.2.rel1/binrel/arm-gnu-toolchain-13.2.rel1-x…...
在树莓派上添加音频输入设备的几种方法
在树莓派上添加音频输入设备可以通过以下步骤完成,具体方法取决于设备类型(如USB麦克风、3.5mm接口麦克风或HDMI音频输入)。以下是详细指南: 1. 连接音频输入设备 USB麦克风/声卡:直接插入树莓派的USB接口。3.5mm麦克…...

spring Security对RBAC及其ABAC的支持使用
RBAC (基于角色的访问控制) RBAC (Role-Based Access Control) 是 Spring Security 中最常用的权限模型,它将权限分配给角色,再将角色分配给用户。 RBAC 核心实现 1. 数据库设计 users roles permissions ------- ------…...

PydanticAI快速入门示例
参考链接:https://ai.pydantic.dev/#why-use-pydanticai 示例代码 from pydantic_ai import Agent from pydantic_ai.models.openai import OpenAIModel from pydantic_ai.providers.openai import OpenAIProvider# 配置使用阿里云通义千问模型 model OpenAIMode…...