SOLID 设计原则 - 这篇最容易消化

news2024/9/23 1:36:43

面向对象设计原则 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: Float
    init(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: PaymentProtocol
    init(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://www.coloradmin.cn/o/98785.html

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

相关文章

DevOps实战系列【第十章】:详解Jenkins Pipeline基本概念和语法

个人亲自录制全套DevOps系列实战教程 :手把手教你玩转DevOps全栈技术 流水线基本概念 官方中文手册: https://www.jenkins.io/zh/doc/book/pipeline 我们最好在结合英文文档去看,因为翻译过来的中文比较乱。 Jenkins pipeline是一套插件&…

MySQL 日志,难怪被模仿

一.前言 日志是mysql数据库的重要组成部分,记录着数据库运行期间各种状态信息。mysql日志主要包括错误日志、查询日志、慢查询日志、事务日志、二进制日志几大类。 通过分析日志,我们可以优化数据库性能,排除故障,甚至能够还原数…

中文文献检索网站

1. 中国知网 网址: https://www.cnki.net/ 中国知网被称众多科研人称国内文献论文最全最权威的中文文献库,知网提供中国学术文献、外文文献、学位论文、报纸、会议、年鉴、工具书等各类资源统一检索、统一导航、在线阅读和下载服务。 2. 掌桥科研 网址…

25.Django大型电商项目之地址管理——如何使用三级联动菜单数据加载地址、保存数据、动态获取数据、设置默认值

1. 地址管理基本页面 1.1 概述 1.2 流程 修改templates的跳转链接center.html <ul><li><a href"/userapp/address/">地址管理</a></li> </ul>templates {% extends base.html %} {% block title %}用户中心{% endblock %} {…

东北大学数据结构第八周(排序)

7-1 快速排序 作者 朱允刚 单位 吉林大学 给定包含n个元素的整型数组a[1],a[2],…,a[n]&#xff0c;利用快速排序算法对其进行递增排序&#xff0c;请输出排序过程&#xff0c;即每次Partition之后的数组。每次选择所处理的子数组的第一个元素作为基准元素。 输入格式: 输入为…

MapReduce 序列化案例

文章目录MapReduce 序列化案例一、案例需求二、案例分析map 阶段Reduce 阶段三、代码实现1、编写流量统计的Bean对象2、Mapper阶段代码MapReduce 序列化案例 一、案例需求 1、需求&#xff1a; 统计每一个手机号耗费的总上行流量&#xff0c;下行流量&#xff0c;总流量 2、输…

二、ZFNet可视化卷积神经网络——可解释性机器学习(DataWhale组队学习)

目录引言ZFNet的网络结构可视化反卷积反池化反激活反卷积训练细节特征可视化特征演化特征不变性局部遮挡测试敏感性分析相关性分析消融实验宽度影响深度影响迁移学习能力有效性分析总结引言 纽约大学ZFNet&#xff0c;2013年ImageNet图像分类竞赛冠军模型。对AlexNet进行改进的…

AOP 操作

AOP 操作AOP 操作&#xff08;准备&#xff09;1. Spring 框架一般是基于 AspectJ 实现 AOP 操作&#xff08;1&#xff09;什么是 AspectJ2. 基于 AspectJ 实现 AOP 操作3. 在项目工程里面引入 AOP 先关的依赖4. 切入点表达式举例1&#xff1a;对 com.fairykunkun.dao.UserDao…

谈谈自己对依赖注入的理解

1. 絮絮叨叨 1.1 想学习Google Guice 在工作的过程中&#xff0c;发现有名的大数据组件Presto大量使用Google Guice实现各种Module的构建 很多bind(interface).to(implementClass).in(scope)语句&#xff0c;实现接口与实现类的绑定&#xff0c;并指定实现类是单例还是多例 /…

Service的绑定过程

前言 Service的绑定过程将分为两个部分来进行讲解&#xff1b;分别是Contextlmpl到AMS的调用过程和Service的绑定过程。 frameworks/base/core/java/android/content/ContextWrapper.javapublic boolean bindService(Intent service, ServiceConnection conn,int flags) {ret…

计算机毕设Python+Vue-新型冠状病毒防控咨询网站(程序+LW+部署)

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

根据端口划分虚拟局域、集线器、中继器、交换机、路由器、网桥----计算机网络

集线器&#xff1a; 连接计算机和交换机&#xff0c;类似于多台中继器。 实现多台电脑的同时使用一个进线接口来上网或组成局域网 中继器&#xff1a; 连接两条电缆&#xff0c;作用是放大前一条电缆里面的信号并传入下一条电缆。 是对接收到的信息进行再生放大&#xff0c;以…

Jenkins + Jmeter + Ant 持续集成

搭建提前安装好&#xff1a;ant Jenkins 环境 一、Jenkins 安装 Ant 插件&#xff1a; 进入Jenkins 配置插件页面&#xff0c;安装ant 插件&#xff1a; 打开插件配置页面&#xff0c;如下图&#xff1a; 点击“Available” 在输入框搜索 ant 安装即可&#xff1a; 二、安装…

计算机毕业设计springboot+vue基本微信小程序的透析耗材管理系统

项目介绍 随着信息技术和网络技术的飞速发展,人类已进入全新信息化时代,传统管理技术已无法高效,便捷地管理信息。为了迎合时代需求,优化管理效率,各种各样的管理程序应运而生,各行各业相继进入信息管理时代,透析耗材管理小程序就是信息时代变革中的产物之一。 任何程序都要遵循…

vue前端案例教学:动态获取最新疫情数据展示(代码详解)

【辰兮要努力】&#xff1a;hello你好我是辰兮&#xff0c;很高兴你能来阅读&#xff0c;昵称是希望自己能不断精进&#xff0c;向着优秀程序员前行&#xff01; 博客来源于项目以及编程中遇到的问题总结&#xff0c;偶尔会有读书分享&#xff0c;我会陆续更新Java前端、后台、…

[leetcode.4]寻找两个正序数组的中位数 多思考边界

题目展示 题目要点 题目其实本身的思路非常简单&#xff0c;就是把两个数组给合并起来&#xff0c;然后寻找中位数&#xff0c;具体可以参考我们使用归并排序时候的最后一步&#xff0c;这题的难点其实在于&#xff08;1&#xff09;时间压缩到lognm&#xff08;2&#xff09;…

[附源码]Nodejs计算机毕业设计基于与协同过滤算法的竞赛项目管理Express(程序+LW)

该项目含有源码、文档、程序、数据库、配套开发软件、软件安装教程。欢迎交流 项目运行 环境配置&#xff1a; Node.js Vscode Mysql5.7 HBuilderXNavicat11VueExpress。 项目技术&#xff1a; Express框架 Node.js Vue 等等组成&#xff0c;B/S模式 Vscode管理前后端分…

基于MMEngine和MMDet搭建目标跟踪框架MMTracking-1.0.0rc1-Win10

缘由&#xff1a; 1. 目标跟踪是工业检测和学术研究的基础课题&#xff0c;其pipeline通常分为视频目标检测、视频实例分割、单目标跟踪、多目标跟踪和Re-ID五类&#xff0c;同时&#xff0c;还细分为在线检测和离线检测两种任务模式。由于现阶段关于目标跟踪的教程较少&#…

机器学习 KNN算法原理

目录 一&#xff1a;KNN算法概念 二&#xff1a;KNN原理 三&#xff1a;KNN超参数 四&#xff1a;KNN算法选择 一&#xff1a;KNN算法概念 KNN(K-Nearest Neighbor)法即K最邻近法&#xff0c;最初由Cover和Hart于1968年提出&#xff0c;是最简单的机器学习算法之一 算法思路…

NNDL 作业9:分别使用numpy和pytorch实现BPTT

6-1P&#xff1a;推导RNN反向传播算法BPTT. 6-2P&#xff1a;设计简单RNN模型&#xff0c;分别用Numpy、Pytorch实现反向传播算子&#xff0c;并代入数值测试.、 forward&#xff1a; 我们知道循环卷积网络的cell的计算公式为&#xff1a; stf(UxtWst−1)\mathrm{s}_tf(…