一文搞懂设计模式之七大原则

大家好,我是晴天。在接下来的一个多月里,我将跟大家一起学习设计模式的一些基础知识和基本应用。不要问我为什么突然想起来写一个设计模式系列的文章,问就是:爱过。。。

一文搞懂设计模式.png

问题引出

作为程序猿的我们,隔三岔五的就会因为看老板不顺眼或者觉得自己英雄无用武之地而选择换一个赏识自己的老板或者能充分展示自我才华的宝地。而当我们进入一家新公司的时候,经常会因为一些历史的原因,而发现新公司的代码像一坨*一样,要么结构很混乱、要么阅读起来很困难扩展起来也非常麻烦重用性更是免谈,总之让人难以下咽。我们想改,但是又不敢改,无从下手,只能在上面添加新的*。这就是祖传代码带来的巨大难题,如果在初期没有一个很好的代码结构设计的话。

设计模式是什么

设计模式是研究类和本身以及类和类之间如何协作的模式,是在软件开发中常见问题的解决方案模板。这些模板是经验丰富的开发者在解决各种问题时提出的最佳实践的总结。设计模式提供了一种通用的、可重复使用的方法,可以用于解决特定类型的问题,以改善软件的结构、可维护性和可扩展性。总之一句话:设计模式是针对于面向对象编程而设计的一套代码编程规范或者说是套路

为什么要用设计模式

它是为了解决结构混乱代码阅读困难代码扩展麻烦以及代码重用很复杂这几个棘手的问题的。


经过上面我粗浅的一个介绍,我们有请今天的主角:设计模式的六大法则出场。。。当当。。。

设计模式的七大原则

  1. 单一职责原则(类和方法,接口)
  2. 开闭原则 (扩展开放,修改关闭)
  3. 里氏替换原则(基类和子类之间的关系)
  4. 依赖倒置原则(依赖抽象接口,而不是具体对象)
  5. 接口隔离原则(接口按照功能细分)
  6. 合成复用原则
  7. 迪米特法则 (类与类之间的亲疏关系)

逐一介绍一下这六大法则

1.单一职责原则

单一职责原则指的是类的职责单一,对外只提供一种方法
通俗理解:你让司机把车开到修理厂,并且对车进行维修,不好意思,司机只负责把车开到修理厂,不负责维修。

代码示例如下:

张三写的初版代码

package mainimport "fmt"// 单一职责:每个类只对外提供一种功能// 司机类
type Driver struct {
}// 司机类有一个开车的方法
func (d *Driver) Drive() {fmt.Println("开车")
}// 司机类还有一个修车的方法
func (d *Driver) Fix() {fmt.Println("修车")
}func main() {d := Driver{}// 司机开车d.Drive() //输出:开车// 司机修车d.Fix() //输出:修车
}

这时候张三想要调整一下代码,把修车的逻辑也改成开车的逻辑,这就是不遵循单一职责原则的坏处,修改某个类方法的逻辑时,有可能会影响到该类的其他方法的准确性,造成误解。在留下这一个大坑后,张三离职了。。。

package mainimport "fmt"// 单一职责:每个类只对外提供一种功能// 司机类
type Driver struct {
}// 司机类有一个开车的方法
func (d *Driver) Drive() {fmt.Println("开车")
}// 司机类还有一个修车的方法
func (d *Driver) Fix() {fmt.Println("开车")  /***这行做了修改***/
}func main() {d := Driver{}// 司机开车d.Drive() //输出:开车// 司机修车d.Fix() //输出:开车
}

这时候,李四入职了,看到张三的代码,发现Drive方法和Fix方法的逻辑是一样一样的,于是李四也修改了一版代码,发现结果仍旧是不变的,就变成了下面这样。

