设计模式简介

什么是设计模式?

  • 在软件工程中,设计模式(design pattern)是对软件设计中普遍存在(反复出现)的各种问题,所提出的解决方案。这个术语是由埃里希·伽玛(Erich Gamma)等人在 1990 年代从建筑设计领域引入到计算机科学的

设计模式的目的?

  • 编写软件过程中,程序员面临着来自耦合性,内聚性以及可维护性,可扩展性,重用性,灵活性等多方面的挑战,设计模式是为了让程序(软件),具有更好的
    • 代码重用性 (即:相同功能的代码,不用多次编写)
    • 可读性 (即:编程规范性, 便于其他程序员的阅读和理解)
    • 可扩展性 (即:当需要增加新的功能时,非常的方便,称为可维护)
    • 可靠性 (即:当我们增加新的功能后,对原来的功能没有影响)
    • 使程序呈现高内聚,低耦合的特性

设计模式包含了面向对象的精髓,“懂了设计模式,你就懂了面向对象分析和设计(OOA/D)的精要”


类之间的关系

关联关系

关联关系是一种结构化关系,用于表示一类对象与另一类对象之间有联系。

  • 用实线连接有关联的对象所对应的类

单向关联:仅一方拥有另一方的引用;

双向关联:关联关系默认为双向关联,双方互相引用对方;

自关联:关联参与者是同一个类;

聚合关系

成员类是整体类的一部分,即成员对象是整体对象的一部分,但是成员对象可以脱离整体对象独立存在。

  • 在聚合关系中,在UML中,聚合关系用带空心菱形的直线表示。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Car{
private Engine engine;
public Car(Engine engine) { //构造注入
this.engine = engine;
}

public void setEngine(Engine engine) { //设值注入
this.engine = engine;
} ……
}
public class Engine{
……
}

组合关系

整体对象可以控制成员对象的生命周期,一旦整体对象不存在,成员对象也将不存在。

  • 在UML中,组合关系用带实心菱形的直线表示。

1
2
3
4
5
6
7
8
9
10
11
public class Head{
private Mouth mouth;
public Head(){
mouth = new Mouth(); //实例化成员类
}
……
}
public class Mouth{
……
}

依赖关系

依赖关系(Dependency)是一种使用关系,当特定事物的改变有可能会影响到使用该事物的其他事物,或者说一个事物使用另一个事物

时,两者之间就存在依赖关系。

  • 依赖关系用带箭头的虚线表示,由依赖的一方指向被依赖的一方。

泛化关系

即继承关系,也称为“is-a-kind-of”关系,用于描述父类与子类之间的关系。

  • 在UML中,泛化关系用带空心三角形的直线来表示。

接口与实现关系

当一个类实现了某个接口的所有方法时,就称该类与接口之间存在实现关系。

  • 在UML中,类与接口之间的实现关系用带空心三角形的虚线来表示。

设计模式的七大原则

  • 设计模式原则,其实就是程序员在编程时,应当遵守的原则,也是各种设计模式的基础(即:设计模式为什么 这样设计的依据)
      1. 单一职责原则
      1. 接口隔离原则
      1. 依赖倒转(倒置)原则
      1. 里氏替换原则
      1. 开闭原则
      1. 迪米特法则
      1. 合成复用原则

单一责任原则

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

  • 注意事项:

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

接口隔离原则

  • 客户端不应该依赖它不需要的接口,即一个类对另一个类的依赖应该建立在最小的接口上

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

依赖倒转原则

  • 高层模块不应该依赖低层模块,二者都应该依赖其抽象

  • 抽象不应该依赖细节,细节应该依赖抽象

  • 依赖倒转(倒置)的中心思想是面向接口编程

  • 依赖倒转原则是基于这样的设计理念:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建的架构比以细节为基础的架构要稳定的多。在 java 中,抽象指的是接口或抽象类,细节就是具体的实现类

  • 使用接口或抽象类的目的是制定好规范,而不涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成

依赖传递

    1. 接口传递
    1. 构造方法传递
    1. setter 方式传递

注意事项

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

里氏替换原则

  • 对继承的思考

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

    • 里氏替换原则(Liskov Substitution Principle)在 1988 年,由麻省理工学院的以为姓里的女士提出的。
    • 如果对每个类型为 T1 的对象 o1,都有类型为 T2 的对象 o2,使得以 T1 定义的所有程序 P 在所有的对象 o1 都 代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型。换句话说,所有引用基类的地方必须能透明地使用其子类的对象
    • 在使用继承时,遵循里氏替换原则,在子类中尽量不要重写父类的方法
    • 里氏替换原则告诉我们,继承实际上让两个类耦合性增强了,在适当的情况下,可以通过聚合,组合,依赖来解决问题。.

通用的做法是:原来的父类和子类都继承一个更通俗的基类,原有的继承关系去掉,采用依赖,聚合,组合等 关系代替

开闭原则

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

迪米特法则

  • 一个对象应该对其他对象保持最少的了解

  • 类与类关系越密切,耦合度越大

  • 迪米特法则(Demeter Principle)又叫最少知道原则,即一个类对自己依赖的类知道的越少越好。也就是说,对于 被依赖的类不管多么复杂,都尽量将逻辑封装在类的内部。对外除了提供的 public 方法,不对外泄露任何信息

  • 迪米特法则还有个更简单的定义:只与直接的朋友通信

  • 直接的朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间 是朋友关系。耦合的方式很多,依赖,关联,组合,聚合等。其中,我们称出现成员变量,方法参数,方法返 回值中的类为直接的朋友,而出现在局部变量中的类不是直接的朋友。也就是说,陌生的类最好不要以局部变 量的形式出现在类的内部

  • 迪米特法则的核心是降低类之间的耦合

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

