SOLID 设计原则 (有点长但很透彻)

article/2025/9/22 23:55:28

面向对象设计原则 SOLID 应该是职业程序员必须掌握的基本原则,每个程序员都应该了然于胸,遵守这 5个原则可以帮助我们写出易维护、易拓展的高内聚低耦合的代码。

它是由罗伯特·C·马丁(知名的 Rob 大叔)21世纪初期 (准确来说,2000年在他的论文Design Principles and
Design Patterns中提到过后 4 个原则,2003 年在它的The Principles of OOD文章中提出了 5 大原则)引入的概念,虽然这 5 个原则不都是 Rob 大叔原创的,但是是他首次把这 5 个原则组合并推广起来的,并很快得到业界认可和推崇。

据我粗略统计,身边写了很多年代码的人,大多数对 SOLID 的认识只停留在表面,甚至讲不清楚其概念。本文通过讲述概念和代码示例让大家全面了解这 5 大原则,然后再通过讨论他们之间的联系以及终极目标,让大家从宏观上领略其含义,以便能在日常开发中使用。

SOLID 概览

图1

备注:关于 SOLID 每一条原则的描述,我见到过不同的版本,有细微差别,但是传递的信息都是一样的。下面贴一个 Rob 大叔在《The Principles of OOD》的中的描述:
图2

单一职责

一个类应该仅具有一种单一功能,或者说有且仅有一个原因使类变更。
这个原则最容易理解,但也是最容易被违反的原则。我是做移动端开发的,所经历过的项目,大多数类会逐渐变成多功能类,就像下图中的多功能瑞士军刀。
图3
这个概念容易理解,但是不好把握。单一职责到底要“单一”到什么程度?这是有商量余地的,甚至没有一个标准答案。

如果把“车”定义为一个类,从人类工具这个角度去看,它足够单一了,能明确和其它工具区别开来。如果从交通工具这个角度来看,它还是太笼统,不够单一,我们需要把它拆分成不同的类,比如“拉人的车”、“拉货物的车”。还可以继续细分为“自动驾驶的轿车”和“非自动驾驶的轿车”等等。

通常,我们会根据以下几个角度进行分类:

  • 用途:例如工具类,处理不同种类事务的函数放在不同的工具类中;
  • 变化频率:处理数据的类变化频率低,而负责用户交互或展示的类变化频率较高;
  • 业务类别:例如登录业务和注册业务要分开;
  • 设计模式中的分层:例如 MVC, MVVM, VIPER 等。

但是具体要分的多细,我们得根据实际情况,项目在不同阶段不同规模下,“单一”的颗粒度是实时变化的,在动态中寻求一个平衡。就像厨师在学炒菜时,是如何掌握“盐少许”的。他会不断地品尝,直到味道刚好为止。写代码也一样,你需要识别需求变化的信号,不断“品尝”你的代码,当“味道”不够好时,持续重构,直到“味道”刚刚好。

代码示例:

class Square {var side: Floatinit(side: Float) {self.side = side}func calculateArea() -> Float {return side * side}func calculatePerimeter() -> Float {return side * 4}func draw() {// render an square image}func rotate(degree: Float) {// rotate the square image to the degree and re-render}
}

这是一个“正方形”类,仔细看的话我们会发现一些坏味道。calculateArea()calculatePerimeter()是计算面积和周长的,属于数据处理范畴,并且我们知道这两个函数基本是不会变化的。而draw()rotate(degree: Float)是展示相关的操作,可能会根据不同屏幕分辨率进行调整,所以应该被剥离开。那么我们可以根据“单一职责”对它进行优化,分成两个类:


class Square {func calculateArea() -> Float {return side * side}func calculatePerimeter() -> Float {return side * 4}
}

class SquareUI {func draw() {// render an square image}func rotate(degree: Float) {// rotate the square image to// the degree and re-render}
}

开闭原则

软件应该是对于扩展开放的,但是对于修改封闭的。
听起来好苛刻,只能拓展,不能修改?好难哦!

我么为什么要遵循这个原则?要知道,每一次修改都会引入破坏现有功能的风险,而且不方便。小时候玩过小霸王游戏机吧?要玩不同的游戏,只需要插上不同的游戏卡就可以,不需要把游戏机拆开修改一翻吧。手柄坏了只需要买个新的插上去就行。
在这里插入图片描述
再比如,假设你是一名成功的开源类库作者,很多开发者使用你的类库。如果某天你要扩展功能,只能通过修改某些代码完成,结果导致类库的使用者都需要修改代码。更可怕的是,他们被迫修改了代码后,又可能造成别的依赖者也被迫修改代码。这种场景绝对是一场灾难。

早些时候,大家通过继承的方式实现开闭原则。新建的类通过继承原有的类实现来重用原类的代码。后来由于抽象化接口的出现,多态成为实现开闭原则的主流形式。多态开闭原则的定义倡导对抽象基类的继承。接口规约可以通过继承来重用,但是实现不必重用。已存在的接口对于修改是封闭的,并且新的实现必须,至少,实现那个接口。

代码示例:

假设有一个支付管理器,它支持现金支付和银联支付:

class PaymentManager {func makeCashPayment(amount: Double){// perform}  func makeVisaPayment(amount: Double){// perform}
}

某天需求发生了变化,需要增加微信支付和支付宝支付功能。那么我们就需要修改这个类,增加两个函数:

