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

news2024/12/26 16:46:58

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

一文搞懂设计模式.png

问题引出

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

设计模式是什么

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

为什么要用设计模式

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


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

设计模式的七大原则

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

逐一介绍一下这六大法则

1.单一职责原则

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

代码示例如下:

张三写的初版代码

package main

import "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 main

import "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 main

import "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 main

import "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 main

import "fmt"

type People struct {
}

func (p *People) Drive() {
    fmt.Println("司机开车")
}

func (p *People) Fly() {
    fmt.Println("飞行员开飞机")
}

func main() {
    var p People
    p.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 main

import "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 *BMWCar
    var audi *AudiCar
    var z3 Zhang3
    var l4 Li4
    z3.DriveBMW(bmw)
    z3.DriveAudo(audi)
    l4.DriveBMW(bmw)
    l4.DriveAudo(audi)
}

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

依赖倒转.drawio.png

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

package main

import "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 Car
    car = new(Benz)
    // 抽象司机
    var driver Driver
    // 里氏替换原则,用具体实现类替换抽象类
    driver = new(zhang3)
    driver.Drive(car) // zhang3 开汽车  benz is running
    car = 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 main

import "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.coloradmin.cn/o/1171348.html

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

相关文章

上班族必备:制作电子宣传册的网站

​对于上班族来说,制作电子宣传册是一项非常重要的技能。因为宣传册是展示公司形象、产品特点、服务优势的重要工具,也是与客户沟通交流的重要手段。那么,如何制作一份高质量的电子宣传册呢?今天就为大家推荐几个制作电子宣传册的…

hackergame2023菜菜WP

文章目录 总结Hackergame2023更深更暗组委会模拟器猫咪小测标题HTTP集邮册Docker for everyone惜字如金 2.0Git? Git!高频率星球低带宽星球小型大语言模型星球旅行日记3.0JSON ⊂ YAML? 总结 最近看到科大在举办CTF比赛,刚好我学校也有可以参加,就玩了…

SQL左连接实战案例