合成复用原则

  • 原则是尽量使用合成/聚合的方式,而不是使用继承

总结

核心思想:

  • 找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起。
  • 针对接口编程,而不是针对实现编程。
  • 为了交互对象之间的松耦合设计而努力

单例模式

  • 所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例, 并且该类只提供一个取得其对象实例的方法(静态方法)。

饿汉式(静态常量)

实现方法

    1. 构造器私有化 (防止 new )
    1. 类的内部创建对象
    1. 向外暴露一个静态的公共方法 getInstance()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* 单例模式---饿汉式
* @Author Maple
* @Date 2022/7/3 10:08
* @Version 1.0
*/
public class SingletonByEager {
//1.构造器私有化
private SingletonByEager(){

}
//2.类内部创建静态常量
private static final SingletonByEager instance=new SingletonByEager();
//3.暴露公有接口
public static SingletonByEager getInstance(){
return instance;
}

public static void main(String[] args) {
SingletonByEager instance1 = SingletonByEager.getInstance();
SingletonByEager instance2 = SingletonByEager.getInstance();

System.out.println(instance1==instance2);

System.out.println(instance1+"hashcode:"+instance1.hashCode());
System.out.println(instance2+"hashcode:"+instance2.hashCode());
}

}

优缺点分析

  • 优点:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题
  • 缺点:在类装载的时候就完成实例化,没有达到 Lazy Loading 的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费
  • 这种方式基于 classloder 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,在单例模式中大 多数都是调用 getInstance 方法,但是导致类装载的原因有很多种,因此不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 就没有达到 lazy loading 的效果
  • 结论:这种单例模式可用,可能造成内存浪费

还有一种是在静态代码块进行初始化对象,与此方法等价

懒汉式

线程不安全

  • 在使用时才创建对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* 单例模式----懒汉式
* @Author Maple
* @Date 2022/7/3 10:24
* @Version 1.0
*/
public class SingletonByLazy {
//构造器私有化
private SingletonByLazy(){

}
//2.私有化定义对象
private static SingletonByLazy instance;
//3.暴露对外接口
public static SingletonByLazy getInstance(){
//只有在被调用的时候才创建对象
if (instance==null){
instance=new SingletonByLazy();
}
return instance;
}

public static void main(String[] args) {
SingletonByLazy instance1 = SingletonByLazy.getInstance();
SingletonByLazy instance2 = SingletonByLazy.getInstance();

System.out.println(instance1==instance2);
}
}

优缺点分析

  • 起到了 Lazy Loading 的效果,但是只能在单线程下使用
  • 如果在多线程下,一个线程进入了 if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过 了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式
  • 结论:在实际开发中,不要使用这种方式

线程安全

  • 解决方案,在返回实例的方法中加入同步锁
1
2
3
4
5
6
public static synchronized SingletonByLazy getInstance(){
if (instance==null){
instance=new SingletonByLazy();
}
return instance;
}
  • 解决了线程安全问题
  • 效率太低了,每个线程在想获得类的实例时候,执行 getInstance()方法都要进行同步。而其实这个方法只执行 一次实例化代码就够了,后面的想获得该类实例,直接 return 就行了。方法进行同步效率太低
  • 结论:在实际开发中,不推荐使用这种方式

双重检测

  • 变量用volatile修饰
  • 返回实例方法进行双重判断
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class SingletonByLazy {
//构造器私有化
private SingletonByLazy(){

}
//2.私有化定义对象
private static volatile SingletonByLazy instance; //volatile 防止指令重排
//3.暴露对外接口
public static SingletonByLazy getInstance(){
//只有在被调用的时候才创建对象
if (instance==null){
//此时可能有多个线程进来,它们拿到的instance可能都为null
synchronized (SingletonByLazy.class){
//此处必定只有一个线程
if (instance==null){
instance=new SingletonByLazy();
}
}
}
return instance;
}

public static void main(String[] args) {
SingletonByLazy instance1 = SingletonByLazy.getInstance();
SingletonByLazy instance2 = SingletonByLazy.getInstance();

System.out.println(instance1==instance2);
}
}

优缺点分析

  • Double-Check 概念是多线程开发中常使用到的,如代码中所示,我们进行了两次 if (singleton == null)检查,这 样就可以保证线程安全
  • 这样,实例化代码只用执行一次,后面再次访问时,判断 if (singleton == null),直接 return 实例化对象,也避免的反复进行方法同步.
  • 线程安全;延迟加载;效率较高
  • 结论:在实际开发中,推荐使用这种单例设计模式

静态内部类实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.me.design.singleton;

/**
* 通过静态内部类实现
* @Author Maple
* @Date 2022/7/3 11:00
* @Version 1.0
*/
public class SingletonByLazy2 {
//1.构造器私有化
private SingletonByLazy2(){

}
//2.创建静态内部类
private static class SingletonInner{
private static final SingletonByLazy2 INSTANCE=new SingletonByLazy2();
}
//3.暴露公有接口
public static SingletonByLazy2 getInstance(){
return SingletonInner.INSTANCE;
}

public static void main(String[] args) {
SingletonByLazy2 instance1 = SingletonByLazy2.getInstance();
SingletonByLazy2 instance2 = SingletonByLazy2.getInstance();
System.out.println(instance1==instance2);
}
}

