1. 设计模式

1.1. 是什么

软件工程中,设计模式(design pattern)是对软件设计中普遍存在(反复出现)
的各种问题,所提出的解决方案。

  1. 设计模式是程序员在面对同类软件工程设计问题所总结出来的有用的经验,模式不是代码,而是某类问题的通用解决方案,设计模式(Design pattern)代表了最佳的实践。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。
  2. 设计模式的本质提高软件的维护性,通用性和扩展性,并降低软件的复杂度

1.2. 为什么

编写软件过程中,程序员面临着来自 耦合性,内聚性以及可维护性,可扩展性,重用性,灵活性 等多方面的挑战,设计模式是为了让程序 (软件),具有更好

  1. 代码重用性 (即:相同功能的代码,不用多次编写)
  2. 可读性 (即:编程规范性, 便于其他程序员的阅读和理解)
  3. 可扩展性 (即:当需要增加新的功能时,非常的方便,称为可维护)
  4. 可靠性 (即:当我们增加新的功能后,对原来的功能没有影响)
  5. 使程序呈现高内聚,低耦合的特性
    %%
    1620-🏡⭐️◼️设计模式的目的?高内聚 (结构上)、低耦合 (行为上);(3 易 2 可):易复用、易扩展、易维护;高可读、高可靠◼️⭐️-point-202301231620%%

1.3. 分类

设计模式分为三种类型,共 23 种

  1. 创建型模式:
  • 单例模式
  • 工厂方法模式
  • 抽象工厂模式
  • 原型模式
  • 建造者模式
  1. 结构型模式:
  • 代理模式
  • 适配器模式
  • 装饰者模式
  • 桥接模式
  • 外观模式
  • 组合模式
  • 享元模式
  1. 行为型模式:
  • 模板方法模式
  • 策略模式
  • 命令模式
  • 职责链模式
  • 状态模式
  • 观察者模式
  • 中介者模式
  • 迭代器模式
  • 访问者模式
  • 备忘录模式
  • 解释器模式
    %%
    1427-🏡⭐️◼️23 种设计模式有什么?◼️⭐️-point-202301231427%%

2. 七大设计原则

设计模式原则,其实就是程序员在编程时,应当遵守的原则,也是各种设计模
式的基础 (即:设计模式为什么这样设计的依据)

设计模式常用的七大原则有:

  1. 开闭原则
  2. 里氏替换原则
  3. 合成复用原则
  4. 依赖倒转 (倒置) 原则
  5. 接口隔离原则
  6. 迪米特法则
  7. 单一职责原则
    %%
    1432-🏡⭐️◼️七大设计原则:开闭原则、里氏代换原则、组合复用原则、依赖倒转原则、接口隔离、迪米特法则、单一职责原则◼️⭐️-point-202301231432%%

2.1. 开闭原则 (Open Closed Principle)

2.1.1. 是什么

  1. 开闭原则(Open Closed Principle)是编程中最基础、最重要的设计原则
  2. 一个软件实体如类,模块和函数应该对扩展开放 (对提供方),对修改关闭 (对使用)。用抽象构建框架,用实现扩展细节。
  3. 当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。
  4. 编程中遵循其它原则,以及使用设计模式的目的就是遵循开闭原则。

2.1.2. 为什么

对扩展开放,对修改关闭。在程序需要进行拓展的时候,不修改原有的代码,而实现一个热插拔的效果。简言之,是为了使程序的扩展性好,易于维护和升级。

想要达到这样的效果,我们需要使用接口和抽象类

因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保持软件架构的稳定。而软件中易变的细节可以从抽象派生来的实现类来进行扩展,当软件需要发生变化时,只需要根据需求重新派生一个实现类来扩展就可以了。

2.1.3. 案例

下面以 搜狗输入法 的皮肤为例介绍开闭原则的应用。

【例】搜狗输入法 的皮肤设计。

分析:搜狗输入法 的皮肤是输入法背景图片、窗口颜色和声音等元素的组合。用户可以根据自己的喜爱更换自己的输入法的皮肤,也可以从网上下载新的皮肤。这些皮肤有共同的特点,可以为其定义一个抽象类(AbstractSkin),而每个具体的皮肤(DefaultSpecificSkin 和 HeimaSpecificSkin)是其子类。用户窗体可以根据需要选择或者增加新的主题,而不需要修改原代码,所以它是满足开闭原则的。

