设计模式

  • 设计模式概述
  • 面向对象概述
    • 单一职责原则
    • 开闭原则
    • 里氏代换原则
    • 依赖倒置原则
    • 组合复用原则
    • 接口隔离原则
    • 迪米特法则

设计模式概述

模式是在特定环境下人们解决某类重复出现问题的一套成功或有效的解决方案。

设计模式是在特定环境下为解决某一通用软件设计问题提供的一套定制的解决方案,该方案描述了对象和类之间的相互作用

设计模式是一套被反复使用、多数人知晓的、经过分类别的代码设计经验总结

目的:为了可重用代码、让代码更容易被他人理解、提高代码可靠

设计模式的组成:模式名称、问题、解决方案、效果

分类

  • 根据目的分类:
    • 创建型模式(创建对象)
    • 结构性模式(处理类和对象的组合)
    • 行为型模式(描述类和对象如何交互和怎么分配职责)
  • 根据范围分类:
    • 类模式(处理类和子类之间的关系)静态关系
    • 对象模式(处理对象之间的关系) 关系更具动态性

设计模式-分类

面向对象概述

面向对象优点:可维护、可复用、可扩展、灵活性高

面向对象编程特性:封装、继承、多态

衡量软件质量的属性:

  • 可维护性:指软件能够被理解、改正、适应及扩展的难易程度

  • 可复用性:指软件能够被重复使用的难易程度

目标:

  • 实现设计方案或者源代码的复用
  • 确保系统易于扩展和修改
  • 具有良好的可维护性

面向对象设计原则:

  • 单一职责原则

    一个对象应该只包含单一的职责,并且该职责被完整地封装在一个类中

  • 开闭原则(总原则)

    软件实体应当对扩展开放,对修改关闭

  • 里氏代换原则

    任何父类出现的地方,子类都可以替代出现

  • 依赖倒转原则

    要依赖于抽象,不要依赖于具体的实现。抽象不应该依赖于细节,细节应该依赖于抽象

  • 接口隔离原则

    客户端不应该依赖那些它不需要的接口

  • 合成复用原则

    优先使用对象组合,而不是继承来达到复用的目的

  • 迪米特法则

    又称最少知识原则,一个对象应当对其他对象尽可能少的了解,不和陌生人说话

单一职责原则

描述:

  • 一个对象应该只包含单一的职责,并且该职责被完整地封装在一个类中。
  • 就一个类而言,应该仅有一个引起它变化的原因

分析:

  • 一个类职责越多,复用的可能性就越小
  • 当一个职责变化时,可能导致对象状态变化,从而影响其他职责的工作。因此,应该将不同的职责封装到不同的类中
  • 单一职责原则是实现高内聚,低耦合的指导方针

好处:

  • 类的复杂性降低,职责明确
  • 可读性、可维护性高

使用场景:接口设计、类的设计、方法的设计

开闭原则(核心/总原则)

描述:

  • 软件实体应当对扩展开放,对修改关闭
  • 即开放-封闭原则,软件实体(类、模块、函数等)应该可以扩展,但是不能修改。

分析:

  • 软件实体可以是一个软件模块、一个由多个类组成的局部结构或一个独立的类

  • 开闭原则是指软件实体应尽量在不修改原有代码的情况下进行扩展,面对需求,对程序的改动是增加新代码,不是更改现有代码。

  • 稳定的抽象层,灵活的实现层

  • 找到系统可变的因素,将其封装起来

    设计人员必须对于他设计的模块应该对哪种变化封闭做出选择,必须先猜测出最有可能发生的变化种类,然后构造抽象来隔离那些变化。最初编写程序时假设变化不会发生,当变化发生时,就创建抽象来隔离以后发生的同类变化,拒绝不成熟的抽象。(接口???)

里氏代换原则

继承优点:

  • 代码共享,可以减少类的工作量
  • 提高代码复用
  • 子类可以对父类的方法进行重写
  • 提高代码的可扩展性

继承缺点:

  • 继承是入侵式
  • 降低代码的灵活性
  • 增强了耦合性
  • Java单一继承,C++多重继承

