一文读懂 SOLID 原则

大家好,我是孔令飞,字节跳动云原生开发专家、前腾讯云原生技术专家、云原生实战营 知识星球星主、《企业级 Go 项目开发实战》作者。欢迎关注我的公众号【令飞编程】,Go、云原生、AI 领域技术干货不错过。

在 Go 项目开发中,你经常会听到软件开发要遵循 SOLID 原则。另外,在面试过程中,也经常有面试官问到 SOLID 原则。在我的职业生涯中,就遇到过 2 个面试官问我什么是 SOLID 原则。所以,作为开发者,掌握 SOLID 原则及开发方式是一项必备的技能。

那么 SOLID 原则是什么?如何遵循 SOLID 原则呢?本文详细为你解答这些疑问。

SOLID 原则介绍

SOLID 原则是由罗伯特·C·马丁在 21 世纪早期引入,指代了面向对象编程和面向对象设计的五个基本原则。遵循SOLID 原则可以确保我们设计的代码是易维护、易扩展、易阅读的。SOLID 原则同样也适用于 Go 程序设计。具体 SOLID 编码原则见下表:

简写全称中文描述
SRPThe Single Responsibility Principle单一功能原则
OCPThe Open Closed Principle开闭原则
LSPThe Liskov Substitution Principle里氏替换原则
DIPThe Dependency Inversion Principle依赖倒置原则
ISPThe Interface Segregation Principle接口分离原则

Single Responsibility Principle:单一功能原则

**单一功能原则:**一个类或者模块只负责完成一个职责(或功能)。

简单来说就是保证我们在设计函数、方法时做到功能单一,权责明确,当发生改变时,只有一个改变它的原因。如果函数/方法承担的功能过多,就意味着很多功能会相互耦合,这样当其中一个功能发生改变时,可能会影响其它功能。单一功能原则,可以使代码后期的维护成本更低、改动风险更低。

例如,有以下代码,用来创建一个班级,班级包含老师和学生,代码如下:

package srptype Class struct {Teacher *TeacherStudent *Student
}type Teacher struct {Name  stringClass int
}type Student struct {Name  stringClass int
}func createClass(teacherName, studentName string, class int) (*Teacher, *Student) {teacher := &Teacher{Name:  teacherName,Class: class,}student := &Student{Name:  studentName,Class: class,}return teacher, student
}func CreateClass() *Class {teacher, student := createClass("colin", "lily", 1)return &Class{Teacher: teacher,Student: student,}
}

上面的代码段通过 createClass 函数创建了一个老师和学生,老师和学生属于同一个班级。但是现在因为老师资源不够,要求一个老师管理多个班级。这时候,需要修改 createClass 函数的 class 参数,因为创建学生和老师是通过 createClass 函数的 class 参数偶合在一起,所以修改创建老师的代码,势必会影响创建学生的代码,其实,创建学生的代码我们是压根不想改动的。这时候 createClass 函数就不满足单一功能原则。需要修改为满足单一功能原则的代码,修改后代码段如下:

package srptype Class struct {Teacher *TeacherStudent *Student
}type Teacher struct {Name  stringClass int
}type Student struct {Name  stringClass int
}func CreateStudent(name string, class int) *Student {return &Student{Name:  name,Class: class,}
}func CreateTeacher(name string, classes []int) *Teacher {return &Teacher{Name:  name,Class: classes,}
}func CreateClass() *Class {teacher := CreateTeacher("colin", []int{1, 2})student := CreateStudent("lily", 1)return &Class{Teacher: teacher,Student: student,}
}

上述代码,我们将 createClass 函数拆分成 2 个函数 CreateStudent CreateTeacher,分别用来创建学生和老师,各司其职,代码互不影响。

Open / Closed Principle:开闭原则

**开闭原则:**软件实体应该对扩展开放、对修改关闭。

简单来说就是通过在已有代码基础上扩展代码,而非修改代码的方式来完成新功能的添加。开闭原则,并不是说完全杜绝修改,而是尽可能不修改或者以最小的代码修改代价来完成新功能的添加。

以下是一个满足开闭原则的代码段:

type IBook interface {GetName() stringGetPrice() int
}// NovelBook 小说
type NovelBook struct {Name   stringPrice  int
}func (n *NovelBook) GetName() string {return n.Name
}func (n *NovelBook) GetPrice() int {return n.Price
}

上述代码段,定义了一个 Book 接口和 Book 接口的一个实现:NovelBook(小说)。现在有新的需求,对所有小说打折统一打 5 折,根据开闭原则,打折相关的功能应该利用扩展实现,而不是在原有代码上修改,所以,新增一个 OffNovelBook 接口,继承 NovelBook,并重写 GetPrice 方法。

