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

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

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

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

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

1. “S” 单一职责

在这里插入图片描述

这一原则教导我们:

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

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

class Person:
  def __init__(self, name, age):
    self.name = name
    self.age = age
  
    def send_email(self, message):
        # Code to send an email to the person
        print(f"Sending email to {self.name}: {message}")

    def calculate_tax(self):
        # Code to calculate tax for the person
        tax = self.age * 100
        print(f"{self.name}'s tax: {tax}")

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

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

class EmailSender:
    def send_email(person, message):
        # Code to send an email to the person
        print(f"Sending email to {person.name}: {message}")

class TaxCalculator:
    def calculate_tax(person):
        # Code to calculate tax for the person
        tax = person.age * 100
        print(f"{person.name}'s tax: {tax}")

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

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

在这里插入图片描述

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

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

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

class Shape:
    def __init__(self, shape_type, width, height):
        self.shape_type = shape_type
        self.width = width
        self.height = height

    def calculate_area(self):
        if self.shape_type == "rectangle":
            # Calculate and return the area of a rectangle
        elif self.shape_type == "triangle":
            # Calculate and return the area of a triangle

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

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

class Shape:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def calculate_area(self):
        pass

class Rectangle(Shape):
    def calculate_area(self):
        # Implement the calculate_area() method for Rectangle

class 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()。

class Vehicle:
    def start_engine(self):
        pass

class Car(Vehicle):
    def start_engine(self):
        # Start the car engine
        print("Car engine started.")

class Motorcycle(Vehicle):
    def start_engine(self):
        # Start the motorcycle engine
        print("Motorcycle engine started.")

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

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

class Bicycle(Vehicle):
    def ride(self):
        # Rides the bike
        print("Riding the bike.")

    def start_engine(self):
         # Raises an error
        raise NotImplementedError(
        	"Bicycle does not have an engine.")

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

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

class Vehicle:
    def start_engine(self):
        pass

class Car(Vehicle):
    def start_engine(self):
        # Start the car engine
        print("Car engine started.")

class Motorcycle(Vehicle):
    def start_engine(self):
        # Start the motorcycle engine
        print("Motorcycle engine started.")

class Bicycle():
    def ride(self):
        # Rides the bike
        print("Riding the bike.")

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

class VehicleWithEngines:
    def start_engine(self):
        pass

class VehicleWithoutEngines:
    def ride(self):
        pass

class Car(VehicleWithEngines):
    def start_engine(self):
        # Start the car engine
        print("Car engine started.")

class Motorcycle(VehicleWithEngines):
    def start_engine(self):
        # Start the motorcycle engine
        print("Motorcycle engine started.")

class Bicycle(VehicleWithoutEngines):
    def ride(self):
        # Rides the bike
        print("Riding the bike.")

4. “I”代表接口隔离

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

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

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

class Animal:
    def walk(self):
        pass

    def swim(self):
        pass

    def fly(self):
        pass

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

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

class Dog(Animal):
    # Dogs can only walk
    def walk(self):
        print("Dog is walking.")

class Fish(Animal):
    # Fishes can only swim
    def swim(self):
        print("Fish is swimming.")

class Bird(Animal):
    # Birds cannot swim
    def walk(self):
        print("Bird is walking.")

    def fly(self):
        print("Bird is flying.")

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

class Walkable:
    def walk(self):
        pass

class Swimmable:
    def swim(self):
        pass

class Flyable:
    def fly(self):
        pass

class 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自然生成报告的类。要执行此操作,它需要首先从数据库获取数据。

class SQLDatabase:
    def fetch_data(self):
        # Fetch data from a SQL database
        print("Fetching data from SQL database...")

class ReportGenerator:
    def __init__(self, database: SQLDatabase):
        self.database = database

    def generate_report(self):
        data = self.database.fetch_data()
        # Generate report using the fetched data
        print("Generating report...")

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

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

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

class Database():
    def fetch_data(self):
        pass

class SQLDatabase(Database):
    def fetch_data(self):
        # Fetch data from a SQL database
        print("Fetching data from SQL database...")