描述:

  • 如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有发生变化,那么类型S是类型T的子类型。
  • 所有引用基类的地方必须能透明地使用其子类的对象
  • 子类型必须能够替换掉它们的父类型。由于子类型的可替换性才使得使用父类类型的模块在无需修改的情况下就可以扩展。

分析:

  • 将一个基类对象替换成它的子类对象,程序将不会产生任何错误和异常

  • 尽量使用基类类型来对对象进行定义

  • 运用时,父类应该是抽象类或者接口,让子类继承父类或者实现父类

  • 无需修改原有类型的代码。易于通过新增的方式来扩展系统功能

  • 是对“开闭原则”的补充。实现“开闭原则”的关键步

    骤是抽象化,而基类不子类的继承关系就是抽象化的具体实

    现。

依赖倒置原则(面向接口编程)

描述:

  • 高层模块不应该依赖低层模块,它们都应该依赖抽象。抽象不应该依赖于细节,细节应该依赖于抽象。
  • 面向接口编程

分析:

  • 尽量在传递参数或者关联关系中,传递层次高的抽象类或者接口
  • 编程的过程中,尽量使用抽象层进行编程
  • 针对抽象层编程,具体类通过依赖注入的方式注入到其他对象。构造注入、设值注入(Setter)、接口注入

接口隔离原则

描述:

  • 客户端不应该依赖那些它不需要的接口。
  • 类间的依赖关系应该建立在最小的接口上。

分析:

  • 接口细化,如果接口内容过于庞杂,需要将其划分为更小的
  • 每一个接口承担相对独立的角色
  • 接口隔离 VS 单一职责
    • 单一职责更加重视类的实现上的单一,面向开发者,注重业务逻辑的职责划分
    • 接口隔离更多面向使用者,隔离要求接口方法尽可能精炼

接口隔离原则:

  • 接口要尽量小,不臃肿
  • 接口要高内聚,减少对外的交互
  • 定制服务。为每一类(某个接口)的使用者提供单独的服务
  • 接口的设计是有限度的。细粒度也并不是越小越好

合成复用原则

也成为,”组合\聚合原则“。

描述:

  • 优先使用对象组合,而不是通过继承来达到复用的目的

分析:

  • 合成复用原则就是在一个新对象里面通过关联关系或者组合关系来使用一些对象,使之成为新对象的一部分,达到复用的目的

  • 新对象通过委派调用已有对象的方法达到复用功能的目的

  • 复用时要尽量使用组合/聚合关系(关联关系),少用继承

  • 继承复用 VS 组合/聚合复用

    • 继承复用:实现简单,易于扩展。破坏系统的封装性;继承来的实现是静态的,不可能在运行时改变;没有足够的灵活性
    • 组合/聚合复用:耦合度相对较低,有选择性的调用成员对象的操作。

Coda法则;

  • 子类是基类的一个特殊种类(is-a),而不是基类的一个角色(has-a)
  • 永远不会出现永远不会出现将子类替换成另一个类的子类的情况
  • 子类具有扩展基类的责任,而不是去置换(Override)
  • 只有在分类学上有意义时,才可以使用继承

迪米特法则(最少知识原则)

描述:

  • 每一个软件单位对其他的单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位。

分析:

  • 每一个软件实体应该尽可能少的与其他实体发生相互作用
  • 一个对象应该对其他对象应该有最少的了解(接口隔离)
  • 一个类对其他自己需要耦合或者调用的类应该知道的尽量少
  • 应用迪米特法则可以降低系统的耦合度

UML类图

泛化(继承)

  • 关系:一种继承关系,表示一般与特殊的关系,子类如何特化父类的所有特征和行为

  • 箭头指向:带三角的箭头实线,指向父类

    image-20220511205535344

实现

  • 关系:类与接口的关系,表示类是接口所有特征和行为的实现。

  • 箭头指向:带三角箭头的虚线,箭头指向接口

    image-20220511210024413

关联

  • 关系:拥有关系,使一个类知道另一个类的属性和方法。

    如:老师与学生,丈夫与妻子关联可以是双向的,也可以是单向的。双向的关联可以有两个箭头或者没有箭头,单向的关联有一个箭头。

  • 箭头指向:带普通箭头的实线,指向被拥有者

