每个开发人员都应了解的 SOLID 原则

每个开发人员都应了解的 SOLID 原则

面向对象的编程类型为软件开发带来了全新的设计。

这使开发人员能够将具有相同目的/功能的数据合并到一个类中,以处理该类的唯一目的,而无需考虑整个应用程序。

但是,这种面向对象编程并不能防止程序混乱或无法维护。

因此,罗伯特-C-马丁Robert C. Martin制定了五项准则。这五条准则/原则使开发人员能够轻松创建可读和可维护的程序。

这五项原则被称为 S.O.L.I.D 原则(缩写由 Michael Feathers 提出)。

S:单一责任原则

O: 开放-封闭原则

L:利斯科夫替代原则

I:接口隔离原则

D:依赖反转原则

下面我们将详细讨论这些原则。

注:本文中的大多数示例可能并不适合实际情况,或不适用于现实世界的应用。这完全取决于你自己的设计和用例。最重要的是理解并知道如何应用/遵循这些原则。

提示:使用 Bit (GitHub) 等工具可以轻松地在项目和应用程序中共享和重用组件(和小模块)。

它还能帮助您和您的团队节省时间、保持同步并加快共同开发的速度。它是免费的,不妨一试。

作者的推荐协作工具:

跨应用程序和项目轻松共享组件 组件发现与协作 - Bit

Bit 是开发人员共享组件和协作的地方,让他们共同打造令人惊叹的软件。发现共享组件...

单一责任原则

"......你只有一项工作"--《雷神索尔:毁灭之战》中洛基对斯库奇说

一个类应该只有一项工作。

一个类只能负责一件事。如果一个类有多个职责,它就会变得耦合。一个职责的改变会导致另一个职责的修改。

注:这一原则不仅适用于类,也适用于软件组件和微服务。 例如,请考虑以下设计

class Animal {
    constructor(name: string){ }
    # 构造函数(名称:字符串)
    getAnimalName() { }
    saveAnimal(a: Animal) { }
}

Animal 类的构造违反了 SRP

如何违反 SRP 的?

SRP 规定类应有一项职责,在这里可以得出两项职责:

动物数据库管理和动物属性管理。

构造函数和 getAnimalName 管理动物属性,而 saveAnimal 则管理动物在数据库中的存储。

这种设计将来会产生什么问题?

如果应用程序发生变化,影响到数据库管理功能。使用动物属性的类就必须修改并重新编译,以适应新的变化。

你看充满了僵化的味道,就像多米诺骨牌效应,触动一张牌就会影响到其他所有的牌。

为了使这个系统符合 SRP,我们创建了另一个类,专门负责将动物存储到数据库中:

class Animal {
    constructor(name: string){ }
    getAnimalName() { }
}
class AnimalDB {
    getAnimal(a: Animal) { }
    saveAnimal(a: Animal) { }
}

在设计我们的类时,我们应该将相关的功能放在一起,这样当它们发生变化时,变化的原因就会相同。如果功能变化的原因不同,则应尽量将它们分开。- 史蒂夫-芬顿

正确运用这些原则,我们的应用程序就会变得高度内聚。

开放-封闭原则

软件实体(类、模块、函数)应开放供扩展,而非修改。

让我们继续我们的动物类。

class Animal {
    constructor(name: string){ }
    getAnimalName() { }
}

我们要遍历动物列表并发出它们的声音。

//...
const animals: Array<Animal> = [
    new Animal('lion')、
    new Animal('mouse')
];
function AnimalSound(a: Array<Animal>) {
    for(int i = 0; i <= a.length; i++) {
        if(a[i].name == 'lion')
            log('roar');
        if(a[i].name == 'mouse')
            log('squeak');
    }
}

AnimalSound(animals); 函数 AnimalSound 不符合开放-封闭原则,因为它不能对新的动物种类进行封闭。

如果我们添加一种新的动物,蛇:

//...
const animals: Array<Animal> = [
    new Animal('lion')、
    new Animal('mouse')、
    new Animal('snake')
]
//...

我们必须修改 AnimalSound 函数:

