SOLID原则:现代软件架构的永恒基石

news2024/9/29 18:05:47

关注TechLead,复旦博士,分享云服务领域全维度开发技术。拥有10+年互联网服务架构、AI产品研发经验、团队管理经验,复旦机器人智能实验室成员,国家级大学生赛事评审专家,发表多篇SCI核心期刊学术论文,阿里云认证的资深架构师,上亿营收AI产品研发负责人。

file

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

如果有,不必感到羞愧。我们在学习的过程中都会写出有缺陷的代码。不过好消息是,只要你愿意,改善代码其实很简单。

提升代码质量的最佳方式之一就是学习一些编程设计原则。可以把编程原则视为成为更优秀程序员的通用指南——代码的原始哲学。如今有各种各样的设计原则(有人可能会认为甚至太多了),但我将介绍五个核心原则,它们组成了一个首字母缩略词:SOLID。

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

1. 首先是SOLID中的’S’——单一职责原则

file

这个原则告诉我们:

将代码拆分为每个模块只负责一个职责

让我们来看这个执行不相关任务的Person类,它既发送电子邮件又计算税款。

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
    
    def send_email(self, message):
        print(f"发送邮件给 {self.name}: {message}")
    
    def calculate_tax(self):
        tax = self.age * 100
        print(f"{self.name} 的税款: {tax}")

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

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

class EmailSender:
    def send_email(self, person, message):
        print(f"发送邮件给 {person.name}: {message}")

class TaxCalculator:
    def calculate_tax(self, person):
        tax = person.age * 100
        print(f"{person.name} 的税款: {tax}")

虽然代码行数增多了,但我们现在可以更清楚地识别代码的每个部分所要完成的任务,测试时也更干净,同时可以在其他地方复用这些部分,而无需担心不相关的方法。

2. 接下来是’O’——开闭原则

file
这个原则建议我们设计的模块应当:

能够在未来添加新功能,而不直接修改现有代码

一旦某个模块被使用,它就基本上是“锁定的”,这减少了新添加的功能破坏代码的风险。

这五个原则中,开闭原则可能是最难完全掌握的,因为它的性质带有一定的矛盾性。让我们通过一个例子来解释:

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":
            return self.width * self.height
        elif self.shape_type == "triangle":
            return self.width * self.height / 2

在上面的例子中,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):
        return self.width * self.height

class Triangle(Shape):
    def calculate_area(self):
        return self.width * self.height / 2

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

通过遵循开闭原则,我们可以在不修改现有Shape类的情况下添加新形状,从而扩展代码的功能,而无需改变其核心实现。

3. 现在是’L’——里氏替换原则 (LSP)

file
里氏替换原则告诉我们:

子类应该能够替换它们的超类,而不会破坏程序的功能

这意味着什么呢?让我们来看一个Vehicle类及其start_engine()方法。

class Vehicle:
    def start_engine(self):
        pass

class Car(Vehicle):
    def start_engine(self):
        print("汽车发动机启动。")

class Motorcycle(Vehicle):
    def start_engine(self):
        print("摩托车发动机启动。")

根据里氏替换原则Vehicle的任何子类都应该能够启动引擎而不会出现问题。

但如果我们添加了一个Bicycle类,很显然自行车没有引擎。因此,以下是不正确的解决方案示例:

class Bicycle(Vehicle):
    def start_engine(self):
        raise NotImplementedError("自行车没有引擎。")

为了正确遵守里氏替换原则,我们可以采取两个解决方案。让我们先看第一个。

解决方案1: Bicycle成为它自己的独立类(没有继承),确保所有Vehicle子类与超类行为一致。

class Vehicle:
    def start_engine(self):
        pass

class Car(Vehicle):
    def start_engine(self):
        print("汽车发动机启动。")

class Motorcycle(Vehicle):
    def start_engine(self):
        print("摩托车发动机启动。")

class Bicycle:
    def ride(self):
        print("骑自行车。")

解决方案2:Vehicle超类拆分为两类,一类是有引擎的车辆,另一类是没有引擎的。这样所有子类都能与其超类保持一致,而不需要改变预期行为或引入例外。

class VehicleWithEngines:
    def start_engine(self):
        pass

class VehicleWithoutEngines:
    def ride(self):
        pass

class Car(VehicleWithEngines):
    def start_engine(self):
        print("汽车发动机启动。")

class Motorcycle(VehicleWithEngines):
    def start_engine(self):
        print("摩托车发动机启动。")

class Bicycle(VehicleWithoutEngines):
    def ride(self):
        print("骑自行车。")

4. 接下来是’I’——接口隔离原则

file
这个原则的定义有些模糊,但可以归纳为:

客户端特定的接口优于通用接口。
换句话说,类不应该被迫依赖它们不使用的接口。相反,它们应依赖于更小、更具体的接口。

假设我们有一个Animal接口,包含walk()swim()fly()方法。

class Animal:
    def walk(self):
        pass

    def swim(self):
        pass

    def fly(self):
        pass