优缺点分析

  • 这种方式采用了类装载的机制来保证初始化实例时只有一个线程。
  • 静态内部类方式在 Singleton 类被装载时并不会立即实例化,而是在需要实例化时,调用 getInstance 方法,才 会装载 SingletonInstance 类,从而完成 Singleton 的实例化。
  • 类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM 帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的
  • 优点:避免了线程不安全,利用静态内部类特点实现延迟加载,效率高
  • 结论:推荐使用

总结

  • 单例模式保证了 系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使 用单例模式可以提高系统性能
  • 当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使用 new
  • 单例模式使用的场景:需要频繁的进行创建和销毁的对象、创建对象时耗时过多或耗费资源过多(即:重量级对象),但又经常用到的对象、工具类对象、频繁访问数据库或文件的对象(比如数据源、session 工厂等

工厂模式

  • 分为三类
    • 简单工厂模式
    • 工厂方法模式
    • 抽象工厂模式

简单工厂模式

简单工厂模式是属于创建型模式,是工厂模式的一种。简单工厂模式是由一个工厂对象决定创建出哪一种产品类的实例。简单工厂模式是工厂模式家族中最简单实用的模式

  • 简单工厂模式:定义了一个创建对象的类,由这个类来封装实例化对象的行为
  • 在软件开发中,当我们会用到大量的创建某种、某类或者某批对象时,就会使用到工厂模式

工厂方法模式

  • 定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个。工厂方法让类把实例化推迟到子类。

抽象工厂模式

  • 定义了一个接口用于创建相关或有依赖关系的对象簇,而无需指明具体的类
  • 抽象工厂模式可以将简单工厂模式和工厂方法模式进行整合。
  • 从设计层面看,抽象工厂模式就是对简单工厂模式的改进(或者称为进一步的抽象)。
  • 将工厂抽象成两层,AbsFactory(抽象工厂) 和 具体实现的工厂子类。程序员可以根据创建对象类型使用对应的工厂子类。这样将单个的简单工厂类变成了工厂簇,更利于代码的维护和扩展。

总结

  • 简单工厂只有一个具体工厂类来创建一种基类的多个不同派生类,
  • 工厂方法就是有多个派生于一个基类的具体工厂类,每个具体工厂只生产一种基类的一个派生类
  • 抽象工厂也是只有一个工厂基类,但是每个具体工厂生产多个相关基类的各一个派生类

工厂模式的意义:将实例化对象的代码提取出来,放到一个类中统一管理和维护,达到和主项目的依赖关系的解耦。从而提高项目的扩展和维护性。


原型模式

  • 原型模式(Prototype 模式)是指:用原型实例指定创建对象的种类,并且通过拷贝这些原型,创建新的对象
  • 原型模式是一种创建型设计模式,允许一个对象再创建另外一个可定制的对象,无需知道如何创建的细节
  • 工作原理是:通过将一个原型对象传给那个要发动创建的对象,这个要发动创建的对象通过请求原型对象拷贝它 们自己来实施创建,即 对象.clone()
  • 形象的理解:孙大圣拔出猴毛, 变出其它孙大圣

浅拷贝

  • 对于数据类型是基本数据类型的成员变量,浅拷贝会直接进行值传递,也就是将该属性值复制一份给新的对象。
  • 对于数据类型是引用数据类型的成员变量,比如说成员变量是某个数组、某个类的对象等,那么浅拷贝会进行 引用传递,也就是只是将该成员变量的引用值(内存地址)复制一份给新的对象。因为实际上两个对象的该成员变量都指向同一个实例。在这种情况下,在一个对象中修改该成员变量会影响到另一个对象的该成员变量值
  • 浅拷贝是使用默认的 clone()方法来实现
    • 实现Cloneable接口
    • 重新clone方法

深拷贝

  • 复制对象的所有基本数据类型的成员变量值

  • 为所有引用数据类型的成员变量申请存储空间,并复制每个引用数据类型成员变量所引用的对象,直到该对象可达的所有对象。也就是说,对象进行深拷贝要对整个对象(包括对象的引用类型)进行拷贝

  • 实现Cloneable接口,重写clone方法

    • 如果存在引用类型,重写clone方法进行级联clone
  • 通过对象序列化实现深拷贝(推荐)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public Object deepClone() {
//创建流对象
ByteArrayOutputStream bos = null;
ObjectOutputStream oos = null;
ByteArrayInputStream bis = null;
ObjectInputStream ois = null;
try {
//序列化
bos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(bos);
oos.writeObject(this); //当前这个对象以对象流的方式输出
//反序列化
bis = new ByteArrayInputStream(bos.toByteArray());
ois = new ObjectInputStream(bis);
DeepProtoType copyObj = (DeepProtoType) ois.readObject();
return copyObj;
} catch (Exception e) {
e.printStackTrace();
return null;
} finally {
//关闭流
try {
bos.close();
oos.close();
bis.close();
ois.close();
} catch (Exception e2) {
System.out.println(e2.getMessage());
}
}
}

总结

  • 创建新的对象比较复杂时,可以利用原型模式简化对象的创建过程,同时也能够提高效率
  • 不用重新初始化对象,而是动态地获得对象运行时的状态
  • 如果原始对象发生变化(增加或者减少属性),其它克隆对象的也会发生相应的变化,无需修改代码
  • 在实现深克隆的时候可能需要比较复杂的代码
  • 缺点:需要为每一个类配备一个克隆方法,这对全新的类来说不是很难,但对已有的类进行改造时,需要修改 其源代码,违背了 ocp 原则。

建造者模式

  • 建造者模式(Builder Pattern)又叫生成器模式,是一种对象构建模式。它可以将复杂对象的建造过程抽象出来(抽象类别),使这个抽象过程的不同实现方法可以构造出不同表现(属性)的对象。
  • 建造者模式 是一步一步创建一个复杂的对象,它允许用户只通过指定复杂对象的类型和内容就可以构建它们, 用户不需要知道内部的具体构建细节。

四个角色

  • Product(产品角色): 一个具体的产品对象。
  • Builder(抽象建造者): 创建一个 Product 对象的各个部件指定的 接口/抽象类。
  • ConcreteBuilder(具体建造者): 实现接口,构建和装配各个部件。
  • Director(指挥者): 构建一个使用 Builder 接口的对象。它主要是用于创建一个复杂的对象。它主要有两个作用
    • 隔离了客户与对象的生产过程,
    • 负责控制产品对象的生产过程。

总结

  • 客户端(使用程序)不必知道产品内部组成的细节。
  • 每一个具体建造者都相对独立,用户使用不同的具体建造者即可得到不同的产品对象。
  • 可以更加精细地控制产品的创建过程 。将复杂产品的创建步骤分解在不同的方法中,使得创建过程更加清晰。
  • 增加新的具体建造者无须修改原有类库的代码,指挥者类针对抽象建造者类编程,系统扩展方便,符合“开闭原则”。
  • 建造者模式所创建的产品一般具有较多的共同点,其组成部分相似,如果产品之间的差异性很大,则不适合使用建造者模式,因此其使用范围受到一定的限制。
  • 如果产品的内部变化复杂,可能会导致需要定义很多具体建造者类来实现这种变化,导致系统变得很庞大,因 此在这种情况下,要考虑是否选择建造者模式.
  • 抽象工厂模式 VS 建造者模式
    • 抽象工厂模式实现对产品家族的创建,一个产品家族是这样的一系列产品:具有不同分类维度的产品组合,采用抽象工厂模式不需要关心构建过程,只关心什么产品由什么工厂生产即可。
    • 而建造者模式则是要求按照指定的蓝图建造产品,它的主要目的是通过组装零配件而产生一个新产品

适配器模式

  • 适配器模式(Adapter Pattern)将某个类的接口转换成客户端期望的另一个接口表示,主的目的是兼容性,让原本因接口不匹配不能一起工作的两个类可以协同工作。其别名为包装器(Wrapper)
  • 适配器模式属于结构型模式
  • 主要分为三类:类适配器模式、对象适配器模式、接口适配器模式

类适配器

  • Java 是单继承机制,所以类适配器需要继承 src 类这一点算是一个缺点, 因为这要求 dst 必须是接口,有一定局限性;
  • src 类的方法在 Adapter 中都会暴露出来,也增加了使用的成本。
  • 由于其继承了 src 类,所以它可以根据需求重写 src 类的方法,使得 Adapter 的灵活性增强了。

对象适配器

  • 基本思路和类的适配器模式相同,只是将 Adapter 类作修改,不是继承 src 类,而是持有src类的实例,以解决兼容性的问题。 即:持有 src 类,实现 dst 类接口,完成 src->dst 的适配
  • 根据“合成复用原则”,在系统中尽量使用关联关系(聚合)来替代继承关系。
  • 对象适配器模式是适配器模式常用的一种

接口适配器

  • 一些书籍称为缺省适配器模式。
  • 核心思路:当不需要全部实现接口提供的方法时,可先设计一个抽象类实现接口,并为该接口中每个方法提供 一个默认实现(空方法),那么该抽象类的子类可有选择地覆盖父类的某些方法来实现需求
  • 适用于一个接口不想使用其所有的方法的情况。

总结

  • 三种命名方式,是根据 src 是以怎样的形式给到 Adapter(在 Adapter 里的形式)来命名的。
    • 类适配器:以类给到,在 Adapter 里,就是将 src 当做类,继承
    • 对象适配器:以对象给到,在 Adapter 里,将 src 作为一个对象,持有
    • 接口适配器:以接口给到,在 Adapter 里,将 src 作为一个接口,实现
  • Adapter 模式最大的作用还是将原本不兼容的接口融合在一起工作。
  • 实际开发中,实现起来不拘泥于我们讲解的三种经典形式

桥接模式

  • 桥接模式(Bridge 模式)是指:将实现与抽象放在两个不同的类层次中,使两个层次可以独立改变
  • 是一种结构型设计模式
  • Bridge 模式基于类的最小设计原则,通过使用封装、聚合及继承等行为让不同的类承担不同的职责。它的主要特点是把抽象(Abstraction)与行为实现(Implementation)分离开来,从而可以保持各部分的独立性以及应对他们的

总结

  • 实现了抽象和实现部分的分离,从而极大的提供了系统的灵活性,让抽象部分和实现部分独立开来,这有助于系统进行分层设计,从而产生更好的结构化系统。

  • 对于系统的高层部分,只需要知道抽象部分和实现部分的接口就可以了,其它的部分由具体业务来完成。

  • 桥接模式替代多层继承方案,可以减少子类的个数,降低系统的管理和维护成本。

  • 桥接模式的引入增加了系统的理解和设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计和编程

  • 桥接模式要求正确识别出系统中两个独立变化的维度(抽象、和实现),因此其使用范围有一定的局限性。

  • 对于那些不希望使用继承或因为多层次继承导致系统类的个数急剧增加的系统,桥接模式尤为适用。


装饰者模式

  • 装饰者模式:动态的将新功能附加到对象上。在对象功能扩展方面,它比继承更有弹性
  • 装饰者模式也体现了 开闭原则(ocp)

原理

  • 装饰者模式就像打包一个快递

    • 主体:比如:陶瓷、衣服 (Component) // 被装饰者
    • 包装:比如:报纸填充、塑料泡沫、纸板、木板(Decorator)
  • 包含角色

    • 抽象构件(Component)角色:定义一个抽象接口以规范准备接收附加责任的对象。比如下边的Drink
    • 具体构件(ConcreteComponent)角色:实现抽象构件,通过装饰角色为其添加一些职责。 比如下方各类咖啡
    • 抽象装饰(Decorator)角色:继承抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能。Decorator
    • 具体装饰(ConcreteDecorator)角色:实现抽象装饰的相关方法,并给具体构件对象添加附加的责任。具体的调料

源码应用

总结

  • 继承可实现功能的扩展,但如果需要拓展的功能的种类很繁多(例如KFC肯德基订单需求),那么势必生成很多子类。

  • 装饰者模式通过:装饰者类内部含有被装饰者(组合关系),且被装饰者与装饰者都继承自共同的父类。这样可以通过将被装饰者的子类实例对象传入-> 装饰者子类的实例对象中,拓展被装饰者继承类即可实现动态的将新功能 附加到装饰者子类实例对象上。


组合模式

  • 组合模式(Composite Pattern),又叫部分整体模式,它创建了对象组的树形结构,将对象组合成树状结构以 表示“整体-部分”的层次关系。
  • 组合模式依据树形结构来组合对象,用来表示部分以及整体层次,属于结构型模式。
  • 组合模式使得用户对单个对象和组合对象的访问具有一致性,即:组合能让客户以一致的方式处理个别对象以及组合对象

  • Component :这是组合中对象声明接口,在适当情况下,实现所有类共有的接口默认行为,用于访问和管理 Component 子部件, Component 可以是抽象类或者接口
  • Leaf : 在组合中表示叶子节点,叶子节点没有子节点
  • Composite :非叶子节点, 用于存储子部件, 在 Component 接口中实现子部件的相关操作,比如增加, 删除

总结

  • 简化客户端操作。客户端只需要面对一致的对象而不用考虑整体部分或者节点叶子的问题。
  • 具有较强的扩展性。当我们要更改组合对象时,我们只需要调整内部的层次关系,客户端不用做出任何改动.
  • 方便创建出复杂的层次结构。客户端不用理会组合里面的组成细节,容易添加节点或者叶子从而创建出复杂的树形结构
  • 需要遍历组织机构,或者处理的对象具有树形结构时, 非常适合使用组合模式.
  • 要求较高的抽象性,如果节点和叶子有很多差异性的话,比如很多方法和属性都不一样,不适合使用组合模式

外观模式

  • 外观模式(Facade),也叫“过程模式:外观模式为子系统中的一组接口提供一个一致的界面,此模式定义了一个高层接口,这个接口使得这一子系统更加容易使用
  • 外观模式通过定义一个一致的接口,用以屏蔽内部子系统的细节,使得调用端只需跟这个接口发生调用,而无 需关心这个子系统的内部细节

jsBWgH.png

总结

  • 外观模式对外屏蔽了子系统的细节,因此外观模式降低了客户端对子系统使用的复杂性
  • 外观模式对客户端与子系统的耦合关系 - 解耦,让子系统内部的模块更易维护和扩展
  • 通过合理的使用外观模式,可以帮我们更好的划分访问的层次
  • 当系统需要进行分层设计时,可以考虑使用 Facade 模式
  • 在维护一个遗留的大型系统时,可能这个系统已经变得非常难以维护和扩展,此时可以考虑为新系统开发一个 Facade 类,来提供遗留系统的比较清晰简单的接口,让新系统与 Facade 类交互,提高复用性
  • 不能过多的或者不合理的使用外观模式,使用外观模式好,还是直接调用模块好。要以让系统有层次,利于维护为目的

享元模式

  • 享元模式(Flyweight Pattern) 也叫 蝇量模式: 运用共享技术有效地支持大量细粒度的对象
  • 常用于系统底层开发,解决系统的性能问题。像数据库连接池,里面都是创建好的连接对象,在这些连接对象 中有我们需要的则直接拿来用,避免重新创建,如果没有我们需要的,则创建一个
  • 享元模式能够解决重复对象的内存浪费的问题,当系统中有大量相似对象,需要缓冲池时。不需总是创建新对象,可以从缓冲池里拿。这样可以降低系统内存,同时提高效率
  • 享元模式经典的应用场景就是池技术了,String常量池、数据库连接池、缓冲池等等都是享元模式的应用,享元模式是池技术的重要实现方式

内部状态和外部状态

  • 比如围棋、五子棋,它们都有大量的棋子对象
    • 围棋和五子棋只有黑白两色
    • 所以棋子颜色就是棋子的内部状态
    • 而各个棋子之间的差别就是位置的不同,当我们落子后,落子颜色是定的,但位置是变化 的,所以棋子坐标就是棋子的外部状态
  • 享元模式提出了两个要求:细粒度和共享对象。这里就涉及到内部状态和外部状态了,即将对象的信息分为两 个部分:内部状态和外部状态
    • 内部状态:对象共享出来的信息,存储在享元对象内部且不会随环境的改变而改变
    • 外部状态:对象得以依赖的一个标记,是随环境改变而改变的、不可共享的状态
  • 举个例子:围棋理论上有 361 个空位可以放棋子,每盘棋都有可能有两三百个棋子对象产生,因为内存空间有限,一台服务器很难支持更多的玩家玩围棋游戏,如果用享元模式来处理棋子,那么棋子对象就可以减少到只有两个实例,这样就很好的解决了对象的开销问题

  • FlyWeight 是抽象的享元角色, 他是产品的抽象类, 同时定义出对象的外部状态和内部状态的接口或实现
  • ConcreteFlyWeight 是具体的享元角色,是具体的产品类,实现抽象角色定义相关业务
  • UnSharedConcreteFlyWeight 是不可共享的角色,一般不会出现在享元工厂。
  • FlyWeightFactory 享元工厂类,用于构建一个池容器(集合), 同时提供从池中获取对象方法

总结

  • 在享元模式这样理解,“享”就表示共享,“元”表示对象
  • 系统中有大量对象,这些对象消耗大量内存,并且对象的状态大部分可以外部化时,我们就可以考虑选用享元模式
  • 用唯一标识码判断,如果在内存中有,则返回这个唯一标识码所标识的对象,用 HashMap/HashTable 存储
  • 享元模式大大减少了对象的创建,降低了程序内存的占用,提高效率
  • 享元模式提高了系统的复杂度。需要分离出内部状态和外部状态,而外部状态具有固化特性,不应该随着内部状态的改变而改变,这是我们使用享元模式需要注意的地方.
  • 使用享元模式时,注意划分内部状态和外部状态,并且需要有一个工厂类加以控制。
  • 享元模式经典的应用场景是需要缓冲池的场景,比如 String 常量池、数据库连接池

代理模式

  • 代理模式:为一个对象提供一个替身,以控制对这个对象的访问。即通过代理对象访问目标对象
  • 这样做的好处是:可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能。
  • 被代理的对象可以是远程对象、创建开销大的对象或需要安全控制的对象
  • 代理模式有不同的形式, 主要有三种 静态代理、动态代理 (JDK 代理、接口代理)和 Cglib 代理 (可以在内存 动态的创建对象,而不需要实现接口, 他是属于动态代理的范畴) 。

  • 静态代理:静态代理的代理类在编译器就生成。
  • 动态代理:动态代理的代理类在java运行时动态生成。

注:Proxy可持有抽象层实例,这样该代理类就可以实现代理所有实现接口的类了。

静态代理

  • 静态代理在使用时,需要定义接口或者父类,被代理对象(即目标对象)与代理对象一起实现相同的接口或者是继承相同父类
    • 优点:在不修改目标对象的功能前提下, 能通过代理对象对目标功能扩展
    • 缺点:因为代理对象需要与目标对象实现一样的接口,所以会有很多代理类,一旦接口增加方法,目标对象与代理对象都要维护

动态代理

  • 代理对象不需要实现接口,但是目标对象要实现接口,否则不能用动态代理

  • 代理对象的生成,是利用 JDK 的 API,动态的在内存中构建代理对象,动态代理也叫做:JDK 代理、接口代理

    • public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)
      
      • ClassLoder ,类加载器
      • interfaces,增强方法所在的类,这个类实现的接口,支持多个接口
      • InvocationHandler,实现这个接口 InvocationHandler,创建代理对象,写增强的部分