    func makeWechatPayment(amount: Double){// perform}func makeAlipayPayment(amount: Double){// perform}

类似这样,每次需求发生变化,我们都得改这个类,并且调用方代码也可能需要被改动。坏味道就出来了,也明显违反了开闭原则。我们用多态把它重构成符合开闭原则的代码。

// 协议 / 接口
protocol PaymentProtocol {func makePayment(amount: Double)
}// 遵从协议的具体类
class CashPayment: PaymentProtocol {func makePayment(amount: Double) {// perform}
}// 遵从协议的具体类
class VisaPayment: PaymentProtocol {func makePayment(amount: Double) {// perform}
}// 这个类以后就不需要修改了,要增加新的支付方式的话,直接新建遵从PaymentProtocol的类
class PaymentManager {func makePayment(amount: Double, payment: PaymentProtocol) {payment.makePayment(amount: amount)}
}

重构后,PaymentManager就是小霸王游戏机的主机,各个具体类就是游戏卡。

里氏替换

程序中的对象应该是可以在不改变程序正确性的前提下被它的子类所替换的。
里氏替换是约束继承的原则。如果正确遵守了历史替换原则,子类可以替换父类而不会破坏功能,那么也就帮助我们实现了开闭原则。

大家都知道,面向对象语言中,子类本来就可以替换父类,为什么还要强调里氏替换原则?重点在“不改变程序正确性”,这里是指业务的正确性,而不是编译的正确性。子类替换父类,在编译时不会报错,但运行时业务可能就错了。

例如,鸵鸟是鸟类的一种,那么“鸵鸟”就可以继承“鸟类”,这似乎合乎常理。但是如果“鸟类”中存在一个 fly() 函数,那么“鸵鸟”就得实现它,可鸵鸟不会飞啊,鸵鸟的 fly() 函数必定是空函数或者抛出异常。这个时候用“鸵鸟”替换“鸟类”就出问题了。
在这里插入图片描述
通常我们会依靠 “A 是 B 的一种” 语法进行子类继承设计,比如“鸵鸟是鸟类的一种”,“正方形是矩形的一种”,这样的划分往往会因为过于粗糙而违反了里氏替换原则。

那么如何解决? 接口分离原则就是为它服务的。

接口分离原则

客户端不应该强制实现他们不需要的函数(多个特定客户端接口要好于一个宽泛用途的接口)。

在上述“鸵鸟”继承“鸟类”的例子中,“鸟类”作为一个大而全的接口存在,它可能是这样:

protocol BirdProtocal {func eat()func fly()func fastRun()func swim()
}

那么不管是“鸵鸟”还是“白鹭”, 直接继承它总会违背历史替换原则,也违背了接口分离原则。

让我们把这个接口分离一下:

protocol BirdProtocal {func eat()
}
// 会飞的鸟
protocol BirdCanFly: BirdProtocal {func fly()
}
// 会快跑的鸟
protocol BirdCanFastRun: BirdProtocal {func fastRun()
}
// 会游泳的鸟
protocol BirdCanSwim: BirdProtocal {func swim()
}

那么“鸵鸟”和“白鹭”的具体实现类就会是这样子:

// 鸵鸟会快速奔跑、会吃食物
class Ostrich: BirdCanFastRun {func eat() {//}func fastRun() {//}
}
// 白鹭会飞、会游泳、会吃食物
class Egret: BirdCanFly, BirdCanSwim {func eat() {//}func fly() {//}func swim() {//}
}

接口这样细分之后,具体类就不会被强制实现他们不需要的函数。
如果“鸵鸟”继承“会快跑的鸟”也不会违反里氏替换原则了。
同时大家也可以看出,这样细分之后的接口,职责也更单一了,也符合了单一职责原则。

依赖倒置

依赖于抽象而不是一个实例。或者可以解释为高层模块不应该依赖底层模块,两者都应该依赖其抽象。要针对接口编程,不要针对实现编程。

他的核心思想是面向接口(协议)编程。可以依靠了依赖注入的方式,实现了解耦。

还是支付的例子。

class PayHandler {func makePayment(type: String, amount: Double) {if type == "CASH" {let cashPayment = CashPayment()cashPayment.makePayment(amount: amount)} else if type == "VISA" {let visPayment = VisaPayment()visPayment.makePayment(amount: amount)} else {// defult payment}// ...}
}

这是一个典型的面向过程的编程方式,PayHandler依赖各个具体的Payment模块.
依赖倒置原则能有效避免过程试编程,拥抱面向对象编程。
我们让各个具体的Payment模块遵守PaymentProtocol接口,就像开闭原则示例代码那样。用依赖注入的方式重构PayHandler

class PayHandler {let paymentManager: PaymentProtocolinit(paymentManager: PaymentProtocol) {self.paymentManager = paymentManager}func makePayment(ammount: Double) {let result = self.paymentManager.makePayment(amount: ammount)// do something else}
}

这样改了之后,PayHandler和各个低层次模块都依赖PaymentProtocol协议(接口), 我们从外部注入低层次模块,直接降低了PayHandler和各个低层次模块的耦合度。这样我们就用依赖倒置原则实现了开闭原则。
在这里插入图片描述

总结

SOLID 的这 5 个设计原则,单独存在的威力不大,应该把它作为一个整体来理解和应用,从而更好地指导你的软件设计。他们的共同目的就是帮助你写出高内聚、低耦合的代码。其中单一职责是基础,接口分离是体现单一职责的最好体现,开闭原则是理想目标,其他几个原则都会直接或间接达成开闭原则,里氏替换是针对继承的约束原则,依赖倒置指导我们从面向过程走向面向对象。他们是有内在联系的,就像练习拳击,耐力、速度、力量、步法、灵活性,都是为打出高质量拳服务的,它们既有联系,又缺一不可。
在这里插入图片描述
有人会说,SOLID 原则太理想化了,实际开发中根本做不到百分百遵守,尤其开闭原则,拿到新需求后,不改动旧代码,只添加新代码进行拓展,不可能啊。没关系,这根本不妨碍它成为我们代码设计的终极目标。起码我们知道了什么是好的设计,这样才能不断往这个目标迈进。

参考资料:
写了这么多年代码,你真的了解SOLID吗?
https://en.wikipedia.org/wiki/SOLID
Design Principles and Design Patterns. Robert C. Martin
The Principles of OOD. Robert C. Martin


http://chatgpt.dhexx.cn/article/GcN5sAF6.shtml

相关文章

软件开发SOLID设计原则

前言:SOLID设计原则,不管是软件系统还是代码的实现,遵循SOLID设计原则,都能够有效的提高系统的灵活和可靠性,应对代码实现的需求变化也能更好的扩展和维护。因此提出了五大原则——SOLID。 我是通过老师讲解以及老师…

Python 中的 SOLID 原则

💂 个人网站:【海拥】【摸鱼游戏】【神级源码资源网】🤟 前端学习课程:👉【28个案例趣学前端】【400个JS面试题】💅 想寻找共同学习交流、摸鱼划水的小伙伴,请点击【摸鱼学习交流群】 SOLID 是一组面向对象…

Kotlin SOLID 原则

Kotlin SOLID 原则 许多 Kotlin 开发者并不完全了解 SOLID 原理,即使他们知道,他们也不知道为什么要使用它。您准备好了解所有细节了吗? 介绍 亲爱的 Kotlin 爱好者,您好!欢迎来到我的新文章。今天我要讲的是 Kotli…

超易懂!原来 SOLID 原则要这么理解!

点击蓝色 “陈树义” 关注我哟 说到 SOLID 原则,相信有过几年工作经验的朋友都有个大概印象,但就是不知道它具体是什么。甚至有些工作了十几年的朋友,它们对 SOLID 原则的理解也停留在表面。今天我们就来聊聊 SOLID 原则以及它们之间的关系。…

SOLID五大原则【图解】

目录 前序 五大基本原则-SOLID 1. SRP 2. OCP 3. LSP 4. ISP 5. DIP 参考链接 前序 做C语言开发的应该都知道,C是面向过程开发的,而c是面向对象开发的。而封装、继承与多态是面向对象开发的三大特征。 但你可能不知道OOD(Object-Oriented Desi…

我所理解的SOLID原则

S.O.L.I.D 是面向对象设计(OOD)和面向对象编程(OOP)中的几个重要编码原则(Programming Priciple)的首字母缩写。 面向对象设计的原则 SRP The Single Responsibility Principle单一职责原则OCP The Open Closed Principle开放封闭原则LSP The Liskov Substitution Principle里…

浅谈 SOLID 原则的具体使用

单一职责原则(SRP)开放封闭原则(OCP)里氏替换原则(LSP)接口隔离原则(ISP)依赖倒置原则(DIP)小结 SOLID 是面向对象设计5大重要原则的首字母缩写,当…

设计模式之SOLID原则再回首

本科阶段学过设计模式,那时对设计模式的五大原则——SOLID原则的概念与理解还是比较模糊,此时过去了2年时间,在学习《高级软件工程》课程中老师又提到了设计模式,课程中还详细讨论了五大原则的过程,这次SOLID原则再回首作者提出了一些更通俗的理解吧~ 一. 什么是设计模式&…

程序设计原则之SOLID原则

设计模式中的SOLID原则,分别是单一原则、开闭原则、里氏替换原则、接口隔离原则、依赖倒置原则。前辈们总结出来的,遵循五大原则可以使程序解决紧耦合,更加健壮。 SOLID原则是由5个设计原则组成,SOLID对应每个原则英文字母的开头…

SOLID原则

SOLID原则是一组设计原则,它们旨在帮助开发人员创建易于维护和可扩展的软件系统,这些原则的缩写代表以下5个原则: 1. 单一职责原则(SRP):一个类应该只有一个职责。 2. 开闭原则(OCP)…

【KAFKA】kafka可视化工具kafkaTool 免费下载

【资源是免费的,官网可下载,可是官网下载的网络实在是太慢了有时候还会断线,我也是花了很长时间才下载下来的,提供给大家一个方便】 符合kafka version 0.11 mac 版:链接:https://pan.baidu.com/s/1q6qKrEbaDGukvqH…

windows 安装kafka流程

1、安装jdk 安装地址:www.oracle.com/java/technologies/downloads 下载好后进行安装,基本上一路点击下一步,不要忘记了把安装目录更换一下! 安装好后需要配置环境变量 找到 "计算机-属性-高级系统设置-高级-环境变量“ 1&…

Window下安装Kafka

目录 一、下载安装 二、配置 三、启动 一、下载安装 注意:Kafka安装文件中包含zookeeper 首先打开Kafka的网站:https://kafka.apache.org/ 点击 Download Kafka,选择适合的版本进行下载。 这里后缀 .tgz 格式文件兼容Windows系统&#x…

kafka的安装和使用(详细版)

原创地址: https://www.cnblogs.com/lilixin/p/5775877.html Kafka安装与使用 下载地址:https://www.apache.org/dyn/closer.cgi?path/kafka/0.8.1.1/kafka_2.10-0.8.1.1.tgz 安装以及启动kafka 步骤1:安装kafka $ tar -xzf kafka_2.10-…

kafka-manager 的下载及安装

kafka-manager 的下载及安装 kafka-manager的功能 为了简化开发者和服务工程师维护Kafka集群的工作,yahoo构建了一个叫做Kafka管理器的基于Web工具,叫做 Kafka Manager。 这个管理工具可以很容易地发现分布在集群中的哪些topic分布不均匀,或…

怎样安装Kafka?

1、Kafka是Java开发的应用程序,可以运行在Windows、 MacOS和 Linux等多 种操作系统上。最常见的是将Kafka安装在Linux系统上。 2、在安装Kafka之前,需要先安装Java环境,虽然运行 Zookeeper 和 Kafka 只需要 Java运行时版本,但也可…

Kafka锦集(一):Kafka的介绍 | 下载和安装 | kafka服务无法关闭 | bin/kafka-server-stop.sh无效 | 总结的很详细

前言 从本篇开始,带你一起领略Kafka的世界,下面重点介绍它的下载、安装,带你避坑。 提示:如果你只是想解决“bin/kafka-server-stop.sh无效”的问题,直接下滑到文章尾部的4.2章节进行查看! 一、Kafka介绍…

docker 下载kafka

Kafka采用的是订阅-发布的模式,消费者主动的去kafka集群拉取消息,与producer相同的是,消费者在拉取消息的时候也是找leader去拉取。 kafka存在的意义:去耦合、异步、中间件的消息系统 首先安装zookeeper docker search zookeepe…

windows下安装kafka

一.下载 kafka官网下载地址:http://kafka.apache.org/downloads.html,下载二进制的. 二.安装 1.安装zookeeper windows环境下安装zookeeper(单机版) 安装并启动后的界面: 2.安装kafka 我下载的kafka_2.13-2.8.0.tgz,并解压到D:\Tools\kafka_2.13-2.8.0目录下 编辑文件Kaf…

kafka tool下载安装和使用

一、下载安装 下载连接:https://www.kafkatool.com/download.html kafka tool官网介绍 Kafka工具是用于管理和使用Apache Kafka集群的GUI应用程序。 它提供了一种直观的UI,可让用户快速查看Kafka集群中的对象以及集群主题中存储的消息。 它包含面向开发…