问题是,并不是所有动物都能执行这些动作。例如,狗不能游泳或飞行,因此这两个从Animal接口继承的方法对狗来说是多余的。

class Dog(Animal):
    def walk(self):
        print("狗在走路。")

class Fish(Animal):
    def swim(self):
        print("鱼在游泳。")

class Bird(Animal):
    def walk(self):
        print("鸟在走路。")
    
    def fly(self):
        print("鸟在飞行。")

我们需要将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("狗在走路。")

class Fish(Swimmable):
    def swim(self):
        print("鱼在游泳。")

class Bird(Walkable, Flyable):
    def walk(self):
        print("鸟在走路。")
    
    def fly(self):
        print("鸟在飞行。")

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

5. 最后是’D’——依赖倒置原则

file
这个原则非常简单明了:

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

让我们来看一个例子。假设我们有一个ReportGenerator类,用于生成报告。要执行此操作,它需要先从数据库中获取数据。

class SQLDatabase:
    def fetch_data(self):
        print("从SQL数据库获取数据...")

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

    def generate_report(self):
        data = self.database.fetch_data()
        print("生成报告...")

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

这在目前运行良好,但如果我们想切换到不同的数据库(例如MongoDB),这种紧密耦合会使得难以在不修改ReportGenerator类的情况下更换数据库实现。

为了遵守依赖倒置原则,我们引入一个抽象(或接口),让SQLDatabaseMongoDatabase都依赖于它。

class Database:
    def fetch_data(self):
        pass

class SQLDatabase(Database):
    def fetch_data(self):
        print("从SQL数据库获取数据...")

class MongoDatabase(Database):
    def fetch_data(self):
        print("从Mongo数据库获取数据...")

注意,ReportGenerator类现在依赖于新的Database接口。

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

    def generate_report(self):
        data = self.database.fetch_data()
        print("生成报告...")

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

依赖倒置意味着我们的模块不需要知道它们接收到的是哪种具体实现——只需确保它们接收到某些输入并返回某些输出即可。

结论

现在,网上有很多关于SOLID设计原则的讨论,探讨它们是否经受住了时间的考验。在这个多范式编程、云计算和机器学习的现代世界里,SOLID是否仍然适用?

其实,SOLID原则将永远是良好代码设计的基础。当处理小型应用时,这些原则的好处可能不那么明显,但一旦开始参与大规模项目,代码质量上的差异是非常值得学习这些原则的。SOLID所倡导的模块化设计仍然使这些原则成为现代软件架构的基石,我认为这种情况在短时间内不会改变。

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

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

相关文章

面积开运算bwareaopen

一个非常有用的二值图像形态学后处理算法,建立在连通分量分析的基础之上。 bwareaopen 从二值图像中删除小对象 语法 BW2 bwareaopen(BW,P) BW2 bwareaopen(BW,P,conn) 说明 BW2 bwareaopen(BW,P) 从二值图像 BW 中删除少于 P 个像素的所有连通分量&#x…

docker简介、安装、基础知识

基础知识 Docker简介: 1.Docker是一种用于构建、发布及运行应用程序的开源项目,他通过容器化技术简化了应用程序的部署和管理 2.Docker是一个开源的应用容器引擎,基于go语言开发,为应用打包、部署平台,而非单纯的虚…

计算机毕业设计 办公用品管理系统的设计与实现 Java实战项目 附源码+文档+视频讲解

博主介绍:✌从事软件开发10年之余,专注于Java技术领域、Python人工智能及数据挖掘、小程序项目开发和Android项目开发等。CSDN、掘金、华为云、InfoQ、阿里云等平台优质作者✌ 🍅文末获取源码联系🍅 👇🏻 精…

C++(Qt)软件调试---内存调试器Dr.Memory(21)

C(Qt)软件调试—内存调试器Dr. Memory(21) 文章目录 C(Qt)软件调试---内存调试器Dr. Memory(21)[toc]1、概述🐜2、安装Dr.Memory🪲3、命令行使用Dr.Memory🦗4、Qt Creator集成使用Dr.Memory&…

预计OpenAI在新一轮融资后估值可达1500亿美元!Hugging Face平台托管模型数量破100万|AI日报

文章推荐 HuggingChat macOS版正式发布!文章内附体验地址!我国打造糖尿病专用AI模型|AI日报 今日热点 OpenAI在新一轮融资后估值可能达到1500亿美元 知情人士表示,Thrive Capital将在OpenAI目前的65亿美元融资轮中投资超过10亿…

如何高效管理知识产权全链条?

为了有效保护企业的创新成果,确保技术创意的顺利转化,以及高效管理知识产权案件,建立一套完善的知识产权管理体系至关重要。对于企业而言,如何有效地管理知识产权的各个环节,从研发项目到技术创意,再到提案…

排序(交换排序:快排)