UML类图几种关系的总结

​ 上图中,老师与学生是双向关联,老师有多名学生,学生也可能有多名老师。但学生与某课程间的关系为单向关联,一名学生可能要上多门课程,课程是个抽象的东西他不拥有学生。

​ 下图为自身关联:

UML类图几种关系的总结

聚合

  • 关系:整体与部分的关系,且部分可以离开整体而单独存在。如车和轮胎是整体和部分的关系,轮胎离开车仍然可以存在。
    • 属于关联关系的一种,是强的关联关系
  • 箭头指向:带空心菱形的实心线,菱形指向整体

UML类图几种关系的总结

组合

  • 关系:是整体与部分的关系,但部分不能离开整体而单独存在。如公司和部门是整体和部分的关系,没有公司就不存在部门。

    • 组合关系是关联关系的一种,是比聚合关系还要强的关系,它要求普通的聚合关系中代表整体的对象负责代表部分的对象的生命周期。
  • 箭头指向:带实心菱形的实线,菱形指向整体

UML类图几种关系的总结

依赖

  • 关系:使用的关系,一个类的实现需要另一个类的协助,所以要尽量不使用双向的互相依赖。
  • 箭头指向:带箭头的虚线,指向被使用者

UML类图几种关系的总结

​ 各种关系的强弱顺序:

​ 泛化 = 实现 > 组合 > 聚合 > 关联 > 依赖

创建型

对象的创建过程和使用过程分离,降低了系统的耦合度,使得系统更易于扩展。

关注对象创建过程,对类的实例化过程进行了抽象

更加符合单一职责原则

简单工厂(静态工厂)

专门定义一个类来负责创建其他类的实例,可以根据参数的不同返回不同类的实例,被创建的实例通常都具有共同的父类。

image-20220512202254892

分析:

  • 单一职责原则
  • 增加新类型时,只需要添加新的具体产品类并实现工厂方法
  • 工厂方法:静态方法,通过类名直接调用,只需要传入一个简单的参数

优点:

  • 对象创建和使用的分离
  • 客户端只需知道具体产品类所对应的参数
  • 一定程度上提高灵活性

缺点:

  • 职责过重,承担了所有产品的创建逻辑
  • 增加系统中类的个数
  • 扩展困难,一旦添加新产品就得修改工厂逻辑

适用环境:

  • 工厂类负责创建的对象比较少
  • 客户端只知道传入工厂类的参数,对如何创建对象不关心

工厂方法

定义: 工厂父类负责定义创建产品对象的公共接口**;工厂子类则负责生成具体的产品对象;这样做的目的是将产品类的实例化操作延迟到工厂子类中完成,通过工厂子类确定究竟应该实例化哪一个具体产品类

image-20220512213033199

分析:

  • 简单工厂模式的进一步抽象和推广
  • 保持简单工厂的优点,并克服了他的缺点
  • 将具体创建工作交给工厂子类完成
  • 可以在不修改工厂角色的情况下引进新产品
  • 增加具体产品—》增加具体工厂,符合开闭原则

优点:

  • 向客户隐藏了哪种具体产品类将被实例化这一细节
  • 让工厂自主确定创建何种产品对象
  • 开闭原则

缺点:

  • 类的个数成对增加,增大系统开销
  • 增加了系统的抽象性和理解难度

使用场景:

  • 客户不知道他所需要的对象的类
  • 抽象工厂类通过其子类来指定创建哪个对象

抽象工厂

动机:(需要一个工厂可以生产多个产品对象)

  • 产品等级结构(产品等级的继承结构)
  • 产品族(同一个工厂生产的,位于不同产品等级结构中的一组产品)

image-20220512214947510

定义:提供一个创建一系列相关或相互依赖对象的接口,而无须指定他们具体的类。 (Kit模式)

image-20220512215138408

优点:

  • 隔离了具体类的生成
  • 能够保证客户端始终只使用一个产品族中的对象
  • 增加新的产品族很方便,符合开闭原则

