SOLID 原则:编写可扩展且可维护的代码

ops/2024/10/24 4:31:55/

有人告诉过你,你写的是“糟糕的代码”吗?

如果你有,那真的没什么可羞愧的。我们在学习的过程中都会写出有缺陷的代码。好消息是,改进起来相当简单——但前提是你愿意。

改进代码的最佳方法之一是学习一些编程设计原则。你可以将编程原则视为成为更好程序员的一般指南 - 可以说是代码的原始哲学。现在,有一系列的原则(有人可能会说甚至可能过多),但我将介绍五个基本原则,它们都归为缩写 SOLID

ps:我将在示例中使用 Python,但这些概念可以轻松转移到其他语言,例如 Java。

1. “S” 单一职责

在这里插入图片描述

这一原则教导我们:

将我们的代码分成每个负责一个职责的模块

让我们看一下这个Person执行不相关任务(例如发送电子邮件和计算税金)的类。

python">class Person:def __init__(self, name, age):self.name = nameself.age = agedef send_email(self, message):# Code to send an email to the personprint(f"Sending email to {self.name}: {message}")def calculate_tax(self):# Code to calculate tax for the persontax = self.age * 100print(f"{self.name}'s tax: {tax}")

根据单一职责原则,我们应该将Person类拆分为几个较小的类,以避免违反该原则。

python">class Person:def __init__(self, name, age):self.name = nameself.age = ageclass EmailSender:def send_email(person, message):# Code to send an email to the personprint(f"Sending email to {person.name}: {message}")class TaxCalculator:def calculate_tax(person):# Code to calculate tax for the persontax = person.age * 100print(f"{person.name}'s tax: {tax}")

现在我们可以更轻松地识别代码的每个部分试图完成的任务,更干净地测试它,并在其他地方重用它(而不必担心不相关的方法)。

2. “O” 开放/封闭原则

在这里插入图片描述

该原则建议我们设计模块时应能够:

将来添加新的功能,而无需直接修改我们现有的代码

一旦模块被使用,它基本上就被锁定了,这减少了任何新添加的内容破坏您的代码的可能性。
由于其矛盾性,这是 5 项原则中最难完全理解的原则之一,因此让我们看一个例子:

python">class Shape:def __init__(self, shape_type, width, height):self.shape_type = shape_typeself.width = widthself.height = heightdef calculate_area(self):if self.shape_type == "rectangle":# Calculate and return the area of a rectangleelif self.shape_type == "triangle":# Calculate and return the area of a triangle

在上面的例子中,类直接在其方法Shape中处理不同的形状类型。这违反了开放/封闭原则,因为我们修改了现有代码,而不是扩展它。calculate_area()

这种设计是有问题的,因为随着形状类型的增加,该calculate_area()方法会变得更加复杂,更难维护。它违反了职责分离的原则,使代码的灵活性和可扩展性降低。让我们来看看解决这个问题的一种方法。

python">class Shape:def __init__(self, width, height):self.width = widthself.height = heightdef calculate_area(self):passclass Rectangle(Shape):def calculate_area(self):# Implement the calculate_area() method for Rectangleclass Triangle(Shape):def calculate_area(self):# Implement the calculate_area() method for Triangle

在上面的例子中,我们定义了基类Shape,其唯一目的是允许更具体的形状类继承其属性。例如,该类Triangle扩展了calculate_area()方法来计算并返回三角形的面积。

通过遵循开放/封闭原则,我们可以添加新形状而无需修改现有Shape类。这使我们能够扩展代码的功能,而无需更改其核心实现。

3. “L” 里氏替换原则(LSP)

在这里插入图片描述
在这个原则中,Liskov 基本上是想告诉我们以下内容:

子类应该能够与其超类互换使用,而不会破坏程序的功能。

那么这到底意味着什么呢?让我们考虑一个Vehicle有一个名为 的方法的类start_engine()。

python">class Vehicle:def start_engine(self):passclass Car(Vehicle):def start_engine(self):# Start the car engineprint("Car engine started.")class Motorcycle(Vehicle):def start_engine(self):# Start the motorcycle engineprint("Motorcycle engine started.")

根据里氏替换原则,的任何子类Vehicle也应该能够顺利启动引擎。

但是,如果我们添加一个Bicycle类,我们显然就无法启动引擎了,因为自行车没有引擎。下面演示了解决这个问题的错误方法。