快速排序: 写快排的注意事项 1.单趟排序hoare 2.不写优化只说优化就行 理想的情况下:每次排序都是二分,直到二分到最后,那就相当于递归高度次(logN),每一层单趟排都是O(N),时间复杂度O(NlogN) 空间复杂度就…

PHP程离禁用一段IP的写法示例

PHP程离禁用一段IP的写法示例 。 在PHP中,如果你想禁用一段IP地址的访问,你可以使用$_SERVER[REMOTE_ADDR]来获取访问者的IP地址,然后通过判断IP地址是否在你想要禁用的范围内来决定是否拒绝服务。 以下是一个简单的例子,展示了…

net Core aspx视图引擎 razor视图引擎

视图引擎 》》定义,什么是视图引擎 视图引擎就是,将服务器端模板转换为HTML标记,并在控制器的操作方法触发时在web浏览器中呈现 现在都推荐 Razor视图引擎了(也是默认视图引擎),aspx引擎不推荐了。 ASPX …

AI新掌舵:智享AI直播系统:直播界的新浪潮还是真人主播的终结者?

AI新掌舵:智享AI直播系统:直播界的新浪潮还是真人主播的终结者? 在数字化浪潮的汹涌澎湃中,人工智能(AI)以其前所未有的速度渗透至各行各业,其中,直播领域正经历着一场前所未有的变革…

javascript:冻结对象

1 作用 冻结一个对象,使对象不可扩展。 2 特性 对象的属性不可再被新增、删除对象的属性的值不可再被修改对象的属性的描述符中任意配置项都不可被重新定义 3 代码示例 3.1 冻结对象 Object.freeze() 代码如下: use strict let initialData {a: 1…

C#案例 | 基于C#语言在Excel中进行二次开发(一):简单系统搭建:打印输出“Hello Excel C#”

基于C#语言在Excel中进行二次开发(一):简单系统搭建:打印输出”Hello Excel & C#” 实现效果第一步:前期准备第二步:打开VS 2022,创建项目第三步:程序界面设计 实现效果 在Exce…

【大模型】通俗解读变分自编码器VAE

目录 写在前面 一、VAE结构 二、损失函数 三、代码实现 1.训练代码 2.推理生成图片 3.插值编辑图片 四、总结 写在前面 论文地址:https://arxiv.org/abs/1312.6114 大模型已经有了突破性的进展,图文的生成质量都越来越高,可控性也越来…

cesium渲染的3Dtiles的模型下载!glb模型文件下载!

前端开发测试或者学习cesium的时候最难最麻烦就是找到一个合适的模型,现在就直接给各位放几个可以满足我们测试使用的模型文件。 模型文件下载—香港3DTiles模型文件 某盘 通过百度网盘分享的文件:hk-效果图.png,hk.zip等2个文件 链接&…

react中的ref三种形式

1&#xff0c;字符串形式 <!-- 创建盒子 --><div id"test"></div> <script type"text/babel">class Demo extends React.Component{render(){return(<div><input type"text" refinput1 /><button onCl…

奔驰EQS450suv升级增强AR抬头显示HUD案例分享

以下是奔驰 EQS450 SUV 升级增强版 AR 抬头显示的一般改装案例步骤及相关信息&#xff1a; 配件&#xff1a;通常包括显示屏、仪表模块、饰板等。 安装步骤&#xff1a; 1. 拆下中控的仪表。 2. 在仪表上预留位置切割出合适的孔位&#xff0c;用于安装显示器。 3. 将显示器…

宝塔部署若依前端出现502解决方法

一、前言 ‌若依系统是一个基于Java语言的开源项目&#xff0c;旨在帮助开发者减少开发时间&#xff0c;特别适用于需要快速开发出一套具有用户管理、菜单管理、权限管理、定时任务、日志管理等功能的简单系统。‌ 系统分为前后端分离、分布式等架构 部署教程如下&#xff1a…

单体到微服务架构服务演化过程

架构服务化 聊聊从单体到微服务架构服务演化过程 单体分层架构 在 Web 应用程序发展的早期&#xff0c;大部分工程是将所有的服务端功能模块打包到单个巨石型&#xff08;Monolith&#xff09;应用中&#xff0c;譬如很多企业的 Java 应用程序打包为 war 包&#xff0c;最终会形…

软文代发高效率推广方式解析-华媒舍

在这个时代&#xff0c;软文代发成为了一种非常实用的推广方法。如何有效地开展软文代发营销推广&#xff0c;并不是每个人都知道的。下面我们就以高效软文代发推广方式大曝光为主线&#xff0c;为书友详细介绍科谱有关的内容。 一、什么叫软文代发 软文代发是指由企业或个人必…

引入 LangChain4j 来简化 LLM 与 Java 应用程序的集成

作者&#xff1a;来自 Elastic David Pilato LangChain4j 框架于 2023 年创建&#xff0c;其目标如下&#xff1a; LangChain4j 的目标是简化将 LLM 集成到 Java 应用程序的过程。 LangChain4j 提供了一种标准方法&#xff1a; 根据给定内容&#xff08;例如文本&#xff09;创…