//...
function AnimalSound(a: Array<Animal>) {
    for(int i = 0; i <= a.length; i++) {
        if(a[i].name == 'lion')
            log('roar');
        if(a[i].name == 'mouse')
            log('squeak');
        if(a[i].name == '蛇')
            log('hiss');
    }
}
AnimalSound(animals);

你看,每出现一种新动物,AnimalSound 函数就会增加一个新逻辑。

这只是一个简单的例子。当您的应用程序发展壮大并变得复杂时,您会发现每次添加新动物时,if 语句都会在 AnimalSound 函数中重复出现,遍布整个应用程序。

我们如何使它(AnimalSound)符合 OCP 标准呢?

class Animal {
        makeSound();
        //...
}
class Lion extends Animal {
    makeSound() {
        return 'roar';
    }
}
class Squirrel extends Animal {
    makeSound() {
        return 'squeak';
    }
}
class Snake extends Animal {
    makeSound() {
        return 'hiss';
    }
}
//...
function AnimalSound(a: Array<Animal>) {
    for(int i = 0; i <= a.length; i++) {
        log(a[i].makeSound());
    }
}
AnimalSound(animals);

动物现在有了一个虚拟方法 makeSound。我们让每个动物扩展 Animal 类并实现虚拟 makeSound 方法。

每种动物都会在 makeSound 中添加自己的发声方法。AnimalSound 会遍历动物数组,并调用其 makeSound 方法。

现在,如果我们添加一个新的动物,AnimalSound 不需要更改。我们只需将新动物添加到动物数组中即可。

现在,AnimalSound 符合 OCP 原则。

另一个例子:

假设你有一家商店,你可以使用这个类给你最喜欢的顾客打八折:

class Discount {
    giveDiscount() {
        return this.price * 0.2
    }
}

当您决定向 VIP 客户提供双倍的 20% 折扣时。您可以这样修改该类:

class Discount {
    giveDiscount() {
        if(this.customer == 'fav') {
            return this.price * 0.2;
        }
        if(this.customer == 'vip') {
            return this.price * 0.4;
        }
    }
}

不,这违反了 OCP 原则。

OCP 禁止这样做。如果我们想给不同类型的客户提供新的折扣,你会发现需要添加一个新的逻辑。

为了遵循 OCP 原则,我们将添加一个新类来扩展折扣类。在这个新类中,我们将实现它的新行为:

class VIPDiscount: Discount {
    getDiscount() {
        return super.getDiscount() * 2;
    }
}

如果您决定向超级 VIP 客户提供 80% 的折扣,它应该是这样的:

class SuperVIPDiscount: VIPDiscount {
    getDiscount() {
        return super.getDiscount() * 2;
    }
}

你看,扩展无需修改。

利斯科夫替代原则

子类必须可以替代其超类

该原则的目的是确保子类可以替代其超类而不会出错。如果代码发现自己在检查类的类型,那么它一定违反了这一原则。

让我们以动物为例。

//...
function AnimalLegCount(a: Array<Animal>) {
    for(int i = 0; i <= a.length; i++) {
        if(typeof a[i] == Lion)
            log(LionLegCount(a[i]));
        if(typeof a[i] == Mouse)
            log(MouseLegCount(a[i]));
        if(typeof a[i] == 蛇)
            log(SnakeLegCount(a[i]));
    }
}
AnimalLegCount(animals);

这违反了 LSP 原则以及 OCP 原则。它必须知道每一种动物类型,并调用相关的计算腿函数。

每创建一个新动物,都必须修改函数以接受新动物。

//...
class Pigeon extends Animal {
        
}
const animals[]: Array<Animal> = [
    //...,
    new Pigeon();
]
function AnimalLegCount(a: Array<Animal>) {
    for(int i = 0; i <= a.length; i++) {
        if(typeof a[i] == Lion)
            log(LionLegCount(a[i]));
        if(typeof a[i] == Mouse)
            log(MouseLegCount(a[i]));
         if(typeof a[i] == Snake)
            log(SnakeLegCount(a[i]));
        if(typeof a[i] == Pigeon)
            log(PigeonLegCount(a[i]));
    }
}
AnimalLegCount(animals);