要求:用表df1和表df2的数据,得到df3 一、创建表 CREATE TABLE df1 (姓名 varchar(255) DEFAULT NULL,年龄 int DEFAULT NULL,部门 varchar(255) DEFAULT NULL,id int DEFAULT NULL );CREATE TABLE df2 (部门 varchar(255) DEFAULT NULL,年龄 int DEFAU…

【LeetCode刷题-链表】--328.奇偶链表

328.奇偶链表 /*** Definition for singly-linked list.* public class ListNode {* int val;* ListNode next;* ListNode() {}* ListNode(int val) { this.val val; }* ListNode(int val, ListNode next) { this.val val; this.next next; }* }*/ clas…

【漏洞复现】Apache_Tomcat_PUT方法任意写文件(CVE-2017-12615)

感谢互联网提供分享知识与智慧,在法治的社会里,请遵守有关法律法规 文章目录 1.1、漏洞描述1.2、漏洞等级1.3、影响版本1.4、漏洞复现1、基础环境2、漏洞扫描3、漏洞验证工具扫描验证POC 1.6、修复建议 说明内容漏洞编号CVE-2017-12615漏洞名称Tomcat_PU…

python实现多帧torch.istft的结果可以由多个单帧torch.istft的结果重叠拼接得到

🔥 🔥 🔥 背景:做某个项目(由于项目处于保密状态,只提供思路),需要求多帧的istft。但是手头只有单帧的istft代码(当然不能python代码,不然就直接调包)。 📣 …

Quartus II 13.0波形仿真(解决无法产生仿真波形问题)

目录 前言 新建工程 创建Verilog文件,写代码 波形仿真(解决没有输出波问题) 前言 这么说把Quartus II 13.0是我目前来讲见过最恶心的软件,总是一大堆麻烦事,稍微哪里没弄好就后面全都出问题。很多人在写完Verilog代…

ke9案例三:页面提交文件,我服务器端接收

案例三:页面提交文件,我服务器端接收 ProcessFile.java 1value "/process-file" 2获取邮件消息的所有部分part--Collection<Part> partsrequest.getParts(); 3遍历每一个part 4之后可以打印头文件等String headerpart.getHeader("content-disposition&q…

配件管理系统软件哪家好?企业配件管理要怎么做?

在许多企业中&#xff0c;生产数据的记录仍然依赖于纸质流转卡&#xff0c;这种传统的方式带来了许多问题。手工填写导致的字迹潦草、数据不准确或不完全、统计困难等都是无法避免的问题。为了解决这些问题&#xff0c;我们推荐使用一款智能配件管理系统&#xff0c;帮助企业从…

Springboot搭建微服务案例之Eureka注册中心

一、父工程依赖管理 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation"http://maven.apache.org…

常见通信协议

博客内容&#xff1a;UART,IIC,SPI 文章目录 一、UART二、IIC三、SPI总结 一、UART 一种串行通信接口协议&#xff0c;用于在计算机系统和外部设备之间进行数据传输。UART可以支持异步和同步两种通信方式。在异步通信方式下&#xff0c;数据传输不需要在发送方和接收方之间进行…

自学SLAM(7)非线性优化实践:曲线拟合问题(使用ceres库和SLAM常用的g2o库)

前言 本次文章针对的是第四个视屏中的实践问题 肯定会有部分方法没有说到&#xff0c;比如高斯牛顿法&#xff0c;后面我会把此次视屏对应的作业写好&#xff0c;然后补充到此次博客&#xff01;&#xff01; 文章目录 前言1.曲线拟合题目&#xff1a;2.非线性最小二乘2.1 黄金…

网络基础扫盲-初识网络

博客内容&#xff1a;初识网络 文章目录 一、OSI七层网络模型二、TCP/IP四层模型1、MAC地址与IP地址 前言 在以前网络不够发之前&#xff0c;各个实验室进行一些研究时需要进行数据的交流&#xff0c;但是那时车马很慢&#xff0c;一生只够跑几次&#xff0c;所以就有人研究了网…

Reshape.XL 1.2 for Excel插件 Crack

特征 插件 Reshape.XL 包括 130 个基本可组合功能。使用它们&#xff0c;您可以快速轻松地进行非常复杂的数据转换和处理。它们的架构和基本定义受到 SQL 和 R 语言的强烈启发。 到目前为止&#xff0c;类似的功能只能通过脚本语言供程序员使用。借助 Reshape.XL 插件&#xf…

Pyhotn: Mac安装selenium和chromedriver-119

1.0 安装selenium 终端输入&#xff1a; pip install selenium 查看版本&#xff1a; pip show selenium2.0 安装chromedriver 查看chrome版本 网上大多数是&#xff0c;基本到114就停了。 https://registry.npmmirror.com/binary.html?pathchromedriver/ 各种搜索&#…

Java自学第4课:Java数组,类,对象

1 一维数组的创建和使用 2种创建形式&#xff1a; &#xff08;1&#xff09;先声明&#xff0c;再用new分配内存 &#xff08;2&#xff09;声明的同时分配内存 2种幅值形式 &#xff08;1&#xff09;用new{}赋值 &#xff08;2&#xff09;用{}赋值 如果不使用的话&a…

【jvm】虚拟机栈

目录 一、背景二、栈与堆三、声明周期四、作用五、特点&#xff08;优点&#xff09;六、可能出现的异常七、设置栈内存大小八、栈的存储单位九、栈运行原理十、栈帧的内部结构10.1 说明10.2 局部变量表10.3 操作数栈10.4 动态链接10.5 方法返回地址10.6 一些附加信息 十一、代…

【强化学习】16 ——PPO(Proximal Policy Optimization)

文章目录 前言TRPO的不足PPO特点 PPO-惩罚PPO-截断优势函数估计算法伪代码PPO 代码实践参考 前言 TRPO 算法在很多场景上的应用都很成功&#xff0c;但是我们也发现它的计算过程非常复杂&#xff0c;每一步更新的运算量非常大。于是&#xff0c;TRPO 算法的改进版——PPO 算法…

【PyQt学习篇 · ⑪】:QPushButton和QCommandLinkButton的使用

文章目录 构造函数菜单设置扁平化默认处理右键菜单QCommandLinkButton的使用 构造函数 QPushButton的构造函数如下&#xff1a; """QPushButton(parent: Optional[QWidget] None)QPushButton(text: Optional[str], parent: Optional[QWidget] None)QPushButt…

基于动力学模型的机械臂pid控制

参考资料&#xff1a; 一、如何实现机械臂的控制 在最常见的对机械臂动力学实现控制的问题中&#xff0c;我们会有一段机械臂末端的期望轨迹S&#xff0c;希望通过对机械臂关节处电机转矩的控制实现末端沿期望轨迹的完美运动。控制问题主要分为镇定和跟踪两种&#xff0c;上面…