package mainimport "fmt"// 单一职责:每个类只对外提供一种功能// 司机类
type Driver struct {
}// 司机类有一个开车的方法
func (d *Driver) Drive() {fmt.Println("开车")
}// 司机类还有一个修车的方法
func (d *Driver) Fix() {fmt.Println("开车")
}func main() {d := Driver{}// 司机开车d.Drive() //输出:开车// 司机修车d.Drive() //输出:开车   /**这行做了修改**/
}

这时候李四也离职了,可怜的王五入职了,他发现无论是司机开车,还是司机修车,都需要调用Drive方法,他可能就认为:哦~司机在开车和修车之前都需要调用Drive方法。这就造成了很严重的歧义。

这种歧义其实就是没有遵守单一职责原则而导致的。那么正确的写法应该怎样呢,王五做了如下修改:

package mainimport "fmt"// 单一职责:每个类只对外提供一种功能// 司机类:专门负责开车
type Driver struct {
}// 司机类有一个开车的方法
func (d *Driver) Drive() {fmt.Println("开车")
}// 修理工类:专门负责修车
type Fixer struct {     /**修改:增加了一个修理工类**/
}// 修理工类有一个修车的方法
func (f *Fixer) Fix() {   /**修改:对修理工类增加一个修理方法**/fmt.Println("修车")
}func main() {d := Driver{}// 司机开车d.Drive() //输出:开车// 修理工修车f := Fixer{}f.Fix() // 输出:修车
}

这样呢,无论你修改司机类的方法还是修理工类的方法,都不会影响到其他方法。

2.开闭原则

开闭原则是指类的改动是通过增加代码来实现的,而不是修改源代码。(对扩展开放,对修改关闭)通俗理解:你需要汽车司机,就去招募汽车司机,需要飞行员就去招募飞行员,你不能要求一个人既会开汽车又会开飞机,甚至以后还要求他会开坦克…

代码示例如下:

张三写的初版代码(不遵循开闭原则的)

package mainimport "fmt"type People struct {
}func (p *People) Drive() {fmt.Println("司机开车")
}func (p *People) Fly() {fmt.Println("飞行员开飞机")
}func main() {var p Peoplep.Drive() //司机开车p.Fly()   //飞行员开飞机
}

这种结构有个问题就是,如果还有其他的人物角色,比如船员,那么我们只能再增加一个船员的方法

func (p *People) Ship() {fmt.Println("船员开船")
}

这样其实就对People这个类进行了修改,那么就有可能会影响到这个类的其他方法的功能。

那么应该怎么修改呢,我们来看一下王五的优化方法

package main// 开闭原则import "fmt"/*
开闭原则:
类的改动是通过增加代码来实现的,而不是修改源代码
*/
// 通过抽象出People,让其他类直接实现People的方法即可
type People interface {doWork()
}type Driver struct {
}func (d *Driver) doWork() {fmt.Println("司机开车")
}type Pilot struct {
}func (p *Pilot) doWork() {fmt.Println("飞行员开飞机")
}func main() {// 司机开车d := Driver{}d.doWork()// 飞行员开飞机p := Pilot{}p.doWork()
}

这样写的好处在于,当有一个新的职业出现时,只需要继承People的doWork方法即可,完全不会影响之前其他职业的方法。

*更进一步:开闭原则的基础上进行拓展,多态的实现

package main// 开闭原则import "fmt"/*
开闭原则:
类的改动是通过增加代码来实现的,而不是修改源代码
*/
type People interface {doWork()
}type Driver struct {
}func (d *Driver) doWork() {fmt.Println("司机开车")
}type Pilot struct {
}func (p *Pilot) doWork() {fmt.Println("飞行员开飞机")
}// 对抽象对象进行操作,用于实现多态方法
// 多态:父类指针指向子类对象,调用子类对象的方法
func PeopleDoWork(p People) {    /***修改的部分***/p.doWork()
}func main() {d := Driver{}d.doWork() // 司机开车p := Pilot{}p.doWork() // 飞行员开飞机fmt.Println("---------------")PeopleDoWork(&Driver{}) // 司机开车  /**修改的部分**/PeopleDoWork(&Pilot{})  // 飞行员开飞机  /**修改的部分**/
}