为了使该函数遵循 LSP 原则,我们将遵循 Steve Fenton 提出的 LSP 要求:

如果超类 Animal有一个接受超类类型 Animal参数的方法。其子类 Pigeon 应接受超类类型 Animal 类型或子类类型 Pigeon 类型 作为参数。

现在,我们可以重新实现 AnimalLegCount 函数:

function AnimalLegCount(a: Array<Animal>) {
    for(let i = 0; i <= a.length; i++) {
        a[i].LegCount();
    }
}
AnimalLegCount(animals);

AnimalLegCount 函数不关心传递的动物类型,它只是调用 LegCount 方法。

它只知道参数必须是 Animal 类型,要么是 Animal 类,要么是它的子类。

现在,动物类必须实现/定义一个 LegCount 方法:

class Animal {
    //...
    LegCount();
}

它的子类必须实现 LegCount 方法:

//...
class Lion extends Animal{
    //...
    LegCount() {
        //...
    }
}
//...

当传递给 AnimalLegCount 函数时,它会返回狮子的腿数。

你看,AnimalLegCount 不需要知道 Animal 的类型就能返回它的腿数,它只需调用 Animal 类型的 LegCount 方法,

因为根据契约,Animal 类的子类必须实现 LegCount 函数。

接口隔离原则

制作客户机专用的细粒度接口

不应强迫客户依赖他们不使用的接口。

这一原则解决了实现大型接口的弊端。

让我们看看下面的 IShape 接口:

interface IShape {
    drawCircle();
    drawSquare();
    drawRectangle();
}

该接口用于绘制正方形、圆形和矩形。

实现 IShape 接口的类 Circle、Square 或 Rectangle 必须定义 drawCircle()、drawSquare()、drawRectangle() 方法。

class Circle implements IShape {
    drawCircle(){
        //...
    }
    drawSquare(){
        //...
    }
    drawRectangle(){
        //...
    }    
}

class Square implements IShape {
    drawCircle(){
        //...
    }
    drawSquare(){
        //...
    }
    drawRectangle(){
        //...
    }    
}

class Rectangle implements IShape {
    drawCircle(){
        //...
    }
    drawSquare(){
        //...
    }
    drawRectangle(){
        //...
    }    
}

上面的代码非常有趣:

矩形类实现了它用不上的方法 drawCircle 和 drawSquare;

正方形类实现了drawCircle 和 drawRectangle;

圆形类实现了 drawSquare、drawSquare;

如果我们在 IShape 接口中再添加一个方法,

如 drawTriangle()

interface IShape {
    drawCircle();
    drawSquare();
    drawRectangle();
    drawTriangle();
}

这些类必须实现新方法,否则会出错。

我们看到,要实现一个能画圆但不能画矩形、正方形或三角形的形状是不可能的。我们只需实现抛出错误的方法,说明无法执行操作即可。

ISP 不赞成这种 IShape 接口的设计。

客户端(此处为矩形、圆形和正方形)不应被迫依赖于它们不需要或不使用的方法。

此外,ISP 还规定,接口应只执行一项工作(就像 SRP 原则一样),任何额外的行为分组都应抽象到另一个接口中。

在这里,我们的 IShape 接口执行的操作应由其他接口独立处理。

为了使 IShape 接口符合 ISP 原则,我们将这些行为分离到不同的接口中:

interface IShape {
    draw();
}


interface ICircle {
    drawCircle();
}


interface ISquare {
    drawSquare();
}



interface IRectangle {
    drawRectangle();
}


interface ITriangle {
    drawTriangle();
}


class Circle implements ICircle {
    drawCircle() {
        //...
    }
}


class Square implements ISquare {
    drawSquare() {
        //...
    }
}


class Rectangle implements IRectangle {
    drawRectangle() {
        //...
    }    
}


class Triangle implements ITriangle {
    drawTriangle() {
        //...
    }
}


class CustomShape implements IShape {
   draw(){
      //...
   }
}

