架构整洁之道-设计原则

news2024/9/29 23:34:46

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) 应避免在代码中写入与任何具体实现相关的名字,或者是其他容易变动的事物的名字;

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

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

相关文章

chroot: failed to run command ‘/bin/bash’: No such file or directory

1. 问题描述及原因分析 在busybox的环境下,执行 cd rootfs chroot .报错如下: chroot: failed to run command ‘/bin/bash’: No such file or directory根据报错应该rootfs文件系统中缺少/bin/bash,进入查看确实默认是sh,换成…

vertica10.0.0单点安装_ubuntu18.04

ubuntu的软件包格式为deb,而rpm格式的包归属于红帽子Red Hat。 由于项目一直用的vertica-9.3.1-4.x86_64.RHEL6.rpm,未进行其他版本适配,而官网又下载不到vertica-9.3.1-4.x86_64.deb,尝试通过alian命令将rpm转成deb,但…

【GitHub项目推荐--30 天学会XXX】【转载】

30 天学会 React 这个项目是《30 天 React 挑战》,是在 30 天内学习 React 的分步指南。它需要你学习 React 之前具备 HTML、CSS 和 JavaScript 知识储备。 除了 30 天学会 React,开发者还发布过 30 天学会 JavaScript 等项目。 开源地址:…

解读BEVFormer,新一代CV工作的基石

文章出处 BEVFormer这篇文章很有划时代的意义,改变了许多视觉领域工作的pipeline[2203.17270] BEVFormer: Learning Birds-Eye-View Representation from Multi-Camera Images via Spatiotemporal Transformers (arxiv.org)https://arxiv.org/abs/2203.17270 BEV …

数论Leetcode204. 计数质数、Leetcode858. 镜面反射、Leetcode952. 按公因数计算最大组件大小

Leetcode204. 计数质数 题目 给定整数 n &#xff0c;返回 所有小于非负整数 n 的质数的数量 。 代码 class Solution:def countPrimes(self, n: int) -> int:if n < 2:return 0prime_arr [1 for _ in range(n)]prime_arr[0], prime_arr[1] 0, 0ls list()for i in…

链表--102. 二叉树的层序遍历/medium 理解度C

102. 二叉树的层序遍历 1、题目2、题目分析3、复杂度最优解代码示例4、适用场景 1、题目 给你二叉树的根节点 root &#xff0c;返回其节点值的 层序遍历 。 &#xff08;即逐层地&#xff0c;从左到右访问所有节点&#xff09;。 示例 1&#xff1a; 输入&#xff1a;root […

Django开发_20_form表单前后端关联(2)