当我们新增一个类时,完全不会影响到之前的代码逻辑,可以放心地进行修改。

依赖倒转.drawio (2).png

3.里氏替换原则

里氏替换原则指的是任何抽象类/基类(interface)都可以用它的实现类来进行替换。通俗理解:任何拥有A类驾照的人,都能开C类驾照的车。(这里可以把C类驾照理解为基类)

这个原则的代码示例可以参考前面一段代码示例,People是基类,Driver和Pilot是实现类,任何People出现的地方,都可以用Diver或Pilot来替换。

4.依赖倒转原则

依赖倒转原则是实现层和业务逻辑层只依赖于抽象类(interface),不依赖于具体实现类(struct),面向接口编程。通俗理解:汽车企业造车,同一类车型(抽象),都按照相同的构造制造,不同的型号或批次(具体实现)可以添加一些不同的细节,不是直接按照每一辆车的型号进行制造。

不使用依赖倒转原则的代码

package mainimport "fmt"type BMWCar struct {
}func (c *BMWCar) Run() {fmt.Println("BMW car is running")
}type AudiCar struct {
}func (c *AudiCar) Run() {fmt.Println("Audi car is running")
}type Zhang3 struct {
}func (z *Zhang3) DriveBMW(car *BMWCar) {fmt.Println("zhang3 is driving car")car.Run()
}
func (z *Zhang3) DriveAudo(car *AudiCar) {fmt.Println("zhang3 is driving car")car.Run()
}type Li4 struct {
}func (l *Li4) DriveBMW(car *BMWCar) {fmt.Println("li4 is driving car")car.Run()
}
func (z *Li4) DriveAudo(car *AudiCar) {fmt.Println("Li4 is driving car")car.Run()
}func main() {var bmw *BMWCarvar audi *AudiCarvar z3 Zhang3var l4 Li4z3.DriveBMW(bmw)z3.DriveAudo(audi)l4.DriveBMW(bmw)l4.DriveAudo(audi)
}

大家发现问题没有,不使用依赖倒转,如果新增一个司机wang5,并且新增一个车型benz,就需要把wang5重新实现DriveAudi,DriveBMW,DriveBenz这三个方法,形成司机和车型的全组合。每新增一个用户和一个车型,都需要补充全部的方法。

依赖倒转.drawio.png

使用依赖倒转原则以后,再进行扩充,只需要实现各自抽象类(interface)的方法即可,无需关注其他类都有哪些方法。

package mainimport "fmt"// 依赖倒转原则:实现层和业务逻辑层都只依赖于抽象层// 抽象层
// 抽象层之间相互依赖
type Car interface {Run()
}type Driver interface {Drive(car Car)
}// 实现层
// 汽车实现层
// Benz只需要实现Run方法即可
type Benz struct {
}func (b *Benz) Run() {fmt.Println("benz is running")
}type Bmw struct {
}func (b *Bmw) Run() {fmt.Println("bmw is running")
}// 实现层
// 司机实现层
// zhang3只需要实现Drive方法即可
type zhang3 struct {
}func (z *zhang3) Drive(car Car) {fmt.Println("zhang3 开汽车")car.Run()
}type li4 struct {
}func (l *li4) Drive(car Car) {fmt.Println("li4 开汽车")car.Run()
}// 业务逻辑层
func main() {// 只依赖于抽象层,针对抽象层编程// 抽象汽车var car Carcar = new(Benz)// 抽象司机var driver Driver// 里氏替换原则,用具体实现类替换抽象类driver = new(zhang3)driver.Drive(car) // zhang3 开汽车  benz is runningcar = new(Bmw)driver = new(li4)driver.Drive(car) // li4 开汽车 bmw is running
}