python">class Bicycle(Vehicle):def ride(self):# Rides the bikeprint("Riding the bike.")def start_engine(self):# Raises an errorraise NotImplementedError("Bicycle does not have an engine.")

为了正确遵守 LSP,我们可以采取两种方法。我们来看看第一种方法。

解决方案 1: Bicycle成为自己的类(无需继承),以确保所有Vehicle子类的行为与其超类一致。

python">class Vehicle:def start_engine(self):passclass Car(Vehicle):def start_engine(self):# Start the car engineprint("Car engine started.")class Motorcycle(Vehicle):def start_engine(self):# Start the motorcycle engineprint("Motorcycle engine started.")class Bicycle():def ride(self):# Rides the bikeprint("Riding the bike.")

解决方案 2: 将超类Vehicle分成两个,一个用于带发动机的车辆,另一个用于带发动机的车辆。然后,所有子类都可以与其超类互换使用,而不会改变预期行为或引入异常。

python">class VehicleWithEngines:def start_engine(self):passclass VehicleWithoutEngines:def ride(self):passclass Car(VehicleWithEngines):def start_engine(self):# Start the car engineprint("Car engine started.")class Motorcycle(VehicleWithEngines):def start_engine(self):# Start the motorcycle engineprint("Motorcycle engine started.")class Bicycle(VehicleWithoutEngines):def ride(self):# Rides the bikeprint("Riding the bike.")

4. “I”代表接口隔离

在这里插入图片描述
一般定义指出,我们的模块不应该被迫担心它们不使用的功能。但这有点模棱两可。让我们将这句晦涩难懂的句子转换成一组更具体的指令:

客户端专用接口优于通用接口。这意味着类不应该被迫依赖于它们不使用的接口。相反,它们应该依赖于更小、更具体的接口。

假设我们有一个具有诸如、和等Animal方法的接口。walk()、swim()、fly()

python">class Animal:def walk(self):passdef swim(self):passdef fly(self):pass

问题是,并非所有动物都能完成所有这些动作。

例如:狗不会游泳或飞翔,因此从Animal接口继承的这两种方法都变得多余。

python">class Dog(Animal):# Dogs can only walkdef walk(self):print("Dog is walking.")class Fish(Animal):# Fishes can only swimdef swim(self):print("Fish is swimming.")class Bird(Animal):# Birds cannot swimdef walk(self):print("Bird is walking.")def fly(self):print("Bird is flying.")

我们需要将Animal界面分解为更小、更具体的子类别,然后我们可以使用这些子类别来组成每种动物所需的一组精确的功能。

python">class Walkable:def walk(self):passclass Swimmable:def swim(self):passclass Flyable:def fly(self):passclass Dog(Walkable):def walk(self):print("Dog is walking.")class Fish(Swimmable):def swim(self):print("Fish is swimming.")class Bird(Walkable, Flyable):def walk(self):print("Bird is walking.")def fly(self):print("Bird is flying.")

通过这样做,我们实现了一种设计,其中类仅依赖于它们所需的接口,从而减少了不必要的依赖。这在测试时特别有用,因为它允许我们仅模拟每个模块所需的功能。

5. “D” 依赖倒置

在这里插入图片描述这一点解释起来很简单,它指出:

高级模块不应该直接依赖于低级模块。相反,两者都应该依赖于抽象(接口或抽象类)。

再一次,我们来看一个例子。假设我们有一个ReportGenerator自然生成报告的类。要执行此操作,它需要首先从数据库获取数据。

python">class SQLDatabase:def fetch_data(self):# Fetch data from a SQL databaseprint("Fetching data from SQL database...")class ReportGenerator:def __init__(self, database: SQLDatabase):self.database = databasedef generate_report(self):data = self.database.fetch_data()# Generate report using the fetched dataprint("Generating report...")

在这个例子中,ReportGenerator类直接依赖于具体SQLDatabase类。

目前这还不错,但如果我们想切换到不同的数据库(如 MongoDB)该怎么办?这种紧密耦合使得在不修改类的情况下更换数据库实现变得困难ReportGenerator。

SQLDatabase为了遵守依赖倒置原则,我们将引入和MongoDatabase类都可以依赖的抽象(或接口) 。

python">class Database():def fetch_data(self):passclass SQLDatabase(Database):def fetch_data(self):# Fetch data from a SQL databaseprint("Fetching data from SQL database...")class MongoDatabase(Database):def fetch_data(self):# Fetch data from a Mongo databaseprint("Fetching data from Mongo database...")