缺点:

  • 增加新的产品等级结构麻烦,违背开闭原则

应用场景:

  • 一个系统不应当依赖于产品类实例如何被创建、组合和表达的细节
  • 系统由多于一个产品族,但每次只使用其中某一产品族
  • 产品等级结构稳定

三种工厂对比

image-20220512222704502

建造者

定义:将一个复杂对象的构建与它的表示分离,使得同样的构建可以创建不同的表示

一步一步创建一个复杂的对象,它允许用户通过指定复杂对象的类型和内容就可以构建他们。

用户不需要知道复杂对象的内部组成部分与装配方式。

image-20220513152500583

优点:

  • 将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建不同的产品对象
  • 每一个具体建造者都相对独立,可以方便的替换掉具体建造者增加新的具体建造者,扩展方便,符合开闭院原则
  • 精细的控制产品的创建过程

缺点:

  • 如果产品之间的差异性很大,不适合使用建造者模式。
  • 如果产品的内部变化复杂,可能会需要定义很多具体建造者类来实现这种变化。

使用场景:

  • 需要生成的产品对象有复杂的内部结构
  • 需要生成的产品对象的属性相互依赖。
  • 对象的创建过程独立于创建该对象的类。
  • 隔离复杂对象的创建和使用。

原型

动机:复制一个对象,从而克隆出多个与原型对象一模一样的对象。

定义:用原型实例指定创建对象的种类,并且通过复制这些原型创建新的对象。

工作原理:将一个原型对象传给要发动创建的对象(即客户端对象),这个要发动创建的对象通过请求原型对象复制自己来实现创建过程

创建新对象(也称为克隆对象)的工厂就是原型类自身,工厂方法由负责复制原型对象的克隆方法来实现

通过克隆方法所创建的对象是全新的对象,它们在内存中拥有新的地址,每一个克隆对象都是独立

通过不同的方式对克隆对象进行修改以后,可以得到一系列相似但不完全相同的对象

image-20220513224322201

分析:

  • 能够实现克隆的java类必须实现一个标识接口Cloneable,表示这个java类支持复制

  • 如果没有实现这个接口但调用了clone()方法,java编译器将抛出一个CloneNotSuppertedException异常

  • 浅克隆:

image-20220513224748681

image-20220514170717815

  • 深克隆:

image-20220513224811007

image-20220514170651360

clone()方法满足以下几点。

(1)对任何的对象x,都有x.clone()!=x,即克隆对象与原对象不是同一个对象

(2)对任何的对象x,都有x.clone().getClass()==x.getClass(), 即克隆对象与原对象的类型一样

(3)如果对象x的equals()方法定义恰当,那么**x.clone().equals(x)**应该成立。

  • 在Java语言中,通过覆盖Object类的clone()方法可以实现浅克隆,如果需要实现深克隆,可以通过序列化等方式来实现
  • 在Java语言中,序列化(Serialization)就是将对象写到流的过程,写到流中的对象是原有对象的一个拷贝,而原对象仍然存在于内存中。通过序列化实现的拷贝不仅可以复制对象本身,而且可以复制其引用的成员对象,因此通过序列化将对象写到一个流中,再从流里将其读出来,从而实现深克隆。需要注意的是,能够实现序列化的对象其类必须实现Serializable接口,否则无法实现序列化操作。

Cloneable接口和serializable接口都是空接口,也称为标识接口,没有任何方法的定义。

优点

  • 简化对象创建过程,提高新实例的创建效率
  • 扩展性较好
  • 简化创建结构
  • 可以使用深克隆的方式保存对象的状态

缺点:

  • 需要为每一个类配备一个克隆方法,而且该克隆方法位于一个类的内部,当对已有的类进行改造时,需要修改源代码,违背了开闭原则
  • 实现深克隆时需要编写较为复杂的代码

应用场景:

  • 创建新对象成本较大
  • 对象的状态变化很小
  • 避免使用分层次的工厂类

扩展:

  • 原型管理器:是一个专门负责克隆对象的工厂,其中定义了一个Hashtable类型的集合用于存储原型对象。

image-20220514170620364

单例