Cglib代理

  • 静态代理和 JDK 代理模式都要求目标对象是实现一个接口,但是有时候目标对象只是一个单独的对象,并没有实现任何的接口,这个时候可使用目标对象子类来实现代理,这就是 Cglib 代理
  • Cglib代理也叫作子类代理,它是在内存中构建一个子类对象从而实现对目标对象功能扩展, 有些书也将Cglib代理归属到动态代理。
  • Cglib 是一个强大的高性能的代码生成包,它可以在运行期扩展 java 类与实现 java 接口.它广泛的被许多 AOP 的 框架使用,例如 Spring AOP,实现方法拦截
  • Cglib 包的底层是通过使用字节码处理框架 ASM 来转换字节码并生成新的类

模板模式

  • 在一个抽象类公开定义了执行它的方法的模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。
  • 模板模式定义一个操作中的算法的骨架,而将一些步骤延迟到子类中,使得子类可以不改变一个算法的结构,就可以重定义该算法的某些特定步骤
  • 这种类型的设计模式属于行为型模式

总结

  • 基本思想是:算法只存在于一个地方,也就是在父类中,容易修改。需要修改算法时,只要修改父类的模板方法或者已经实现的某些步骤,子类就会继承这些修改
  • 实现了最大化代码复用。父类的模板方法和已实现的某些步骤会被子类继承而直接使用。
  • 既统一了算法,也提供了很大的灵活性。父类的模板方法确保了算法的结构保持不变,同时由子类提供部分步 骤的实现。
  • 该模式的不足之处:每一个不同的实现都需要一个子类实现,导致类的个数增加,使得系统更加庞大
  • 一般模板方法都加上 final 关键字, 防止子类重写模板方法.
  • 模板方法模式使用场景:当要完成在某个过程,该过程要执行一系列步骤 ,这一系列的步骤基本相同,但其 个别步骤在实现时 可能不同,通常考虑用模板方法模式来处理