请注意,该类ReportGenerator现在还将Database通过其构造函数依赖于新的接口。

python">class ReportGenerator:def __init__(self, database: Database):self.database = databasedef generate_report(self):data = self.database.fetch_data()# Generate report using the fetched dataprint("Generating report...")

高级模块 ( ReportGenerator) 现在不再直接依赖于低级模块 (SQLDatabase或MongoDatabase)。相反,它们都依赖于接口 ( Database)。

依赖反转意味着我们的模块不需要知道它们得到了什么实现——只需要知道它们将接收某些输入并返回某些输出。

结论

在这里插入图片描述
如今,我看到网上有很多关于 SOLID 设计原则的讨论,以及它们是否经受住了时间的考验。在这个多范式编程、云计算和机器学习的现代世界中…… SOLID 是否仍然有意义?

我个人认为 SOLID 原则永远是良好代码设计的基础。有时,在处理小型应用程序时,这些原则的好处可能并不明显,但一旦你开始处理更大规模的项目,代码质量的差异就值得你去学习它们。SOLID 所倡导的模块化仍然使这些原则成为现代软件架构的基础,我个人认为这种情况不会很快改变。

本文译自The SOLID Principles: Writing Scalable & Maintainable Code

参考文档
https://forreya.medium.com/the-solid-principles-writing-scalable-maintainable-code-13040ada3bca


http://www.ppmy.cn/ops/128006.html

相关文章

毕业设计项目系统:基于Springboot框架的心理咨询评估管理系统,完整源代码+数据库+毕设文档+部署说明

本文关键字:Java编程;Springboot框架;毕业设计;毕设项目;编程实战;医护人员管理系统;项目源代码;程序数据库;毕设文档;开题报告和任务书;项目部署…

Android 12.0进程保活白名单功能实现

在Android 12.0系统中,实现进程保活白名单功能是为了确保某些重要的应用程序即使进入后台也能长时间保持运行状态,不被系统自动杀死。这一功能的实现涉及多个核心类和文件,以下是具体的实现步骤和核心功能分析: 一、实现步骤 …

【python Arrow库】一个处理日期和时间的Python库

Arrow库 引言:箭,不仅仅是武器1、安装:搭弓上箭2、基础:箭头的构造3、实战:箭无虚发3.1 案例一:时间比较3.2 案例二:时间格式化3.3 案例三:时区转换 4、结语:箭已离弦 引…

LeetCode15 三数之和 - “贪心+双指针: 基于”两数之和“的拓展题“

Leetcode 15: 三数之和 题目链接 发布在LeetCode上的题解 思路 这道题的思路建立在 167.两数之和 的基础上。先来看看”两数之和“的大概题意: 已知一个非递减的数组,找出满足相加之和等于目标数 target 的两个数,假设每个输…

【linux】网络基础

1. 网络发展 独立模式->网络互联->局域网LAN->广域网WAN 独立模式: 计算机之间相互独立网络互联: 多台计算机连接在一起, 完成数据共享局域网LAN: 计算机数量更多了, 通过交换机和路由器连接在一起广域网WAN: 将远隔千里的计算机都连在一起 2. 认识协议 "协议…

【MySQL 保姆级教学】表结构的操作(4)

表结构的操作 1. 定义和语法2. 创建表 CREATE2.1 创建表的本质2.2 表的存储引擎2.3 表的字符集和校验规则2.4 创建表实例 3. 查看表结构 DESC3.1 作用3.2 示例 4. 修改表结构 ALTER4.1 添加列 ADD4.2 修改列 MODIFY4.3 删除列 DROP4.4 更改列名 CHANGE 5. 修改表名 RENAME6. 删…

CRMEB标准版Mysql修改sql_mode

数据库配置 1.宝塔控制面板-软件商店-MySql-设置 2.点击配置修改,查找sql-mode或sql_mode (可使用CtrlF快捷查找) 3.复制 NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION 然后替换粘贴,保存 注:MySQL8.0版本的 第三步用…

记录一个容易混淆的 Spring Boot 项目配置文件问题

记录一个容易混淆的 Spring Boot 项目配置文件问题 去年,我遇到了这样一个问题: 在这个例子中,由于密码 password 以 0 开头,当它被 Spring Boot 的 bean 读取时,前导的 0 被自动去掉了。这导致程序无法正确读取密码。…