type OffNovelBook struct {NovelBook
}// 重写GetPrice方法
func (n *OffNovelBook) GetPrice() int {return n.NovelBook.GetPrice() / 5
}

Liskov Substitution Principle:里氏替换原则

**里氏替换原则:**如果 S 是 T 的子类型,则类型 T 的对象可以替换为类型 S 的对象,而不会破坏程序。

简单来说,里氏替换原则要求子类(派生类)能够替换父类(基类)并且不影响程序的行为。也就是说,子类应该继承父类的所有属性和行为,并且可以在不改变程序逻辑的情况下进行扩展。在 Go 开发中,里氏替换原则可以通过接口来实现。

例如,以下是一个符合里氏替换原则的代码段:

type Reader interface {Read(p []byte) (n int, err error)
}type Writer interface {Write(p []byte) (n int, err error)
}type ReadWriter interface {ReaderWriter
}func Write(w Writer, p []byte) (int, error) {return w.Write(p)
}

我们可以将 Write 函数中的 Writer 参数替换为其子类型 ReadWriter,而不影响已有程序:

func Write(rw ReadWriter, p []byte) (int, error) {return rw.Write(p)
}

Dependency Inversion Principle:依赖倒置原则

**依赖倒置原则:**依赖于抽象而不是一个实例,其本质是要面向接口编程,不要面向实现编程。

以下是一个不符合依赖倒置原则的示例:

package mainimport "fmt"// 定义一个高层模块
type HighLevelModule struct {lowLevelModule LowLevelModule
}func (hlm HighLevelModule) DoSomething() {hlm.lowLevelModule.DoSomething()
}// 定义一个低层模块
type LowLevelModule struct{}func (llm LowLevelModule) DoSomething() {fmt.Println("Doing something in low level module...")
}func main() {llm := LowLevelModule{}hlm := HighLevelModule{lowLevelModule: llm}hlm.DoSomething()
}

在上面的示例中,HighLevelModule 依赖于 LowLevelModule,而且在 HighLevelModule 中直接实例化了 LowLevelModule。这不符合依赖倒置原则的原因是高层模块应该依赖于抽象而不是具体的实现,而且高层模块不应该直接依赖于低层模块的具体实现。

为了符合依赖倒置原则,我们可以通过将 LowLevelModule 抽象成接口,并在 HighLevelModule 中依赖于该接口,从而实现依赖倒置。以下是优化后的示例:

package mainimport "fmt"// 定义一个低层模块接口
type LowLevelModule interface {DoSomething()
}// 定义一个高层模块
type HighLevelModule struct {lowLevelModule LowLevelModule
}func (hlm HighLevelModule) DoSomething() {hlm.lowLevelModule.DoSomething()
}// 实现低层模块
type ConcreteLowLevelModule struct{}func (cllm ConcreteLowLevelModule) DoSomething() {fmt.Println("Doing something in low level module...")
}func main() {cllm := ConcreteLowLevelModule{}hlm := HighLevelModule{lowLevelModule: cllm}hlm.DoSomething()
}

在优化后的示例中,我们定义了 LowLevelModule 接口来抽象低层模块,并在 HighLevelModule 中依赖于该接口。同时,我们实现了 ConcreteLowLevelModule 结构体来实现 LowLevelModule 接口。这样就符合了依赖倒置原则,高层模块依赖于抽象接口,而不是具体的实现,降低了模块之间的耦合度。

Interface Segregation Principle:接口隔离原则

**接口隔离原则:**是指客户端不应该依赖它不需要的接口,即一个类对另一个类的依赖应该建立在最小的接口上。具体来说,接口隔离原则要求程序员尽量将臃肿庞大的接口拆分成更小的和更具体的接口,让接口中只包含客户感兴趣的方法。

以下是一个不符合接口隔离原则的示例:

package mainimport "fmt"// 定义一个接口
type Machine interface {Print()Scan()
}// 实现接口
type MultiFunctionMachine struct{}func (mfm MultiFunctionMachine) Print() {fmt.Println("Printing...")
}func (mfm MultiFunctionMachine) Scan() {fmt.Println("Scanning...")
}func main() {mfm := MultiFunctionMachine{}mfm.Print()mfm.Scan()
}

在上面的示例中,我们定义了一个 Machine 接口,包含 Print() Scan() 两个方法。然后我们实现了一个 MultiFunctionMachine 结构体来实现这个接口。这个示例不符合接口隔离原则的原因是, MultiFunctionMachine 结构体实现了一个包含打印和扫描功能的接口,但是在实际使用中,可能某些设备只需要其中的一个功能,而不需要同时实现接口中的所有方法。

为了符合接口隔离原则,我们可以将 Machine 接口拆分为两个单一职责的接口,分别表示打印和扫描功能。然后根据需要实现对应的接口。以下是优化后的示例:

package mainimport "fmt"// 定义打印机接口
type Printer interface {Print()
}// 定义扫描仪接口
type Scanner interface {Scan()
}// 实现打印机
type SimplePrinter struct{}func (sp SimplePrinter) Print() {fmt.Println("Printing...")
}// 实现扫描仪
type SimpleScanner struct{}func (ss SimpleScanner) Scan() {fmt.Println("Scanning...")
}func main() {sp := SimplePrinter{}sp.Print()ss := SimpleScanner{}ss.Scan()
}

在优化后的示例中,我们将 Machine 接口拆分为 PrinterScanner 两个单一职责的接口,分别表示打印和扫描功能。然后我们分别实现了 SimplePrinterSimpleScanner 结构体来实现这两个接口,每个结构体只实现了对应的功能。这样就遵循了接口隔离原则,将接口按照单一职责进行拆分,避免了一个类需要实现不需要的方法。


  • 您的支持是我写作的最大动力!如果这篇文章对您有帮助,感谢点赞和关注;
  • 欢迎扫码加入 孔令飞的云原生实战营,带你进阶 Go + 云原生高级开发工程师。
  • 在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

北邮22级信通院DSP:实验三(1):FFT变换、IFFT变换(附每步8点变换蝶形图)保姆级讲解+用C++程序实现复数域的FFT变换和IFFT变换+C++中的chrono头文件讲解

北邮22信通一枚~ 跟随课程进度更新北邮信通院DSP的笔记、代码和文章,欢迎关注~ 获取更多文章,请访问专栏: 北邮22级信通院DSP_青山入墨雨如画的博客-CSDN博客 目录 一、预备知识 1.1 FFT算法 1.2.1由DFT到FFT 1.2.2 基2时域抽选算法 …

Spring+Vue的卓越托管中心管理系统的设计与实现+PPT+论文+讲解+售后

相比于以前的传统手工管理方式,智能化的管理方式可以大幅降低运营人员成本,实现了卓越托管中心管理系统的标准化、制度化、程序化的管理,有效地防止了卓越托管中心管理系统的随意管理,提高了信息的处理速度和精确度,能…

【LAMMPS学习】八、基础知识(5.11)磁自旋

8. 基础知识 此部分描述了如何使用 LAMMPS 为用户和开发人员执行各种任务。术语表页面还列出了 MD 术语,以及相应 LAMMPS 手册页的链接。 LAMMPS 源代码分发的 examples 目录中包含的示例输入脚本以及示例脚本页面上突出显示的示例输入脚本还展示了如何设置和运行各…

最新版Ceph( Reef版本)块存储简单对接k8s

当前ceph 你的ceph集群上执行 1.创建名为k8s-rbd 的存储池 ceph osd pool create k8s-rbd 64 642.初始化 rbd pool init k8s-rbd3 创建k8s访问块设备的认证用户 ceph auth get-or-create client.kubernetes mon profile rbd osd profile rbd poolk8s-rbd部署 ceph-rbd-csi c…

Nginx配置Https缺少SSL模块