ICircle 接口只处理圆形的绘制,IShape 接口处理任何形状的绘制:),ISquare 接口只处理正方形的绘制,IRectangle 接口处理矩形的绘制。

类(圆形、矩形、正方形、三角形等)可以继承 IShape 接口并实现自己的绘制行为。

class Circle implements IShape {
    draw(){
        //...
    }
}

class Triangle implements IShape {
    draw(){
        //...
    }
}

class Square implements IShape {
    draw(){
        //...
    }
}

class Rectangle implements IShape {
    draw(){
        //...
    }
}                                            

然后,我们就可以使用 I 接口创建半圆、直角三角形、等边三角形、钝角矩形等特定形状。

依赖反转原则

应依赖于抽象而非具体事物

A. 高层模块不应依赖低层模块。两者都应依赖抽象。

B. 抽象不应依赖细节。细节应依赖抽象。

在软件开发过程中,我们的应用程序将主要由模块组成。这时,我们必须使用依赖注入来理清思路。高层组件依赖于低层组件来运行。


class XMLHttpService extends XMLHttpRequestService {}
class Http {
    constructor(private xmlhttpService: XMLHttpService) { }
    get(url: string , options: any) {
        this.xmlhttpService.request(url,'GET');
    }
    post() {
        this.xmlhttpService.request(url,'POST');
    }
    //...
}

在这里,Http 是高级组件,而 HttpService 是低级组件。这种设计违反了 DIP A:高层模块不应依赖于低层模块。它应该依赖于自己的抽象。

Http 类被迫依赖 XMLHttpService 类。如果我们要改变 Http 连接服务,也许我们想通过 Nodejs 或甚至模拟 http 服务连接到互联网。

我们将不得不煞费苦心地通过 Http 的所有实例来编辑代码,这违反了 OCP 原则。

Http 类不应该关心你使用的 Http 服务类型。我们创建一个 Connection 接口:

interface Connection {
    request(url: string, opts:any);
}

Connection 接口有一个请求方法。有了它,我们就可以向 Http 类传递 Connection 类型的参数:

class Http {
    constructor(private httpConnection: Connection) { }
    get(url: string , options: any) {
        this.httpConnection.request(url,'GET');
    }
    post() {
        this.httpConnection.request(url,'POST');
    }
    //...
}

现在,无论传递给 Http 的 Http 连接服务是什么类型,它都能轻松连接到网络,而不必费心去了解网络连接的类型。

现在,我们可以重新实现 XMLHttpService 类,以实现 Connection 接口:

class XMLHttpService implements Connection {
    const xhr = new XMLHttpRequest();
    //...
    request(url: string, opts:any) {
        xhr.open();
        xhr.send();
    }
}

我们可以创建多种 Http 连接类型,并将其传递给我们的 Http 类,而不必担心出错。

class NodeHttpService implements Connection {
    request(url: string, opts:any) {
        //...
    }
}
class MockHttpService implements Connection {
    request(url: string, opts:any) {
        //...
    }    
}

现在,我们可以看到高层模块和低层模块都依赖于抽象。

Http 类(高级模块)依赖于 Connection 接口(抽象),而 Http 服务类型(低级模块)反过来也依赖于 Connection 接口(抽象)。

此外,DIP 将迫使我们不违反利斯科夫替代原则:连接类型 Node-XML-MockHttpService 可替代其父类型 Connection。

结论

我们在这里介绍了每个软件开发人员都必须遵守的五项原则。一开始,遵守所有这些原则可能会让人望而生畏,但通过不断的实践和坚持,这些原则将成为我们的一部分,并将对我们应用程序的维护产生巨大的影响。

本文由 mdnice 多平台发布

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

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

相关文章

如何避免GCC优化选项对程序带来的干扰?

引言 先从一小段代码说起&#xff1a; #include <stdio.h>int main() {int sum 0;for (int i 0; i < 100; i) {sum i;}printf("sum %d\n", sum);return 0; }将代码以-O2选项编译后&#xff0c;查看目标程序中的汇率指令&#xff1a; gcc test.c -O2 o…