命令模式

  • 命令模式(Command Pattern):在软件设计中,我们经常需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是哪个, 我们只需在程序运行时指定具体的请求接收者即可,此时,可以使用命令模式来进行设计
  • 命令模式使得请求发送者与请求接收者消除彼此之间的耦合,让对象之间的调用关系更加灵活,实现解耦。
  • 在命令模式中,会将一个请求封装为一个对象,以便使用不同参数来表示不同的请求(即命名),同时命令模式也支持可撤销的操作
    • Invoker 是调用者角色
    • Command: 是命令角色,需要执行的所有命令都在这里,可以是接口或抽象类
    • Receiver: 接受者角色,知道如何实施和执行一个请求相关的操作
    • ConcreteCommand: 将一个接受者对象与一个动作绑定,调用接受者相应的操作,实现 execute

总结

  • 容易设计一个命令队列。只要把命令对象放到列队,就可以多线程的执行命令
  • 容易实现对请求的撤销和重做
  • 命令模式不足:可能导致某些系统有过多的具体命令类,增加了系统的复杂度,这点在在使用的时候要注意
  • 空命令也是一种设计模式,它为我们省去了判空的操作。在上面的实例中,如果没有用空命令,我们每按下一 个按键都要判空,这给我们编码带来一定的麻烦。
  • 命令模式经典的应用场景:界面的一个按钮都是一条命令、模拟 CMD(DOS 命令)订单的撤销/恢复、触发- 反馈机

