一文读懂 SOLID 原则

news2025/1/4 16:12:45

大家好,我是孔令飞,字节跳动云原生开发专家、前腾讯云原生技术专家、云原生实战营 知识星球星主、《企业级 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 srp

type Class struct {
	Teacher *Teacher
	Student *Student
}

type Teacher struct {
	Name  string
	Class int
}

type Student struct {
	Name  string
	Class 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 srp

type Class struct {
	Teacher *Teacher
	Student *Student
}

type Teacher struct {
	Name  string
	Class int
}

type Student struct {
	Name  string
	Class 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() string
	GetPrice() int
}

// NovelBook 小说
type NovelBook struct {
	Name   string
	Price  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 {
	Reader
	Writer
}

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 main

import "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 main

import "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 main

import "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 main

import "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.coloradmin.cn/o/1651565.html

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

相关文章

【快捷部署】024_Hive(3.1.3)

📣【快捷部署系列】024期信息 编号选型版本操作系统部署形式部署模式复检时间024Hive3.1.3Ubuntu 20.04tar包单机2024-05-07 一、快捷部署 #!/bin/bash ################################################################################# # 作者:cx…

【C++题解】1383. 奶牛和草丛

问题:1383. 奶牛和草丛 类型:深度搜索 题目描述: 奶牛Bessie计划好好享受柔软的春季新草。新草分布在 R 行 C 列的牧场里。它想计算一下牧场中的草丛数量。 在牧场地图中,每个草丛要么是单个“#”,要么是有公共边的相…

powershell 注册全局热键——提升效率小工具

powershell 注册全局热键 01 前言 在处理一些重复工作问题的时候,想搞一个小工具,配合全局快捷键来提高效率。因为是Windows系统,想到C#,但是又不想用VS开发,因为那样不够灵活,没办法随时修改随时用&…

构建 WebRTC 一对一信令服务器

构建 WebRTC 一对一信令服务器 构建 WebRTC 一对一信令服务器前言为什么选择 Nodejs?Nodejs 的基本原理浏览器使用 Nodejs安装 Nodejs 和 NPMsocket.io信令服务器搭建信令服务器客户端服务端启动服务器并测试 总结参考 构建 WebRTC 一对一信令服务器 前言 我们在学…

一文读懂三维点云分割

点击下方卡片,关注“小白玩转Python”公众号 什么是点云分割? 点云是世界的一种非结构化三维数据表示,通常由激光雷达传感器、立体相机或深度传感器采集。它由一系列单个点组成,每个点由 x、y 和 z 坐标定义。 自动驾驶模型的点云…

达梦数据库导入数据问题

进行数据导入的时候遇到了导入数据问题 第一个问题: 该工具不能解析此文件,请使用更高版本的工具 这个是因为版本有点低,需要下载最新的达梦数据库 第二个问题: (1)本地编码:PG_GBK, 导入文…

【JavaEE 初阶(三)】多线程代码案例

❣博主主页: 33的博客❣ ▶️文章专栏分类:JavaEE◀️ 🚚我的代码仓库: 33的代码仓库🚚 🫵🫵🫵关注我带你了解更多线程知识 目录 1.前言2.单例模式2.1饿汉方式2.2饿汉方式 3.阻塞队列3.1概念3.2实现 4.定时器4.1概念4.…

js原生写一个小小轮播案例

先上示例&#xff1a; 附上代码 html <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><meta name"viewport" content…

西奥CHT-01软胶囊硬度测试仪:重塑行业标杆,引领硬度测试新纪元

西奥CHT-01软胶囊硬度测试仪&#xff1a;重塑行业标杆&#xff0c;引领硬度测试新纪元 在当今医药领域&#xff0c;软胶囊作为一种广泛应用的药品剂型&#xff0c;其品质的稳定性和安全性直接关系到患者的健康。而在确保软胶囊品质的各项指标中&#xff0c;硬度测试尤为关键。…

Bookends for Mac v15.0.2 文献书籍下载管理

Bookends Mac版可以轻松地将其导入参考 &#xff0c;并直接搜索和进口从数以百计的线上资料来源。Bookends Mac版使用内置在浏览器中下载参考与PDF格式的文件&#xff0c;或和/或网页的点击。 Bookends for Mac v15.0.2注册激活版下载 本文由 mdnice 多平台发布

云密码机的定义与特点

云密码机&#xff0c;作为云计算环境中保障数据安全的关键设备&#xff0c;其重要性不言而喻。它基于虚拟化技术&#xff0c;通过提供高性能的数据加解密、密钥管理等服务&#xff0c;确保云上数据的安全与隐私。下面&#xff0c;安策科技将从云密码机的定义、特点、应用场景以…

JAVA中的线程、死锁、异常

线程 Thread 一、程序 1&#xff0e;一段静态代码&#xff08;静态&#xff09; 二、进程 1&#xff0e;动态的&#xff0c;有开始&#xff0c;有结束&#xff1b;2&#xff0e;程序的一次执行过程&#xff0c;3&#xff0e;操作系统调度分配资源的最小单位&#xff1b; 三、…

图形渲染在AI去衣技术中的奇妙之旅

在这个数字化飞速发展的时代&#xff0c;人工智能&#xff08;AI&#xff09;已经成为了我们生活中不可或缺的一部分。它像一位神秘的魔法师&#xff0c;以其不可思议的力量改变着我们的世界。今天&#xff0c;我要和大家探讨的&#xff0c;是一个颇具争议却技术含金量极高的话…

群晖上部署农场管理系统farmOS

什么是 farmOS &#xff1f; farmOS 是一个基于 Web 的应用程序&#xff0c;用于农场管理、规划和记录保存。它由志愿者社区开发&#xff0c;旨在为农民、开发人员和研究人员提供一个标准平台。 需要注意的是&#xff0c;群晖内核版本太低会遇到下面的错误&#xff0c;这个 AH0…

2024暨南大学校赛热身赛解析

文章目录 A 突发恶疾B Uzi 的真身C 时间管理大师D 基站建设E 在仙境之外weiwandaixu 题目地址 A 突发恶疾 斐波那契数列 fn [0]*1000006fn[0],fn[1] 0,1for i in range(2,1000002):fn[i] (fn[i-1]fn[i-2])%998244353n int(input()) print(fn[n])B Uzi 的真身 分析&#xff…

SSM【Spring SpringMVC Mybatis】——Maven

目录 1、为什么使用Maven 1️⃣获取jar包 2️⃣添加jar包 3️⃣使用Maven便于解决jar包冲突及依赖问题 2、什么是Maven 3、Maven基本使用 3.1 Maven准备 3.2 Maven基本配置 3.3 Maven之Helloworld 4、Maven及Idea的相关应用 4.1 将Maven整合到IDEA中 4.2 在IDEA中新建…

使用脚本一键部署项目的示例(脚本会创建syetemctl的系统服务)

文章目录 说明使用脚本一键部署本项目开启/停止服务开启/关闭开机自动运行更新项目 参考地址&#xff1a;https://github.com/Evil0ctal/Douyin_TikTok_Download_API?tabreadme-ov-file 说明 后续相关项目可以使用这种方式创建脚本&#xff0c;脚本均放置在项目根目录下的bas…

【C++干货基地】揭秘C++STL库的魅力:stiring的初步了解和使用

&#x1f3ac; 鸽芷咕&#xff1a;个人主页 &#x1f525; 个人专栏: 《C干货基地》《粉丝福利》 ⛺️生活的理想&#xff0c;就是为了理想的生活! 引入 哈喽各位铁汁们好啊&#xff0c;我是博主鸽芷咕《C干货基地》是由我的襄阳家乡零食基地有感而发&#xff0c;不知道各位的…

AI大模型探索之路-训练篇17:大语言模型预训练-微调技术之QLoRA

系列篇章&#x1f4a5; AI大模型探索之路-训练篇1&#xff1a;大语言模型微调基础认知 AI大模型探索之路-训练篇2&#xff1a;大语言模型预训练基础认知 AI大模型探索之路-训练篇3&#xff1a;大语言模型全景解读 AI大模型探索之路-训练篇4&#xff1a;大语言模型训练数据集概…

动态规划——路径问题:LCR 166.珠宝的最高价值

文章目录 题目描述算法原理1.状态表示&#xff08;题目经验&#xff09;2.状态转移方程3.初始化4.填表顺序5.返回值 代码实现CJava 题目描述 题目链接&#xff1a;LCR 166.珠宝的最高价值 算法原理 1.状态表示&#xff08;题目经验&#xff09; 对于这种路径类的问题&…