2.1.4. 满足开闭原则的设计模式

[[内功心法专题-设计模式-0、设计模式总结#^8yg365]]

2.2. 里氏代换原则 (Liskov Substitution Principle)

2.2.1. 是什么

里氏代换原则是面向对象设计的基本原则之一。通常类的复用分为继承复用和合成复用两种。里氏代换原则就属于继承复用的复用类型

里氏代换原则:任何基类可以出现的地方,子类一定可以出现。通俗理解:子类可以扩展父类的功能,但不能改变父类原有的功能。换句话说,子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。

如果通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的概率会非常大。

子类可以在任意方法中,替代父类作为方法的入参

2.2.2. 为什么

  1. 继承包含这样一层含义:父类中凡是已经实现好的方法,实际上是在设定规范和契
    约,虽然它不强制要求所有的子类必须遵循这些契约,但是如果子类对这些已经实
    现的方法任意修改,就会对整个继承体系造成破坏
  2. 继承在给程序设计带来便利的同时,也带来了弊端。比如使用继承会给程序带来侵
    入性,程序的可移植性降低,增加对象间的耦合性,如果一个类被其他的类所继承,
    则当这个类需要修改时,必须考虑到所有的子类,并且父类修改后,所有涉及到子
    类的功能都有可能产生故障
  3. 问题提出:在编程中,如何正确的使用继承? => 里氏替换原则

2.2.3. 如何遵循里氏替换原则⭐️🔴

  1. 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法
  2. 子类中可以增加自己特有的方法。
  3. 当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
  4. 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
  5. 里氏替换原则告诉我们,继承实际上让两个类耦合性增强了,在适当的情况下,可
  6. 以通过聚合,组合,依赖 来解决问题。
  7. 其他方案:抽象出更高层的抽象类或者接口。

%%
1440-🏡⭐️◼️里氏代换原则是正确使用继承的法则:如果无法避免使用继承,那么尽量不要覆盖父类的方法。最好使用组合、聚合 + 构造导入、setter 导入的方式◼️⭐️-point-202301231440%%

2.3. 合成复用原则 (Composite Reuse Principle)

合成复用原则是指:尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。

通常类的复用分为继承复用和合成复用两种。

继承复用虽然有简单和易实现的优点,但它也存在以下缺点:

  1. 继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为“白箱”复用
  2. 子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展与维护。
  3. 它限制了复用的灵活性。从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化。

采用组合或聚合复用时,可以将已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已有对象的功能,它有以下优点:

  1. 它维持了类的封装性。因为成分对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用
  2. 对象间的耦合度低。可以在类的成员位置声明抽象。
  3. 复用的灵活性高。这种复用可以在运行时动态进行,新对象可以动态地引用与成分对象类型相同的对象。❕%%
    1443-🏡⭐️◼️白箱复用:继承复用,破坏了类的封装特性,将父类细节暴露给子类,带来变更风险
    黑箱复用:组合复用,被使用类的内部细节对使用类不可见,保护了封装性,不会让变更扩散◼️⭐️-point-202301231443%%

下面看一个例子来理解合成复用原则

【例】汽车分类管理程序

汽车按“动力源”划分可分为汽油汽车、电动汽车等;按“颜色”划分可分为白色汽车、黑色汽车和红色汽车等。如果同时考虑这两种分类,其组合就很多。类图如下:

image-20191229173554296

从上面类图我们可以看到使用继承复用产生了很多子类,如果现在又有新的动力源或者新的颜色的话,就需要再定义新的类。我们试着将继承复用改为聚合复用看一下。

image-20191229173554296

2.4. 依赖倒转原则 (Dependence Inversion Principle)

2.4.1. 是什么

依赖倒转原则 (Dependence Inversion Principle) 是指:
1) 高层模块不应该依赖低层模块,二者都应该依赖其抽象
2) 抽象不应该依赖细节,细节应该依赖抽象
3) 依赖倒转 (倒置) 的中心思想是面向接口编程
4) 依赖倒转原则是基于这样的设计理念:相对于细节的多变性,抽象的东西要稳定的
多。以抽象为基础搭建的架构比以细节为基础的架构要稳定的多。在 java 中,抽象
指的是接口或抽象类,细节就是具体的实现类
5) 使用接口或抽象类的目的是制定好规范,而不涉及任何具体的操作,把展现细节的
任务交给他们的实现类去完成
6) 依赖倒转原则可以看做是开闭原则的具体实践