访问者模式

  • 访问者模式(Visitor Pattern),封装一些作用于某种数据结构的各元素的操作,它可以在不改变数据结构的前提下定义作用于这些元素的新的操作。
  • 主要将数据结构与数据操作分离,解决数据结构和操作耦合性问题
  • 访问者模式的基本工作原理是:在被访问的类里面加一个对外提供接待访问者的接口
  • 访问者模式主要应用场景是:需要对一个对象结构中的对象进行很多不同操作(这些操作彼此没有关联),同时需要避免让这些操作”污染”这些对象的类,可以选用访问者模式解决
    • Visitor 是抽象访问者,为该对象结构中的 ConcreteElement 的每一个类声明一个 visit 操作
    • ConcreteVisitor :是一个具体的访问值 实现每个有 Visitor 声明的操作,是每个操作实现的部分.
    • ObjectStructure 能枚举它的元素, 可以提供一个高层的接口,用来允许访问者访问元素
    • Element 定义一个 accept 方法,接收一个访问者对象
    • ConcreteElement 为具体元素,实现了 accept 方法

总结

  • 访问者模式符合单一职责原则、让程序具有优秀的扩展性、灵活性非常高
  • 访问者模式可以对功能进行统一,可以做报表、UI、拦截器与过滤器,适用于数据结构相对稳定的系统
    • 具体元素对访问者公布细节,也就是说访问者关注了其他类的内部细节,这是迪米特法则所不建议的, 这样造成了具体元素变更比较困难
    • 违背了依赖倒转原则。访问者依赖的是具体元素,而不是抽象元素
  • 因此,如果一个系统有比较稳定的数据结构,又有经常变化的功能需求,那么访问者模式就是比较合适的.