PHP: 开发入门macOS系统下的安装和配置

安装Homebrew 安装 ~~友情提示&#xff1a;这个命令对网络有要求&#xff0c;可能需要翻墙或者用你的手机热点试试&#xff0c;或者把DNS换成&#xff08;114.114.114.114 和 8.8.8.8&#xff09; /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebr…

centos7 部署Tomcat和jpress应用

目录 一、静态、动态、伪静态 二、Web 1.0 和 Web 2.0 三、centos7 部署Tomcat 3.1 安装、配置jdk 3.2 安装 Tomcat 3.3 配置服务启动脚本 3.3.1 创建用户和组 3.3.2 创建tomcat.conf文件 3.3.3 创建服务脚本(tomcat.service) 3.3.4 重新加载守护进程并且测试 四、部…

webpack

文章目录 webpack概念打包的场景为什么要打包在打包之外 - 翻译在打包之外 - 小动作 课程重点模块化利用立即执行函数来改变 作用域模块化的优点模块化方案的进化史AMD&#xff08;成型比较早&#xff0c;应用不是很广泛&#xff09;COMMONJSES6 MODULE webpack 的打包机制webp…

【前端知识】React 基础巩固(四十四)——其他Hooks(useContext、useReducer、useCallback)

React 基础巩固(四十四)——其他Hooks&#xff08;useContext、useReducer、useCallback&#xff09; 一、useContext的使用 在类组件开发时&#xff0c;我们通过 类名.contextType MyContext的方式&#xff0c;在类中获取context&#xff0c;多个Context或者在函数式组件中…

手机设置全局代理ip步骤

在互联网时代&#xff0c;隐私和安全问题备受关注。使用全局代理能够帮助我们保护个人信息&#xff0c;突破地理限制&#xff0c;并提高网络速度。但是&#xff0c;你是否对全局代理的安全性存有疑虑&#xff1f;而且&#xff0c;如何在手机上设置全局代理呢&#xff1f;今天就…

用LangChain开源框架实现知识机器人

前言 Large Language Models (LLMs)在2020年OpenAI 的 GPT-3 的发布而进入世界舞台 。从那时起&#xff0c;他们稳步增长进入公众视野。 众所周知 OpenAI 的 API 无法联网&#xff0c;所以大家如果想通过它的API实现联网搜索并给出回答、总结 PDF 文档、基于某个 Youtube 视频…

优维低代码实践:Context / State

优维低代码技术专栏&#xff0c;是一个全新的、技术为主的专栏&#xff0c;由优维技术委员会成员执笔&#xff0c;基于优维7年低代码技术研发及运维成果&#xff0c;主要介绍低代码相关的技术原理及架构逻辑&#xff0c;目的是给广大运维人提供一个技术交流与学习的平台。 优维…

阿里云 MSE + ZadigX ,无门槛实现云原生全链路灰度发布

作者&#xff1a;ZadigX 企业发布现状痛点 目前企业在选择和实施发布策略时面临以下困境&#xff1a; 1. 缺乏云原生能力&#xff1a; 由于从传统部署转变为云原生模式后&#xff0c;技术架构改造需要具备相关能力的人才。这使得企业在发布策略方面难以入手。 2. 缺乏自动化…

U盘删除的文件怎么找回?4个简单方法分享!

“在u盘里不小心删除的文件到底还能不能找回来呀&#xff1f;真的好着急啊&#xff01;这个u盘对我来说真的很重要&#xff0c;怎么恢复里面的数据呢&#xff1f;请各位大佬帮帮我吧&#xff01;” 作为一个便捷的存储工具&#xff0c;u盘逐渐获得大众的青睐。在互联网时代&…

《练习100》36~40

题目36 # 对10个数排序my_list [2,2,1,1,3,67,43,22,55,10,11]print(my_list) my_list.sort() print(my_list)题目37 # 求一个3*3的矩阵对角线元素之和 # 主对角线&#xff1a; 00 11 22 # 副对角线&#xff1a; 02 11 20def get_diagonal_sum(matrix):matsize len(matrix)…