动机:确保一个类只有一个实例

定义:确保一个类只有一个实例,而且自行实例化向整个系统提供这个实例。

image-20220514171000054

注:

  1. 单例类的构造函数为私有
  2. 提供一个自身的静态成员变量
  3. 提供一个公有静态工厂方法,返回唯一实例

优点

  • 提供了对唯一实例的受控访问
  • 节约系统资源,提高系统性能
  • 允许可变数目的实例(多例类)

image-20220514172335387

缺点:

  • 扩展困难(缺少抽象层)
  • 职责过重
  • 可能会导致共享的单例对象状态丢失

应用场景:

  • 系统只需要一个实例对象,或只允许创建一个对象
  • 只允许使用一个公共访问点

分类:

  • 懒汉式:只在你需要对象才会生成单例对象

需要处理好多个线程同时首次引用此类时的访问限制问题。———synchronized锁机制

image-20220514173548197

  • 饿汉式:在类被加载时就会实例化一个对象

image-20220514172856087

  • 比较:
    • image-20220514173656517
    • 饿:无需考虑多个线程同时访问的问题,调用速度和反应时间优于懒。
    • 懒:实现了延迟加载;锁机制导致系统性能受到影响

结构型

将不同的类和对象组合在一起,形成更大或者更复杂的结构体。提供这些类或对象的关联方式。

类结构型:关心类的组合,一般只存在继承实现关系。

对象结构型:关心类与对象的组合,通过关联关系在一个类中定义另一个类的实例对象,然后通过该对象调用相应的方法。

适配器

动机与定义:在系统中存在不兼容的接口,可以通过引入一个适配器来使得原本因为接口不兼容而不能一起工作的两个类可以协同工作。可以看作是对现有系统的补救以及对现有类进行重用的模式。

image-20220514200126229

分析:

  • 使用的前提时不能或不想修改原来的适配者接口和抽象目标类接口
  • 强调对代码的组织,而不是功能的实现。
  • 类适配器模式违背了合成复用原则

优点

  • 将目标类和适配器类解耦
  • 增加了类的透明性和复用性
  • 灵活性和扩展性非常好

类适配器模式:由于继承关系,置换一些适配者的方法很方便

image-20220514201803616

对象适配器模式:可以报多个不同的适配者适配到同一个目标,还可以适配一个适配者的子类

image-20220514201818660

缺点:

  • 类适配器
    1. 一次最多只能适配一个适配者类,不能同时适配多个
    2. 适配者类不能为最终类(final)
    3. 目标抽象类只能为接口,不能为类
  • 对象适配器:在适配器中置换适配者类的某些方法比较麻烦。

应用场景:

  • 需要使用一些现有的类,而这些类的接口不符合系统的需要
  • 创建一个可以重复使用的类

缺省适配器:不需要实现一个接口所提供的所有方法时,可以先设计一个抽象类实现该接口,并为接口每个方法提供一个默认实现,那么该抽象的子类可以选择性的覆盖父类的某些方法来实现需求。

image-20220514202726655

桥接

定义:

  • 将抽象部分与他的实现部分分离,使他们都可以独立地变化。
  • 用抽象关联取代了传统的多层继承
  • 类之间的静态继承关系转换为动态的对象组合关系

image-20220514204809631

分析

  • 抽象化:将对象的共同性质抽取出来形成类的过程

  • 实现化:针对抽象化给出的具体实现

  • 脱藕:

    • 将抽象化和实现化之间的耦合解开,也就是说将强关联该换为弱关联,将两个角色之间的继承关系改为关联关系。

    • 继承是强耦合

    • 桥接模式中的脱藕是指一个系统中的抽象化和实现化之间使用关联关系而不是继承关系

优点:

  • 分离抽象接口及其实现部分
  • 取代多层继承方案,极大的减少了子类的个数
  • 提高了系统的扩展性,符合开闭
  • 实现细节对客户透明,对用户隐藏实现细节

缺点:

  • 会增加系统的理解与设计难度
  • 正确识别出系统中两个独立变化的维度并不是一件容易的事情