2.4.2. 如何做⭐️🔴

依赖关系传递的三种方式

  1. 接口传递 ❌
  2. 构造方法传递
  3. setter 方式传递
    %%
    1445-🏡⭐️◼️依赖传递的 3 种方式:接口传递、构造方法传递、setter 方法传递,其中接口注入由于在灵活性和易用性比较差,现在从 Spring4 开始已被废弃。◼️⭐️-point-202301231445%%

依赖注入是时下最流行的 IOC 实现方式,依赖注入分为接口注入(Interface Injection),Setter 方法注入(Setter Injection)和构造器注入(Constructor Injection)三种方式。其中接口注入由于在灵活性和易用性比较差,现在从 Spring4 开始已被废弃。
构造器依赖注入:构造器依赖注入通过容器触发一个类的构造器来实现的,该类有一系列参数,每个参数代表一个对其他类的依赖。
Setter 方法注入:Setter 方法注入是容器通过调用无参构造器或无参 static 工厂 方法实例化 bean 之后,调用该 bean 的 setter 方法,即实现了基于 setter 的依赖注入。

2.4.3. 注意细节

  1. 低层模块尽量都要有抽象类或接口,或者两者都有,程序稳定性更好.
  2. 变量的声明类型尽量是抽象类或接口, 这样我们的变量引用和实际对象间,就存在
    一个缓冲层,利于程序扩展和优化
  3. 继承时遵循里氏替换原则

2.4.4. 案例

下面看一个例子来理解依赖倒转原则
【例】组装电脑

现要组装一台电脑,需要配件 cpu,硬盘,内存条。只有这些配置都有了,计算机才能正常的运行。选择 cpu 有很多选择,如 Intel,AMD 等,硬盘可以选择希捷,西数等,内存条可以选择金士顿,海盗船等。

类图如下:

上面代码可以看到已经组装了一台电脑,但是似乎组装的电脑的 cpu 只能是 Intel 的,内存条只能是金士顿的,硬盘只能是希捷的,这对用户肯定是不友好的,用户有了机箱肯定是想按照自己的喜好,选择自己喜欢的配件。

根据依赖倒转原则进行改进:

代码我们只需要修改 Computer 类,让 Computer 类依赖抽象(各个配件的接口),而不是依赖于各个组件具体的实现类。

类图如下:

image-20191229173554296
面向对象的开发很好的解决了这个问题,一般情况下抽象的变化概率很小,让用户程序依赖于抽象,实现的细节也依赖于抽象。即使实现细节不断变动,只要抽象不变,客户程序就不需要变化。这大大降低了客户程序与实现细节的耦合度。

2.5. 接口隔离原则 (Interface Segregation Principle)

客户端不应该被迫依赖于它不使用的方法;一个类对另一个类的依赖应该建立在最小的接口上

下面看一个例子来理解接口隔离原则

类 A 通过接口 Interface1 依赖类 B,类 C 通过接口 Interface1 依赖类 D,如果接口 Interface1 对于类 A 和类 C 来说不是最小接口,那么类 B 和类 D 必须去实现他们不需要的方法。

按隔离原则应当这样处理:
将接口 Interface1 拆分为独立的几个接口,类 A 和类 C 分别与他们需要的接口建立依赖
关系。也就是采用接口隔离原则


%%
1446-🏡⭐️◼️这个图与模板方法模式有点类似◼️⭐️-point-202301231446%%

2.6. 迪米特法则 (Demeter Principle)