class MongoDatabase(Database):
    def fetch_data(self):
        # Fetch data from a Mongo database
        print("Fetching data from Mongo database...")

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

class ReportGenerator:
    def __init__(self, database: Database):
        self.database = database

    def generate_report(self):
        data = self.database.fetch_data()
        # Generate report using the fetched data
        print("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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

当贝F7Pro怎么样?一文看懂当贝秋季新品当贝F7Pro值不值得买?

当贝投影在今年的双11阶段发布了一款全新护眼三色激光投影当贝F7Pro 4K激光投影,这款被誉为“4K激光真旗舰”的激光投影主要是定位高端系列;不仅采用了全新的护眼三色激光技术,全面提升了投影画面的亮度、色彩和色准;在4K分辨率&a…

【Linux系统】Ubuntu的简单操作

什么是 Ubuntu? Ubuntu(乌帮图)是一个非洲词汇,它的意思是“人性对待他人”或“群在故我在”。Ubuntu发行版将Ubuntu精神带到软件世界之中。 目前已有大量各种各样基于GNU/Linux的操作系统,例如:Debian,SuSE,Gentoo,R…

猜数游戏(Fortran)

背景 学了两个月Fortran还没来一次正式练习 于是—— 代码 program gessnum! implicit none 不取消IN规则。integer::num,areal::Ncall random_seed()call random_number(N)aint(N*10)print*,"请输入您猜的数字:"read(*,*)numdo i1,3if (numa)thenpri…

【Next.js 项目实战系列】02-创建 Issue

原文链接 CSDN 的排版/样式可能有问题,去我的博客查看原文系列吧,觉得有用的话,给我的库点个star,关注一下吧 上一篇【Next.js 项目实战系列】01-创建项目 创建 Issue 配置 MySQL 与 Prisma​ 在数据库中可以找到相关内容&…

【Linux】【xmake】安装 + C/C++常用项目配置

文章目录 0. 环境准备1. 子命令create - 快速创建项目build - 构建程序config - 配置编译需要的参数show - 查看当前工程基本信息update - 程序自更新 2. C/C 项目常用配置2.1 项目目标类型2.2 添加宏定义2.3 头文件路径和链接库配置2.4 设置语言标准2.5 设置编译优化2.6 添加源…

《YOLO 目标检测》—— YOLO v3 详细介绍

!!!!!!!!!!!!!还未写完!!!!!!!&#xf…

vscode插件live server无法在手机预览调试H5网页

环境 Window10、vscode:1.94.2、Live Server:v5.7.9、Live Server (Five Server):v0.3.1 问题 PC端预览没有问题,但是在手机点击链接显示访问失败 排查 1. 是否同一局域网 意思就是电脑、手机是不是访问同一个网络。电脑插得…

【设计模式-原型】

**原型模式(Prototype Pattern)**是一种创建型设计模式,旨在通过复制现有对象的方式来创建新对象,而不是通过实例化类来创建对象。该模式允许对象通过克隆(复制)来创建新的实例,因此避免了重新创…

Git核心概念图例与最常用内容操作(reset、diff、restore、stash、reflog、cherry-pick)

文章目录 简介前置概念.git目录objects目录refs目录HEAD文件 resetreflog 与 reset --hardrevert(撤销指定提交)stashdiff工作区与暂存区差异暂存区与HEAD差异工作区与HEAD差异其他比较 restore、checkout(代码撤回)merge、rebase、cherry-pick 简介 本文将介绍Git几个核心概念…

赛氪提供专业技术支持,首届“天翼云息壤杯”高校AI大赛正式开启

2024年9月25日,在ICT中国2024高层论坛暨国际信息通信展主论坛上,首届“天翼云息壤杯”高校AI大赛正式拉开帷幕。中国电信总经理梁宝俊出席并发表了致辞。此次大赛由国务院国资委、工业和信息化部、教育部等部委指导,中国电信集团有限公司和华…

【排序】快排思想以及例子

思想 使用分治法来处理数据 例题 19 97 09 17 01 08 首先确定一个pivot 一般是首位&#xff0c;把比p小的放p的左边&#xff0c;比p大的放p的右边。L是左指 R是右指 首轮排序 p 19 __ 97 09 17 01 08 L R 首先应从R开始判断 08<19 08替换到p所在位置&#xff0c;R移动 p 19…

【AIGC】AI时代降临,AI文案写作、AI绘画、AI数据处理

目录 1、ChatGPTAI文案与写作108招2、AI短视频生成与剪辑实战108招3、AI绘画与摄影实战108招4、AI商业广告设计实战108招5、AI数据处理实战108招6、AI智能办公实战108招 传送门&#xff1a;清华大学出版社AI实战108招 全6册 1、ChatGPTAI文案与写作108招 《ChatGPTAI文案与写…

DDD重构-实体与限界上下文重构

DDD重构-实体与限界上下文重构 概述 DDD 方法需要不同类型的类元素&#xff0c;例如实体或值对象&#xff0c;并且几乎所有这些类元素都可以看作是常规的 Java 类。它们的总体结构是 Name: 类的唯一名称 Properties&#xff1a;属性 Methods: 控制变量的变化和添加行为 一…

MySQL 基础查询

1、DISTINCT select DISTINCT EMPLOYEE_ID ,FIRST_NAME from employees 按照ID去重&#xff0c;DISTINCT的字段要放在前面&#xff0c;不会再继续在FIRST_NAME上去重判断&#xff1b; 如果需要多字段去重&#xff0c;需要用到group by&#xff0c;这个后面讲&#xff1b; …

Redis-04 Redis管道

Redis 管道&#xff08;Pipelining&#xff09;是一种技术&#xff0c;它允许客户端一次发送多个命令给服务器&#xff0c;而无需等待每个命令的响应。这样可以减少网络延迟和提高命令的执行效率。 创建txt文件&#xff0c;里面写需要执行的操作&#xff0c;然后使用cat命令一次…

【C++打怪之路Lv11】-- stack、queue和优先级队列

&#x1f308; 个人主页&#xff1a;白子寰 &#x1f525; 分类专栏&#xff1a;重生之我在学Linux&#xff0c;C打怪之路&#xff0c;python从入门到精通&#xff0c;数据结构&#xff0c;C语言&#xff0c;C语言题集&#x1f448; 希望得到您的订阅和支持~ &#x1f4a1; 坚持…

基于SSM+微信小程序的家庭记账本管理系统(家庭1)

&#x1f449;文末查看项目功能视频演示获取源码sql脚本视频导入教程视频 1、项目介绍 1、管理员端功能有首页、个人中心、用户管理&#xff0c;消费详情管理、收入详情管理、系统管理等。 2、用户端功能有首页、消费详情、收入详情、论坛信息、我的等功能。 2、项目技术 …

C语言教程——数组(1)

目录 系列文章目录 前言 1、一维数组的创建和初始化 1.1数组的创建 1.2数组的初始化 1.3一维数组的使用 总结 1.4一维数组在内存中的存储 2、二维数组的创建和初始化 2.1二维数组的创建 2.2二维数组的初始化 2.3二维数组的使用 2.4二维数组在内存中的存储 3、数组…

时空智友企业流程化管控系统uploadStudioFile接口存在任意文件上传漏洞

免责声明&#xff1a;请勿利用文章内的相关技术从事非法测试&#xff0c;由于传播、利用此文所提供的信息或者工具而造成的任何直接或者间接的后果及损失&#xff0c;均由使用者本人负责&#xff0c;所产生的一切不良后果与文章作者无关。该文章仅供学习用途使用。 1. 时空智友…

【Vulnhub靶场】Kioptrix Level 3

目标 本机IP&#xff1a;192.168.118.128 目标IP&#xff1a;192.168.118.0/24 信息收集 常规 nmap 扫存活主机&#xff0c;扫端口 根据靶机IP容易得出靶机IP为 192.168.118.133 nmap -sP 192.168.118.0/24nmap -p- 192.168.118.133 Getshell 开放22端口和80 端口 访问web…