1、Linux下Nginx配置https nginx下载和安装此处就忽略,可自行百度 1.1、配置https 打开nginx配置文件 vim /opt/app/nginx/conf/nginx.conf相关https配置 server {listen 443 ssl; #开放端口server_name echarts.net;#域名#redirect to https#ssl on; #旧版#ssl证…

c#实现音乐的“vip播放功能”

文章目录 前言1. c#窗体2. 功能3. 具体实现3.1 添加文件3.2 音乐播放3.3 其他功能 4. 整体代码和窗口5. 依赖的第三方库 前言 最近在QQ音乐里重温周杰伦的歌,觉得好听到耳朵怀孕,兴起想要下载下来反复听,发现QQ音乐VIP歌曲下载下来的格式居然…

微信小程序 手机号授权登录

手机号授权登录 效果展示 这里面用的是 uni-app 官方的登录 他支持多端发布 https://zh.uniapp.dcloud.io/api/plugins/login.html#loginhttps://zh.uniapp.dcloud.io/api/plugins/login.html#login 下面是代码 <template><!-- 授权按钮 --><button v-if&quo…

1984. 学生分数的最小差值C++

给你一个 下标从 0 开始 的整数数组 nums &#xff0c;其中 nums[i] 表示第 i 名学生的分数。另给你一个整数 k 。 从数组中选出任意 k 名学生的分数&#xff0c;使这 k 个分数间 最高分 和 最低分 的 差值 达到 最小化 。 返回可能的 最小差值 。 示例 1&#xff1a; 输入&…

硬件设计细节1-缓冲驱动器使用注意事项

目录 一、缓冲驱动器二、实例分析1.硬件结构2.问题描述3.原因分析4.原因定位 三、结论 一、缓冲驱动器 缓冲驱动器通常用于隔离、电平转换等应用场景。在使用时&#xff0c;需要关注的点较多&#xff0c;如电平范围、频率范围、延时、控制方式、方向以及输入输出状态。通常&am…

Git === Git概述 Git安装

第1章 Git概述 Git是一个免费的、开源的分布式版本控制系统&#xff0c;可以快速高效地处理从小型到大型的各种项目。 Git易于学习&#xff0c;占地面积小&#xff0c;性能极快。 它具有廉价的本地库&#xff0c;方便的暂存区域和多个工作流分支等特性。其性能优于Subversion…

配置好Maven后本地仓库不自动加载以及创建Maven报错的解决方法

先退出到全局模式再点击All Settings 点击maven 修改Maven home path修改了这个local不同步 点击Flie中的Module 然后选择maven 点击next设置需要存放的位置以及组名 然后报错 报错误的原因是因为maven版本太高和与你使用的IDEA版本不兼容 将maven的版本改为3.6并修改环境变量 …

【C++】从零开始认识多态

送给大家一句话&#xff1a; 一个犹豫不决的灵魂&#xff0c;奋起抗击无穷的忧患&#xff0c;而内心又矛盾重重&#xff0c;真实生活就是如此。 ​​​​ – 詹姆斯・乔伊斯 《尤利西斯》 _φ(*&#xffe3;ω&#xffe3;)&#xff89;_φ(*&#xffe3;ω&#xffe3;)&…

ReactFlow的ReactFlow实例事件传参undefined处理状态切换

1.问题 ReactFlow的ReactFlow实例有些事件我们在不同的状态下并不需要&#xff0c;而且有时候传参会出现其它渲染效果&#xff0c;比如只读状态下我们不想要拖拉拽onEdgesChange连线重连或删除的功能。 2.思路 事件名称类型默认值onEdgesChange(changes: EdgeChange[]) >…

21物联1班常用网络扫描

网络扫描 1.网络扫描概述2.网络扫描步骤及分类具体步骤 1.网络扫描概述 网络安全扫描技术是一种基于Internet远程检测目标网络或本地主机安全性脆弱点的技术。通过网络安全扫描&#xff0c;系统管理员能够发现所维护的Web服务器的各种TCP/IP端口的分配、开放的服务、Web服务软件…

Unity 性能优化之UI和模型优化(九)

提示&#xff1a;仅供参考&#xff0c;有误之处&#xff0c;麻烦大佬指出&#xff0c;不胜感激&#xff01; 文章目录 前言一、选择UI二、UGUI的优化1.Raycast Target2.UI控件的重叠3.TextMeshPro 二、模型优化1.Model选项卡Mesh CompressionRead/Write Enabled设置Optimize Ga…

【JVM】内存结构

内存结构 Java 虚拟机定义了若干种程序运行期间会使用到的运行时数据区&#xff0c;其中有一些会随着虚拟机启动而创建&#xff0c;随着虚拟机退出而销毁。另外一些则是与线程一一对应的&#xff0c;这些与线程一一对应的数据区域会随着线程开始和结束而创建和销毁。 线程私有…

21物联1班常用网络命令

常用网络命令 ipconfig&#xff08;配置&#xff09;ping(测试)命令1&#xff1a;ping 172.16.0.12&#xff1a;ping ip -t3&#xff1a;ping ip -l 3000&#xff08;注意每个之间都存在空格&#xff09;4&#xff1a;ping ip -n count netstat&#xff08;网络&#xff09;命令…

技术分享-上海泗博MPI转以太网模块MPI-131实现Node-RED直接访问西门子PLC数据

上海泗博自动化MPI-131是一款用于西门子S7系列PLC&#xff08;包括S7-200、S7-300、S7-400&#xff09;以及西门子数控机床&#xff08;如840D、840DSL等&#xff09;的以太网通讯模块&#xff0c;无需编程&#xff0c;即插即用&#xff0c;支持通过模块上下载PLC程序和数据监控…

【busybox记录】【shell指令】shuf

目录 内容来源&#xff1a; 【GUN】【shuf】指令介绍 【busybox】【shuf】指令介绍 【linux】【shuf】指令介绍 使用示例&#xff1a; 打乱内容 - 默认输出 打乱内容 - 最多输出n行 打乱内容 - 将输出写入文件 打乱内容 - 重复输出 打乱内容 - 打乱本条指令的参数 打…

Django调用MTP服务器给指定邮箱发送邮件

Django调用MTP服务器发送邮箱 邮箱的激活链接含有用户数据不能直接发送需要对其进行加密 发送邮箱是借助SMTP服务器进行中转 一. 配置SMTP服务中的邮箱信息以及激活链接 1. 配置邮箱权限 打开网易邮箱设置点击POP3 开启选项 注 : 在打开的过程中会弹出授权密码一点要保存 …