2.6.1. 是什么

  1. 一个对象应该对其他对象保持最少的了解
  2. 类与类关系越密切,耦合度越大
  3. 迪米特法则 (Demeter Principle) 又叫最少知道原则即一个类对自己依赖的类知道的
    越少越好。也就是说,对于被依赖的类不管多么复杂,都尽量将逻辑封装在类的内
    部。对外除了提供的 public 方法,不对外泄露任何信息 ❕%%
    0846-🏡⭐️◼️迪米特法则的含义是什么?🔜📝最少知道原则,即一个类对自己所依赖的类知道的越少越好。被依赖的类不管有多复杂,都不要暴露细节给其他类,而应该封装好只对外提供 public 方法,如此才能减少耦合,提高设计的扩展性和复用性。◼️⭐️-point-202301280846%%
  4. 迪米特法则还有个更简单的定义:只与直接的朋友通信
  5. 直接的朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,
    我们就说这两个对象之间是朋友关系。耦合的方式很多,依赖,关联,组合,聚合
    等。其中,我们称出现在成员变量,方法参数,方法返回值中的类为直接的朋友
    出现在局部变量中的类不是直接的朋友。也就是说,陌生的类最好不要以局部变量
    的形式出现在类的内部如果出现则转移到方法入参中,交给自己的直接朋友来隔离变更%%
    1456-🏡⭐️◼️直接朋友是什么意思?指依赖关系中的成员变量、方法参数、方法返回值所引用的对象 (记忆方法:类的内容从下往下数),而非直接朋友指局部变量。如果一个方法中通过成员变量、方法参数或者方法返回值这种直接朋友带入了不认识类作为方法中的局部变量,那么就要将这些变量拿到直接朋友中封装起来◼️⭐️-point-202301231456%%

2.6.2. 案例

代码示例:
[[pages/002-schdule/001-Arch/001-Subject/013-DemoCode/DesignPattern/src/com/atguigu/principle/demeter/Demeter1.java]]

2.6.3. 注意

  • 1)迪米特法则的核心是降低类之间的耦合
  • 2)但是注意:由于每个类都减少了不必要的依赖,因此迪米特法则只是要求降低类间(对象间)耦合关系,并不是要求完全没有依赖关系

2.7. 单一职责原则 (Single Responsibility Principle)

2.7.1. 是什么

对类来说的,即一个类应该只负责一项职责。如类 A 负责两个不同职责:职责 1,职责 2。
当职责 1 需求变更而改变 A 时,可能造成职责 2 执行错误,所以需要将类 A 的粒度分解为 A1,A2

坚持类级别的单一职责原则,尽量少用方法级别的,否则后面很可能会面临拆分的麻烦

2.7.2. 为什么

  1. 降低类的复杂度,一个类只负责一项职责。
  2. 提高类的可读性,可维护性
  3. 降低变更引起的风险
  4. 通常情况下,我们应当遵守单一职责原则,只有逻辑足够简单,才可以在代码级违
    反单一职责原则;也只有在类中方法数量足够少的情况下,才可以下方到方法级别保持单一职责原则

3. 参考与感谢

3.1. 尚硅谷韩顺平

https://www.bilibili.com/video/BV1G4411c7N4?p=20&vd_source=c5b2d0d7bc377c0c35dbc251d95cf204
课件已下载:/Users/Enterprise/0003-Architecture/架构师之路/尚硅谷 - 设计模式
示例代码:/Users/taylor/Nutstore Files/Obsidian_data/pages/002-schdule/001-Arch/001-Subject/013-DemoCode/DesignPattern

3.2. 黑马程序员

https://www.bilibili.com/video/BV1Np4y1z7BU?p=35

课件已下载:/Volumes/Seagate Bas/001-ArchitectureRoad/资料 -java 设计模式(图解 + 框架源码分析 + 实战)
示例代码:/Users/taylor/Nutstore Files/Obsidian_data/pages/002-schdule/001-Arch/001-Subject/013-DemoCode/design_patterns

[[pages/002-schdule/001-Arch/001-Subject/009-内功心法专题/设计模式/黑马/设计模式]]

3.3. 网络笔记

笔记: https://www.yuque.com/u21195183/fnz31h
代码: https://github.com/vectorxxxx/NOTE_DesignPatterns
https://gitee.com/vectorx/NOTE_DesignPatterns