微服务性能分析工具 Pyroscope 初体验

Go 自带接口性能分析工具 pprof&#xff0c;较为常用的有以下 4 种分析&#xff1a; CPU Profiling: CPU 分析&#xff0c;按照一定的频率采集所监听的应用程序 CPU&#xff08;含寄存器&#xff09;的使用情况&#xff0c;可确定应用程序在主动消耗 CPU 周期时花费时间的位置…

计算机毕设 深度学习手势识别 - yolo python opencv cnn 机器视觉

文章目录 0 前言1 课题背景2 卷积神经网络2.1卷积层2.2 池化层2.3 激活函数2.4 全连接层2.5 使用tensorflow中keras模块实现卷积神经网络 3 YOLOV53.1 网络架构图3.2 输入端3.3 基准网络3.4 Neck网络3.5 Head输出层 4 数据集准备4.1 数据标注简介4.2 数据保存 5 模型训练5.1 修…

Doccano工具安装教程/文本标注工具/文本标注自己的项目/NLP分词器工具/自然语言处理必备工具/如何使用文本标注工具

这篇文章是专门的安装教程&#xff0c;后续的项目创建&#xff0c;如何使用&#xff0c;以及代码部分可以参考这篇文章&#xff1a; NER实战&#xff1a;(NLP实战/命名实体识别/文本标注/Doccano工具使用/关键信息抽取/Token分类/源码解读/代码逐行解读)_会害羞的杨卓越的博客-…

SO_KEEPALIVE、TCP_KEEPIDLE、TCP_KEEPINTVL、保活包

SO_KEEPALIVE SO_KEEPALIVE 是一个套接字选项&#xff0c;用于设置是否启用 keepalive 机制。在这段代码中没有涉及到 SO_KEEPALIVE 选项的设置。 当 SO_KEEPALIVE 被设置为非零值时&#xff0c;表示启用 keepalive 机制。keepalive 是一种用于检测连接是否仍然有效的机制。通…

SVN学习

SVN学习 以下总结是看了一个b站up主的视频总结出来的。 1. 简介 SVN是代码版本管理工具&#xff0c;它能记住每次的修改、查看所有修改记录、恢复到任何历史版本和恢复已经删除的文件。 SVN比起Git的好处就是使用简单&#xff0c;上手快&#xff1b;具备目录级权限控制&…

【LeetCode每日一题】——1572.矩阵对角线元素的和

文章目录 一【题目类别】二【题目难度】三【题目编号】四【题目描述】五【题目示例】六【题目提示】七【解题思路】八【时间频度】九【代码实现】十【提交结果】 一【题目类别】 矩阵 二【题目难度】 简单 三【题目编号】 1572.矩阵对角线元素的和 四【题目描述】 给你一…

交换机VLAN技术和实验(eNSP)

目录 一&#xff0c;交换机的演变 1.1&#xff0c;最小网络单元 1.2&#xff0c;中继器&#xff08;物理层&#xff09; 1.3&#xff0c;集线器&#xff08;物理层&#xff09; 1.4&#xff0c;网桥&#xff08;数据链路层&#xff09; 二&#xff0c;交换机的工作行为 2.…

【计算机视觉中的 GAN 】如何稳定GAN训练(3)

一、说明 在上一篇文章中&#xff0c;我们达到了理解未配对图像到图像翻译的地步。尽管如此&#xff0c;在实现自己的超酷深度GAN模型之前&#xff0c;您必须了解一些非常重要的概念。如本文所提的GAN模型新成员的引入&#xff1a;Wasserstein distance&#xff0c;boundary eq…

AI 绘画Stable Diffusion 研究(一)sd整合包v4.2 版本安装说明

部署包作者:秋葉aaaki 免责声明: 本安装包及启动器免费提供 无任何盈利目的 大家好&#xff0c;我是风雨无阻。众所周知&#xff0c;StableDiffusion 是非常强大的AI绘图工具&#xff0c;需要详细了解StableDiffusion的朋友&#xff0c;可查看我之前的这篇文章&#xff1a; 最…