如果需要增加一个wang5,只需要让wang5实现Drive方法即可

// 司机实现层增加代码如下
type wang5 struct {
}func (l *wang5) Drive(car Car) {fmt.Println("wang5 开汽车")car.Run()
}// 业务逻辑层增加代码如下
var wang5 Driver
wang5=new(wang5)
wang5.Drive(car)

依赖倒转.drawio (1).png
如上图所示,司机只需要实现司机的方法,汽车只需要实现汽车的方法,完全不需要关心其他司机或者其他汽车有什么方法。大大降低了耦合性。

5.接口隔离原则

接口隔离原则是指接口应该“小而专”,不应该强迫用户依赖那些用不到的接口方法
这个原则的代码跟合成复用原则的代码合并到一起介绍。

6.合成复用原则

合成复用原则是指,如果修改父类的方法会影响子类的方法,那么这个父类和子类就不应该采用继承,而应该使用组合

补充知识点:
如果一个struct嵌套了另一个有名结构体,那么这个模式就叫组合。
如果一个struct嵌套了另一个匿名结构体(只有类型没有名字),那么这个结构可以直接访问匿名结构体的方法,从而实现了继承。
如果一个struct嵌套了多个匿名结构体,那么这个结构可以直接访问多个匿名结构体的方法,从而实现了多重继承。

如下代码,举例说明继承和组合的关系:

package mainimport "fmt"type Dog struct {
}func (d *Dog) Eat() {fmt.Println("dog eat food")
}// 继承Dog所有的属性,其中就包括继承了Eat方法
type ChaiDog struct {Dog
}func (c *ChaiDog) Sleep() {fmt.Println("dog is sleeping")
}// 组合方式有两种
// 1.直接在结构体中组合
type JingDog struct {d Dog
}func (j *JingDog) Eat() {j.d.Eat()
}
func (j *JingDog) Sleep() {fmt.Println("jingDog is sleeping")
}// 2.通过参数传递的方式组合
type GuifuDog struct {
}
// 只把Eat方法跟Dog对象耦合,其他方法都不耦合
func (gf *GuifuDog) Eat(dog Dog) {dog.Eat()fmt.Println("guifuDog eat food")
}func main() {// 原始Dog类对象d1 := Dog{}d1.Eat()// chaiDog继承Dog类的所有cd := ChaiDog{}cd.Eat() // 调用继承过来的Eat方法cd.Sleep()jd := JingDog{}jd.Eat()gfd := GuifuDog{}gfd.Eat(d1)
}

继承的弊端:

  1. 灵活性低,继承容易导致代码嵌套层次很深,可维护性变差。
  2. 耦合性高。父类修改方法可能影响到子类行为。
  3. 使用继承,在子类对象调用父类方法的时候,父类的方法也会被调用(不局限于Golang语言),如果继承层级很深的话,所有祖先对象相同的方法也都会被调用一遍,大大降低了效率。

使用组合的方法,可以仅仅依赖某个对象的属性或者某个方法,能够极大降低依赖关系,减小耦合。所以一般推荐使用聚合/组合代替继承。

7.迪米特原则

迪米特原则是指一个对象应该尽量少的了解其他对象,从而降低耦合度

这里留一个扣子,等到本系列后续介绍到外观模式的时候,再来补充迪米特法则的代码示例,敬请期待。。。

写在最后

感谢大家的阅读,晴天将继续努力,分享更多有趣且实用的主题,如有错误和纰漏,欢迎给予指正。 更多文章敬请关注作者个人公众号 晴天码字
我们下期不见不散,to be continued…

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

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

相关文章

Azure 机器学习 - 无代码自动机器学习的预测需求

了解如何在 Azure 机器学习工作室中使用自动化机器学习在不编写任何代码行的情况下创建时序预测模型。 此模型将预测自行车共享服务的租赁需求。 关注TechLead,分享AI全维度知识。作者拥有10年互联网服务架构、AI产品研发经验、团队管理经验,同济本复旦硕…