根据上一篇文章的代码,进一步了解掌握GET,POST的运行机制 一、实例代码 views.py: def show_reverse(request):if request.method "GET":return redirect(reverse("work4:fill"))if request.method "POST":hobby request.POST.get("h…

Android Studio离线开发环境搭建

Android Studio离线开发环境搭建 1.下载离线和解压包2.创建工程3.创建虚拟机tips 1.下载离线和解压包 下载地址 百度网盘&#xff1a;https://pan.baidu.com/s/1XBPESFOB79EMBqOhFTX7eQ?pwdx2ek 天翼网盘&#xff1a;https://cloud.189.cn/web/share?code6BJZf2uUFJ3a&#…

Apache SeaTunnel 数据集成插件开发最新经验总结!

在Apache SeaTunnel的最新插件开发中&#xff0c;connector-v2 maxcompute 连接器实现了基于CatalogTable SaveMode的新版本。 本文主要给大家分享了源端的关键改动包括弃用了过时的方法&#xff0c;改为通过CatalogTable实现数据传递。汇端则增加了对multi-table sink和save…

HTML+JavaScript-04

JavaScript中的循环 for语句 一个for循环会一直执行&#xff0c;直到循环条件为false for(let i0; i<array.length-1; i){//当遍历完数组后结束循环console.log(array[i] "<br/>");//循环语句 }do...while语句 do...while 语句一直重复直到指定的条件求…

深入理解Redis:如何设置缓存数据的过期时间及其背后的机制

目录 Redis 给缓存数据设置过期时间 Redis是如何判断数据是否过期的呢&#xff1f; 过期的数据的删除策略 Redis 内存淘汰机制 Redis 给缓存数据设置过期时间 一般情况下&#xff0c;我们设置保存的缓存数据的时候都会设置一个过期时间。为什么呢&#xff1f; 因为内存是有…

shell编程-7

shell学习第7天 sed的学习1.sed是什么2.sed有两个空间pattern hold3.sed的语法4. sed里单引号和双引号的区别:5.sed的查找方式6.sed的命令sed的标签用法sed的a命令:追加sed的i命令:根据行号插入sed的c命令:整行替换sed的r命令sed的s命令:替换sed的d命令:删除sed中的&符号 7…

Ubuntu20.04添加桌面启动、侧边栏启动和终端启动

桌面启动 新建XX.desktop文件 在桌面新建一个XX.desktop文件&#xff0c;以QtCreator为例。 &#xff08;注意这里不能使用sudo&#xff0c;因为这样会把文件的权限归为root&#xff0c;导致后续设置可执行程序不方便&#xff09; gedit qtcreator.desktop在XX.desktop文件中…

算子:详细篇

目录 一、执行环境 1.1 创建执行环境 1.2 执行模式 二、源算子 2.1 从集合中读取数据 2.2 从文件读取数据 2.3 从socket读取数据 2.4 从kafka读取数据 三、转换算子 3.1 基本转换算子 &#xff08;1&#xff09;映射(map) &#xff08;2&#xff09;过滤(filter) &#xff08…

数据结构与算法教程,数据结构C语言版教程!(第六部分、数据结构树,树存储结构详解)二

第六部分、数据结构树&#xff0c;树存储结构详解 数据结构的树存储结构&#xff0c;常用于存储逻辑关系为 "一对多" 的数据。 树存储结构中&#xff0c;最常用的还是二叉树&#xff0c;本章就二叉树的存储结构、二叉树的前序、中序、后序以及层次遍历、线索二叉树、…

ASP.NET 7 Core Web 读取appsetting.json

把一些配置信息保存在json文件可以避免更改时要重新发布程序的烦恼。 我这里使用的是写一个类文件&#xff0c;然后通过program.cs启动的方式&#xff08;.net 6 开始没有startup了&#xff09;。 项目类型&#xff1a;ASP.NET Core Web MVC / .NET 7.0 / VS2022 第一步…

架构学习(一):scrapy实现按脚本name与日期生成日志文件

原生scrapy日志机制 一般情况下&#xff0c;我们可以直接在setting文件中定义日志文件&#xff0c;这种会把所有脚本的日志都写在同一个文件 LOG_LEVEL INFO # 日志级别 LOG_STDOUT True # 日志标准输出 LOG_FILE rD:\python\crawler\logs\1163.log # 日志文件路径现在…

少儿编程 2023年12月电子学会图形化编程等级考试Scratch一级真题解析(选择题)

2023年12月scratch编程等级考试一级真题 选择题&#xff08;共25题&#xff0c;每题2分&#xff0c;共50分&#xff09; 1、观察下列每个圆形中的四个数&#xff0c;找出规律&#xff0c;在括号里填上适当的数 A、9 B、17 C、21 D、5 答案&#xff1a;C 考点分析&#xf…

niushop靶场漏洞查找-文件上传漏洞等(超详细)

实战漏洞-niushop 一.端口扫描 http://www.xxx.com/index.php?s/admin/login 这里查询到后面的url有且仅有一个&#xff0c;目测估计是后台 访问url 发现确实是后台 二、找漏洞 Sql注入漏洞1&#xff1a; 点击进去 修改id www.xxx.com/index.php?s/goods/goodslist&…

automa插件使用的一些实战经验3

1 子流程的变量怎么传回父流程 主流程向子流程传参很容易 在子流程可以看到&#xff0c;父流程定义的表格&#xff0c;在子流程中是看不到的&#xff0c;那么子流程定义的变量如何传回父流程呢&#xff1f;另外在子流程再添加执行工作流&#xff0c;是无法选择父流程本身&…