迭代器模式

  • 迭代器模式(Iterator Pattern)是常用的设计模式,属于行为型模式
  • 如果我们的集合元素是用不同的方式实现的,有数组,还有 java 的集合类,或者还有其他方式,当客户端要遍 历这些集合元素的时候就要使用多种遍历方式,而且还会暴露元素的内部结构,可以考虑使用迭代器模式解决。
  • 提供一种遍历集合元素的统一接口,用一致的方法遍历集合元素,不需要知道集合对象的底层表示,即:不暴露其内部的结构。

总结

  • 提供一个统一的方法遍历对象,客户不用再考虑聚合的类型,使用一种方法就可以遍历对象了。
  • 隐藏了聚合的内部结构,客户端要遍历聚合的时候只能取到迭代器,而不会知道聚合的具体组成。
  • 提供了一种设计思想,就是一个类应该只有一个引起变化的原因(叫做单一责任原则)。在聚合类中,我们把迭代器分开,就是要把管理对象集合和遍历对象集合的责任分开,这样一来集合改变的话,只影响到聚合对象。 而如果遍历方式改变的话,只影响到了迭代器。
  • 当要展示一组相似对象,或者遍历一组相同对象时使用, 适合使用迭代器模式

观察者模式

  • 观察者模式:对象之间多对一依赖的一种设计方案,被依赖的对象为 Subject,依赖的对象为 Observer,Subject 通知 Observer 变化
    • Subject:登记注册、移除和通知
    • registerObserver 注册
    • removeObserver 移除
    • notifyObservers() 通知所有的注册的用户,根据不同需求,可以是更新数据,让用户来取,也可能是实施推送, 看具体需求定
    • Observer:接收输入

总结

  • 观察者模式设计后,会以集合的方式来管理用户(Observer),包括注册,移除和通知。 这样,我们增加观察者(这里可以理解成一个新的公告板),就不需要去修改核心类 WeatherData 不会修改代码, 遵守了 ocp 原则。

中介者模式

  • 中介者模式(Mediator Pattern),用一个中介对象来封装一系列的对象交互。中介者使各个对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互
  • 中介者模式属于行为型模式,使代码易于维护
  • 比如 MVC 模式,C(Controller 控制器)是 M(Model 模型)和 V(View 视图)的中介者,在前后端交互时起 到了中间人的作用

总结

  • 多个类相互耦合,会形成网状结构, 使用中介者模式将网状结构分离为星型结构,进行解耦
  • 减少类间依赖,降低了耦合,符合迪米特原则
  • 中介者承担了较多的责任,一旦中介者出现了问题,整个系统就会受到影响
  • 如果设计不当,中介者对象本身变得过于复杂,这点在实际使用时,要特别注意