牛客项目(五)-使用kafka实现发送系统通知

kafka入门以及与spring整合 Message.java import java.util.Date;public class Message {private int id;private int fromId;private int toId;private String conversationId;private String content;private int status;private Date createTime;public int getId() {retur…

Marp: 将 Markdown 变为 PPT 式样的 VScode 插件

样例代码&#xff1a; --- marp: true size: 16:9 theme: default header: footer: --- <!-- _footer: Jia ming<br>Gansu University of Political Science and Law --> <!-- _backgroundColor: lightskyblue --> ## <!-- fit --> 笔记检验概述>…

pytorch 中 nn.Conv2d 解释

1. pytorch nn.Con2d 中填充模式 torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride1, padding0, dilation1, groups1, biasTrue, padding_mode‘zeros’, deviceNone, dtypeNone) 1.1 padding 参数的含义 首先 &#xff0c;padd N, 代表的是 分别在 上下&…

鉴源实验室 | 自动驾驶传感器攻击研究

作者 | 付海涛 上海控安可信软件创新研究院汽车网络安全组 来源 | 鉴源实验室 社群 | 添加微信号“TICPShanghai”加入“上海控安51fusa安全社区” 01 自动驾驶汽车的脆弱性 自2015年以来&#xff0c;汽车的信息安全问题受到国内外的广泛关注。而随着汽车的智能化与网联化的…

上海物理、化学高考命题趋势及2024年上海物理、化学高考备考建议

在上海高考时&#xff0c;物理、化学虽然不像语文、英语和数学那样分数高&#xff0c;但是仍然很重要。那么&#xff0c;从这几年的上海物理、化学的高考题目来看&#xff0c;我们互发现什么命题趋势和考题特点呢&#xff1f;如何备考接下来的2024年高考物理和化学呢&#xff1…

Ubuntu下安装vscode,并解决终端打不开vscode的问题

Visual Studio Code安装 1&#xff0c;使用 apt 安装 Visual Studio Code 在官方的微软 Apt 源仓库中可用。按照下面的步骤进行即可&#xff1a; 以 sudo 用户身份运行下面的命令&#xff0c;更新软件包索引&#xff0c;并且安装依赖软件&#xff1a; sudo apt update sud…

uniapp 微信小程ios端键盘弹起后导致页面无法滚动

项目业务逻辑和出现的问题整理 新增页面 用户可以主动添加输入文本框 添加多了就会导致当前页面出现滚动条,这就导致ios端滚动页面的时候去点击输入框键盘抬起再关闭的时候去滚动页面发现页面滚动不了(偶尔出现),经过多次测试发现是键盘抬起的时候 主动向上滑动 100%出现这种问…

【Linux】进程的概念

文章目录 1. 基本概念2. 进程的描述3. 进程的一些基本操作3.1 查看进程3.2 结束进程3.3 通过系统调用获取进程标示符3.4 通过系统调用来创建子进程 4. 进程状态4.1 操作系统的进程状态4.2 Linux对于这些状态的处理方式 1. 基本概念 什么是进程&#xff1f; 在回答这个问题之前…

云安全—docker Deamon攻击面

0x00 前言 本篇文章主要是讲docker Deamon的原理以及docker Deamon攻击面相关的内容&#xff0c;属于抛砖引玉系列&#xff0c;如有不妥之处还请斧正。 0x01 docker Deamon 还是先来看一下docker Deamon的一些相关知识&#xff0c;依旧是采用问答的方式来进行。为了文章的整…

2023-11-04:用go语言,如果n = 1,打印 1*** 如果n = 2,打印 1*** 3*** 2*** 如果n = 3,打印

2023-11-04&#xff1a;用go语言&#xff0c;如果n 1&#xff0c;打印 1*** 如果n 2&#xff0c;打印 1***3*** 2*** 如果n 3&#xff0c;打印 1***3*** 2***4*** 5*** 6*** 如果n 4&#xff0c;打印 1***3*** 2***4*** 5*** 6***10** 9*** 8*** 7*** 输入…

Spring底层原理(六)

Spring底层原理(六) 本章内容 介绍AOP的实现方式、JDK代理的模拟实现与源码 AOP的实现方式 使用代理模式 jdk动态代理cglib动态代理 使用aspectj的编译器&#xff0c;该编译器会直接对字节码进行修改&#xff0c;可以实现静态方法增强 使用javaagent,在jvm option中指定-…

高匿IP有什么作用

在互联网的蓬勃发展中&#xff0c;IP地址作为网络通信的基础&#xff0c;一直扮演着举足轻重的角色。而在诸多IP地址中&#xff0c;高匿IP地址则是一种特殊类型&#xff0c;其作用和价值在某些特定场合下尤为突出。那么&#xff0c;高匿IP地址究竟有哪些用处呢&#xff1f; 首先…

[动态规划] (五) 路径问题: LeetCode 62.不同路径

[动态规划] (五) 路径问题: LeetCode 62.不同路径 文章目录 [动态规划] (五) 路径问题: LeetCode 62.不同路径题目解析解题思路状态表示状态转移方程初始化和填表返回值 代码实现总结 62. 不同路径 题目解析 (1) 机器人从左上角到右下角有多少方法 (2) 机器人只能向左或者向右…

掌握Maven和SpringBoot的灵活性:定制化lib目录和依赖范围

前言 在开发基于Maven和SpringBoot的项目时&#xff0c;我们经常会使用第三方库来满足需求。然而&#xff0c;有时候我们需要更灵活地控制这些库的依赖范围和加载方式。本文将介绍如何使用Maven和SpringBoot实现定制化的lib目录和依赖范围。经过如下定制化后&#xff0c;打包执…

[PyTorch][chapter 61][强化学习-免模型学习1]

前言&#xff1a; 在现实的学习任务中&#xff0c;环境 其中的转移概率P,奖赏函数R 是未知的&#xff0c;或者状态X也是未知的 称为免模型学习&#xff08;model-free learning&#xff09; 目录&#xff1a; 1: 蒙特卡洛强化学习 2&#xff1a;同策略-蒙特卡洛强化学习 3&am…

阿里云免费服务器

文章目录 最近的阿里云活动By the way在云服务器ECS上搭建个人网站正文补充:定期释放补充:不知道阿里云服务器的密码怎么办?成果补充&#xff1a;怎么找到实例操作的后台&#xff1f;补充&#xff1a;怎么查看服务器到期时间&#xff1f; 究竟白嫖了多少&#xff1f;最后&…

修复dinput8.dll文件的缺失,以及修复dinput8.dll文件时需要注意什么

dinput8.dll文件通常在使用大型游戏时容易出现dinput8.dll文件丢失的情况&#xff0c;今天这篇文章将要教大家修复dinput8.dll文件的缺失&#xff0c;同时在修复dinput8.dll文件时需要注意些什么&#xff1f;防止文件在修复的过程中出现其他的错误。 dinput8.dll是DirectInput库…

部署ELK

一、elasticsearch #拉取镜像 docker pull elasticsearch:7.12.1 #创建ELK docker网络 docker network create elk #启动ELK docker run -d --name es --net elk -P -e "discovery.typesingle-node" elasticsearch:7.12.1 #拷贝配置文件 docker cp es:/usr/share/el…

Visual Studio Code 常用快捷键大全

Visual Studio Code 常用快捷键大全 快捷键是编码过程中经常使用&#xff0c;且能够极大提升效率的部分&#xff0c;这里给大家介绍一些VS Code中非常有用的快捷键。 打开和关闭侧边栏 Mac — Command B Windows — Ctrl B Ubuntu — Ctrl B 选择单词 Mac — Command D …