应用场景:

  • 避免在两个层次之间简历静态的继承关系
  • 抽象部分和实现部分可以以继承的方式独立扩展而互不影响
  • 一个类存在两个或多个独立变化的维度,且这些维度都需要独立扩展
  • 不希望使用继承或因为多层继承导致系统类的个数急剧增加的系统

组合

动机:

  • 在树形目录结构中,包含文件和文件夹两类不同的元素
    • 文件夹下可以包含文件,或子文件夹
    • 文件中不能在包含子文件或子文件夹
  • 文件夹 《=》容器
  • 文件《=》叶子
  • 将容器对象和叶子对象进行递归组合

定义:组合多个对象形成树形结构以表示“部分-整体”的结构层次。对单个对象(叶子对象)和组合对象(容器对象)的使用具有一致性

image-20220515223903776

image-20220515224353509

优点:

  • 清楚定义分层次的复杂对象,让客户端忽略了层次的差异
  • 客户端可以一致的使用一个组合结构或者其中单个对象,简化了客户端代码
  • 增加新的容器构件和叶子构建都很方便,符合开闭
  • 为树形结构的面向对象实现提供了一种灵活的解决方案。

缺点:

  • 设计更加抽象,对象业务复杂,实现具有较大困难
  • 增加新构件时很难对容器中的构件类型进行限制

应用场景:

  • 具有整体和部分的层次结构,客户端可以一致对待他们
  • 使用面向对象语言开发的系统中需要处理一个树形结构
  • 一个系统中可以分离出叶子对象和容器对象

透明组合模式:

image-20220515225301336

抽象构件Component中声明了所有用于管理成员对象的方法,包括add()、remove(),以及getChild()等方法

优点:确保所有的构件都有相同的接口

缺点:不安全

安全组合模式:

image-20220515225358986

抽象构件Component中没有声明任何用于管理成员对象的方法,而是在Composite类中声明并实现这些方法

优点:安全

缺点:不够透明,客户端丌能完全针对抽象编程,必须有区别地对待叶子构件和容器构件

使用频率更高

装饰

动机

  • 在不改变一个对象本身功能的基础上给对象增加额外的新行为。

  • 关联关系取代类之间的继承的关系,无须定义子类的方式给对象动态增加职责

  • 引入了装饰类,在装饰类中既可以调用待装饰的原有类方法,也可以增加新的方法

定义:

  • 动态的给一个对象增加一些额外的职责
  • 对客户透明的方式动态的给一个对象附加上更多的职责
  • 在不需要创建更多子类的情况下,让对象的功能得以扩展

image-20220516093308631

image-20220516093716167

透明装饰器模式:

  • 客户端完全针对抽象编程,客户端对象声明因该全部声明为抽象构件类型

  • 是客户端透明的使用装饰之前的对象和装饰之后的对象。

  • 可以对一个已装饰过的对象进行多次装饰

  • 无法在客户端单独调用新增方法adddedBehavior()

半透明装饰模式:

  • 具体构件使用抽象构件类型来定义 (对客户端透明)
  • 用具体装饰类型来定义装饰之后的对象(对客户端不透明)
  • 客户端可以单独调用addedBehavior()方法
  • 缺点:不能实现对一个对象的多次装饰,客户端需要有区别的对待装饰之前的对象和装饰之后的对象

优点:

  • 装饰模式比继承更加灵活,不会导致类的个数急剧增加
  • 通过一种动态的方式来扩展一个对象的功能
  • 多次装饰
  • 具体构件类和具体装饰类可以独立变化,用户可根据需要增加新的具体构件类和具体装饰类,符合开闭原则

缺点:

  • 产生很多小对象,一定程度上影响性能
  • 比继承更加易于出错,排错也更加困难

应用场景:

  • 以动态的、透明的方式给单个对象添加职责
  • 不能采用继承的方式对系统进行扩展或者采用继承不利于系统扩展和维护时

外观

门面模式,为复杂的子系统调用提供一个统一的入口

引入外观类,降低了系统的耦合度功能;

外观类将客户类与子系统的内部复杂性分隔开