备忘录模式

  • 备忘录模式(Memento Pattern)在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这 个状态。这样以后就可将该对象恢复到原先保存的状态
  • 现实生活中的备忘录是用来记录某些要去做的事情,或者是记录已经达成的共同意 见的事情,以防忘记了。而在软件层面,备忘录模式有着相同的含义,备忘录对象主要用来记录一个对象的某种状态,或者某些数据,当要做回退时,可以从备忘录对象里获取原来的数据进行恢复操作

总结

  • 给用户提供了一种可以恢复状态的机制,可以使用户能够比较方便地回到某个历史的状态
  • 实现了信息的封装,使得用户不需要关心状态的保存细节
  • 如果类的成员变量过多,势必会占用比较大的资源,而且每一次保存都会消耗一定的内存, 这个需要注意

解释器模式

  • 在编译原理中,一个算术表达式通过词法分析器形成词法单元,而后这些词法单元再通过语法分析器构建语法分析树,最终形成一颗抽象的语法分析树。这里的词法分析器和语法分析器都可以看做是解释器
  • 解释器模式(Interpreter Pattern):是指给定一个语言(表达式),定义它的文法的一种表示,并定义一个解释器, 使用该解释器来解释语言中的句子(表达式)
  • 应用场景
    • 应用可以将一个需要解释执行的语言中的句子表示为一个抽象语法树
    • 一些重复出现的问题可以用一种简单的语言来表达,比如编译器、运算表达式计算、正则表达式

  • Context: 是环境角色,含有解释器之外的全局信息.
  • AbstractExpression: 抽象表达式, 声明一个抽象的解释操作,这个方法为抽象语法树中所有的节点所共享
  • TerminalExpression: 为终结符表达式, 实现与文法中的终结符相关的解释操作
  • NonTermialExpression: 为非终结符表达式,为文法中的非终结符实现解释操作

总结

  • 当有一个语言需要解释执行,可将该语言中的句子表示为一个抽象语法树,就可以考虑使用解释器模式,让程 序具有良好的扩展性
  • 使用解释器可能带来的问题:解释器模式会引起类膨胀、解释器模式采用递归调用方法,将会导致调试非常复 杂、效率可能降低

状态模式

  • 状态模式(State Pattern):它主要用来解决对象在多种状态转换时,需要对外输出不同的行为的问题。状态和行为是一一对应的,状态之间可以相互转换
  • 当一个对象的内在状态改变时,允许改变其行为,这个对象看起来像是改变了其类

jgczIH.png

  • Context 类为环境角色, 用于维护 State 实例,这个实例定义当前状态
  • State 是抽象状态角色,定义一个接口封装与 Context 的一个特点接口相关行为
  • ConcreteState 具体的状态角色,每个子类实现一个与 Context 的一个状态相关行为

总结

  • 代码有很强的可读性。状态模式将每个状态的行为封装到对应的一个类中
  • 方便维护。将容易产生问题的 if-else 语句删除了
  • 符合“开闭原则”。容易增删状态
  • 会产生很多类。每个状态都要一个对应的类,当状态过多时会产生很多类,加大维护难度
  • 应用场景:当一个事件或者对象有很多种状态,状态之间会相互转换,对不同的状态要求有不同的行为的时候, 可以考虑使用状态模式

策略模式

  • 策略模式(Strategy Pattern)中,定义算法族(策略组),分别封装起来,让他们之间可以互相替换,此模式让算法的变化独立于使用算法的客户
  • 这算法体现了几个设计原则
    • 第一、把变化的代码从不变的代码中分离出来
    • 第二、针对接口编程而不是具体 类(定义了策略接口)
    • 第三、多用组合/聚合,少用继承(客户通过组合方式使用策略)

  • JDK 的 Arrays 的 Comparator 就使用了策略模式
  • 根据重写的compare方法,进行排序,相当于是不同的策略

总结

  • 策略模式的关键是:分析项目中变化部分与不变部分
  • 策略模式的核心思想是:
    • 多用组合/聚合 少用继承
    • 用行为类组合而不是行为的继承,更具有有弹性
  • 体现了“对修改关闭,对扩展开放”原则,客户端增加行为不用修改原有代码,只要添加一种策略(或者行为) 即可,避免了使用多重转移语句(if..else if..else)
  • 提供了可以替换继承关系的办法: 策略模式将算法封装在独立的 Strategy 类中使得你可以独立于其 Context 改 变它,使它易于切换、易于理解、易于扩展
  • 需要注意的是:每添加一个策略就要增加一个类,当策略过多是会导致类数目庞

责任链模式

  • 职责链模式(Chain of Responsibility Pattern), 又叫责任链模式,为请求创建了一个接收者对象的链。这种模式对请求的发送者和接收者进行解耦。
  • 职责链模式通常每个接收者都包含对另一个接收者的引用。如果一个对象不能处理该请求,那么它会把相同的请求传给下一个接收者。
  • 这种类型的设计模式属于行为型模式

总结

  • 将请求和处理分开,实现解耦,提高系统的灵活性
  • 简化了对象,使对象不需要知道链的结构
  • 性能会受到影响,特别是在链比较长的时候,因此需控制链中最大节点数量,一般通过在 Handler 中设置一个 最大节点数量,在 setNext()方法中判断是否已经超过阀值,超过则不允许该链建立,避免出现超长链无意识地 破坏系统性能
  • 调试不方便。采用了类似递归的方式,调试时逻辑可能比较复杂
  • 最佳应用场景:有多个对象可以处理同一个请求时,比如:多级请求、请假/加薪等审批流程、Java Web 中 Tomcat 对 Encoding 的处理、拦截