定义:

  • 外部与子部的通信通过一个统一的外观对对象进行,为子系统中的一组接口提供一个统一的入口。
  • 定义了一个高层接口,使得子系统更加容易使用

image-20220516105149528

分析

  • 迪米特法则的一种具体实现
  • 引入新的外观角色来降低原有系统的复杂度,同时降低客户类与子系统的耦合度

优点:

  • 减少了客户端所需处理的对象数目
  • 实现了子系统与客户端之间的松耦合关系
  • 一个子系统的修改对其他子系统没有任何影响,而且子系统的内部变化也不会影响到外观对象

缺点:

  • 不能很好地限制客户端直接使用子系统类
  • 增加新的子系统需要修改外观类的源代码,违背了开闭原则

应用场景:

  • 为访问一系列复杂的子系统提供一个简单入口
  • 客户端程序与多个子系统之间存在很大的依赖性
  • 在层次化结构中,可以使用外观模式定义系统中每一层的入口,层与层之间不直接产生联系,而是通过外观类建立联系,降低层之间的耦合度

抽象外观类:

解决:增加、删除或更换与外观类交互的子系统类,必须修改外观类或客户端的源代码的问题

代理

动机:

引入一个新对象来实现对真实对象的操作;

引入代理对象来间接访问一个对象

定义:

  • 给某个对象提供一个代理,并且由代理对象控制对原有对象的引用
  • 代理对象在客户端和目标对象之间起到中介的作用
  • 通过代理对象去掉客户不能看到的内容和服务或者添加客户需要的额外的新服务

image-20220516171734765

几种常用的代理模式:

  • 远程代理:为一个位于丌同的地址空间的对象提供一个本地的代理对象
  • 虚拟代理:如果需要创建一个资源消耗较大的对象,先创建一个消耗相对较小的对象来表示,真实对象只在需要时才会被真正创建
  • 保护代理(Protect Proxy):控制对一个对象的访问,可以给不同的用户提供不同级别的使用权限
  • 缓冲代理(Cache Proxy):为某一个目标操作的结果提供临时的存储空间,以便多个客户端可以共享这些结果
  • 智能引用代理(Smart Reference Proxy):当一个对象被引用时,提供一些额外的操作,例如将对象被调用的次数记录下来等

优点:

  • 能够协调调用者和被调用者,一定程度上降低了系统的耦合度
  • 增加和更换代理类无须修改源代码,符合开闭原则
  • 有些类型的代理模式可能会造成请求的处理速度变慢(例如:保护代理
  • 有些代理模式的实现过程较为复杂(例如:远程代理

应用场景:

  • Copy-on-Write代理:它是虚拟代理的一种,把复制(克隆)操作延迟到只有在客户端真正需要时才执行。一般来说,对象的深克隆是一个开销较大的操作。Copy-on-Write代理可以让这个操作延迟.只有对象被用到的时候才被克隆。

  • 防火墙(Firewall)代理:保护目标丌让恶意用户接近。

  • 同步化(Synchronization)代理:使几个用户能够同时使用一个对象而没有冲突

动态代理: 动态的创建代理类,让同一个代理类能够代理多个不同的真实主题类而且可以代理不同的方法。

Java语言实现动态代理时需要用到位于java.lang.reflect包中的一些类

行为型

定义了系统中对象之间的交互与通信,研究系统在运行时对象之间的相互通信与协作,进一步明确对象的职责

类行为模式:使用继承关系在几个类之间分配行为

对象行为模式:使用对象的聚合关联关系来分配行为。

命令

动机:

  • 将请求发送者和接收者完全解耦
  • 发送者与接收者之间没有直接引用关系
  • 发送请求的对象只需要知道如何发送请求,而丌必知道如何完成请求

定义:

  • 将一个请求封装为一个对象,从而使我们可以用不同的请求对客户进行参数化;对请求排队或者记录请求日志,以及支持可撤销的操作

image-20220516191700541

优点:

  • 降低代码耦合度
  • 符合开闭
  • 比较容易的设计一个命令队列或宏命令(组合命令)
  • 为请求的撤销和恢复操作提供了一种设计和实现方案

缺点:

  • 导致某些系统拥有过多的具体命令类

应用场景:

  • 请求调用者和请求接收者解耦
  • 在不同的时间指定请求、将请求排队和执行请求
  • 支持命令的撤销操作和恢复操作
  • 宏命令

image-20220516195122771

迭代器

定义:

  • 提供一种方法来访问聚合对象,而不用暴露这个对象的内部表示
  • 别名:“游标”

image-20220516201309166

优点:

  • 支持以不同方式遍历一个聚合对象,在同一个聚合对象上可以定义多种遍历方式
  • 简化了聚合类
  • 由于引入了抽象层,增加新的聚合类和迭代器都很方便,符合开闭

缺点:

  • 类的个数成对增加,增加了系统复杂性
  • 抽象迭代器设计难度大,需要充分考虑系统将来的扩展
  • 创建一个考虑全面的抽象迭代器不容易

应用场景:

  • 要访问一个聚合对象的内容而无法暴露他的内部表示
  • 为一个聚合对象提供多种遍历方式
  • 为遍历不同聚合结构提供一个统一的接口,客户端可以一致性的操作该接口

观察者(发布-订阅)

定义:定义对象间的一种一对多依赖关系,使得每当一个对象状态发生变化时,其相关依赖对象都能得到通知并自动更新

image-20220516211916638

image-20220516213337259

优点:

  • 实现表示层和数据逻辑层的分离
  • 在观察目标和观察者之间建立一个抽象的耦合
  • 支持广播通信
  • 符合开闭原则

缺点:

  • 花费很多时间
  • 存在循环依赖时,可能导致系统崩溃
  • 没有相应机制让观察者知道所观察者的目标对象是怎么变化,只是知道观察者目标发生了变化

应用场景:

  • 一方面依赖于另一方面
  • 一个对象的改变导致多个对象发生变化,并且不知道具体有多少个对象发生改变,也不知道对象是谁
  • 需要在系统中创建一个触发链

状态

动机:

  • 有些对象具有多种状态
  • 这些状态在某些情况下能够相互转换
  • 对象在不同的状态下将具有不同的行为

定义:

  • 别名:对象行为模型
  • 允许对、一个对象在其内部状态改变时改变他的行为,对象看起来似乎修改了他的类

image-20220601173337931

分析:

  • 解决系统中 复杂对象的状态转换以及不同状态下行为的封装问题
  • 将一个对象状态从该对象中分离出来,封装到专门的状态类中
  • 对于客户端,无须关心对象状态的转换以及对象所处的当前状态

优点:

  • 封装了状态的转换规则 ,可以对状态转换代码迚行集中管理
  • 将所有与某个状态有关的行为放到一个类中
  • 允许状态转换逻辑与状态对象合成一体,而不是提供一个巨大的条件语句块
  • 可以让多个环境对象共享一个状态对象,减少系统中对象的个数

缺点:

  • 增加系统中类和对象的个数
  • 结构与实现复杂,使用不当将导致程序结构和代码混乱,增加系统设计难度
  • 对开闭原则支持不好

使用场景:

  • 对象的行为依赖于他的状态。状态的改变导致行为的变化
  • 在代码中包含大量与对象状态有关的条件语句

策略

动机:实现目标的途径很多的时候,就需要根据实际情况选择更加合适的途径。使用硬编码实现将导致系统违背开闭原则,扩展困难,维护困难

定义:定义一系列算法,将每一个算法封装到策略类中,并且让他们可以相互替换

类图:

策略模式

分析:

  • 算法可以独立于使用它的客户而变化
  • 策略模式提供了一种可插入式(Pluggable)算法的实现方案

优点:

  • 对开闭原则的完美支持
  • 提供了管理相关算法的方案
  • 提供了一种可以替换继承关系的方案
  • 可以避免多重条件选择
  • 提供了一种算法的复用机制,在不同的环境类可以方便地复用策略类

缺点:

  • 客户端必须知道所有的策略类
  • 将造成系统产生大量策略类
  • 无法在客户端同时使用多个策略类

情景:

  • 动态选择算法中的一种
  • 避免难以维护的多重条件选择语句
  • 提高算法的保密性和安全性