设计模式笔记

面向对象的特点

  • 可维护
  • 可复用
  • 可扩展
  • 灵活性好

随着业务变得越来越复杂,面向对象依然能够使得程序结构良好,而面向过程却会导致程序越来越臃肿。

设计模式的六大原则

  • 开闭原则:一个软件实体如类、模块和函数应该对修改封闭,对扩展开放
  • 单一职责原则:一个类只做一件事,一个类应该只有一个引起它修改的原因
  • 里氏替换原则:子类应该可以完全替换父类。也就是说在使用继承时,只扩展新功能,而不要破坏父类原有的功能
  • 依赖倒置原则:细节应该依赖于抽象,抽象不应依赖于细节。把抽象层放在程序设计的高层,并保持稳定,程序的细节变化由低层的实现层来完成
  • 迪米特法则:又名“最少知道原则”,一个类不应知道自己操作的类的细节,换言之,只和朋友谈话,不和朋友的朋友谈话
  • 接口隔离原则:客户端不应依赖它不需要的接口。如果一个接口在实现时,部分方法由于冗余被客户端空实现,则应该将接口拆分,让实现类只需依赖自己需要的接口方法

构建型模式 Creational Patterns

工厂模式 Factory

封装对象的设计模式,隐藏通用的实现细节和操作
可以降低耦合和减少代码重复
详细分类:

  • 简单工厂模式
    让一个工厂类承担构建所有对象的职责
    弊端:
    • 如果需要生产的产品过多,此模式会导致工厂类过于庞大,承担过多的职责,变成超级类
    • 当要生产新的产品时,必须在工厂类中添加新的分支,违背了开闭原则
  • 工厂方法模式
    为每个产品创建专属工厂,降低了产品间的互相依赖
  • 抽象工厂模式 Abstract factory
    在工厂方法模式的基础上,将公共的工厂接口提取到Interface中,然后产品工厂实现接口
    抽象工厂弱化了产品工厂的存在感,统一了方法签名,使得实现替换更加容易
    抽象工厂模式很好的发挥了开闭原则、依赖倒置原则,但缺点是抽象工厂模式太重了,如果 IFactory 接口需要新增功能,则会影响到所有的具体工厂类
    因此,抽象工厂模式适用于增加同类工厂这样的横向扩展需求,不适合新增功能这样的纵向扩展

单例模式 Singleton

单例模式非常常见,某个对象全局只需要一个实例时,就可以使用单例模式。它的优点也显而易见:

  • 它能够避免对象重复创建,节约空间并提升效率
  • 避免由于操作不同实例导致的逻辑错误

单例模式有两种实现方式:

  • 饿汉式
    在声明时初始化,构造方法通常定义为 private,保证了其他类无法实例化此类,必须通过getInstance方法才能获取到唯一的instance实例
    弊端:即使这个单例不需要使用,它也会在类加载之后立即创建出来,占用一块内存,并增加类初始化时间

  • 懒汉式
    先声明一个空变量,需要用时才初始化,在getInstance方法中判空创建,
    好处:按需加载,避免了内存浪费,减少了类初始化时间
    弊端:1. 非线程安全; 2. 将程序加载时间从启动时延后到了运行时,没有做到构建与使用分离,解耦
    解决方案一:给判空过程加上锁,

    1. 双检锁方式,程序效率更高
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      public class Singleton {

      private static Singleton instance = null;

      private Singleton() {
      }

      public static Singleton getInstance() {
      if (instance == null) {
      synchronized (Singleton.class) {
      if (instance == null) {
      instance = new Singleton();
      }
      }
      }
      return instance;
      }
      }
      JVM 底层为了优化程序运行效率,可能会对代码进行指令重排序,在一些特殊情况下会导致出现空指针,更进一步的优化是给 instance 变量加上 volatile 关键字
    2. 方法级别加锁,有一说双重检测法,低版本的JVM无法保证线程安全
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      public class Singleton {
      private static Singleton instance = null;
      private Singleton() {
      }
      public static synchronized Singleton getInstance() {
      if (instance == null) {
      instance = new Singleton();
      }
      return instance;
      }
      }

    解决方案二:静态内部类方式保证懒汉式单例的线程安全

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public class Singleton {

    private static class SingletonHolder {
    public static Singleton instance = new Singleton();
    }

    private Singleton() {
    }

    public static Singleton getInstance() {
    return SingletonHolder.instance;
    }
    }

    1. 静态内部类方式是怎么实现懒加载的
      Java 类的加载过程包括:加载、验证、准备、解析、初始化。初始化阶段即执行类的 clinit 方法(clinit = class + initialize),包括为类的静态变量赋初始值和执行静态代码块中的内容。但不会立即加载内部类,内部类会在使用时才加载。所以当此 Singleton 类加载时,SingletonHolder 并不会被立即加载,所以不会像饿汉式那样占用内存。
      另外,Java 虚拟机规定,当访问一个类的静态字段时,如果该类尚未初始化,则立即初始化此类。当调用Singleton 的 getInstance 方法时,由于其使用了 SingletonHolder 的静态变量 instance,所以这时才会去初始化 SingletonHolder,在 SingletonHolder 中 new 出 Singleton 对象。这就实现了懒加载。
    2. 静态内部类方式是怎么保证线程安全的
      Java 虚拟机的设计是非常稳定的,早已经考虑到了多线程并发执行的情况。虚拟机在加载类的 clinit 方法时,会保证 clinit 在多线程中被正确的加锁、同步。即使有多个线程同时去初始化一个类,一次也只有一个线程可以执行 clinit 方法,其他线程都需要阻塞等待,从而保证了线程安全。

    对于构建不复杂,加载完成后会立即使用的单例对象,推荐使用饿汉式。对于构建过程耗时较长,并不是所有使用此类都会用到的单例对象,推荐使用懒汉式。

建造者模式 Builder

建造者模式用于创建过程稳定,但配置多变的对象,将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示,不用担心忘了指定某个配置,保证了构建过程是稳定的
通过链式调用生成不同的配置
Lombok’s @Builder

原型模式 Prototype

用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象
比如:Object 的 clone()

小结

  • 工厂方法模式:为每一类对象建立工厂,将对象交由工厂创建,客户端只和工厂打交道。
  • 抽象工厂模式:为每一类工厂提取出抽象接口,使得新增工厂、替换工厂变得非常容易。
  • 建造者模式:用于创建构造过程稳定的对象,不同的 Builder 可以定义不同的配置。
  • 单例模式:全局使用同一个对象,分为饿汉式和懒汉式。懒汉式有双检锁和内部类两种实现方式。
  • 原型模式:为一个类定义 clone 方法,使得创建相同的对象更方便。

结构型模式 Structural Patterns

适配器模式 Adapter

适用于有相关性但不兼容的结构,源接口通过一个中间件转换后才可以适用于目标接口,这个转换过程就是适配,这个中间件就称之为适配器
将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作

桥接模式 Bridge

将抽象部分与它的实现部分分离,使它们都可以独立地变化。它是一种对象结构型模式,又称为柄体模式或接口模式

组合模式 Composite

用于整体与部分的结构,当整体与部分有相似的结构,在操作时可以被一致对待时,就可以使用组合模式
又叫部分整体模式,是用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分以及整体层次。这种类型的设计模式属于结构型模式,它创建了对象组的树形结构

组合模式中的安全方式与透明方式

透明方式:在 Component 中声明所有管理子对象的方法,包括 add 、remove 等,这样继承自 Component 的子类都具备了 add、remove 方法。对于外界来说叶节点和枝节点是透明的,它们具备完全一致的接口
安全方式:在 Component 中不声明 add 和 remove 等管理子对象的方法,这样叶节点就无需实现它,只需在枝节点中实现管理子对象的方法即可

装饰模式 Decorator

动态地给一个对象增加一些额外的职责,又称为包装器/“油漆工模式”
Java I/O
主要作用:

  • 增强一个类原有的功能 - 透明装饰模式 - 可以无限装饰
  • 为一个类添加新的功能 - 半透明装饰模式 - 无法多次装饰
    可以说,装饰模式是继承的一种替代方案,比生成子类实现更为灵活

外观模式 Facade

体现Java封装思想,将多个子系统封装起来,提供一个更简洁的接口供外部调用
外部与一个子系统的通信必须通过一个统一的外观对象进行,外观模式定义了一个高层接口,为子系统中的一组接口提供一个一致的界面,使得子系统更加容易使用,又称为门面模式

享元模式 Flyweight

共享对象,提高复用性,DRY(Don’t repeat yourself)

代理模式 Proxy

给某一个对象提供一个代理,并由代理对象控制对原对象的引用
分为:

  • 静态代理
  • 动态代理:优势,节省代码量,只需实现需要控制的方法,通过方法名对被代理类进行动态的控制

总结

  • 适配器模式:用于有相关性但不兼容的接口
  • 桥接模式:用于同等级的接口互相组合
  • 组合模式:用于整体与部分的结构
  • 外观模式:体现封装的思想
  • 享元模式:体现面向对象的可复用性
  • 代理模式:主要用于对某个对象加以控制

行为型模式 Behavioral Pattern

责任链模式 Chain of responsibility

处理职责相同,程度不同的类
优点:

  • 降低对象之间的耦合,发送者无须关心处理细节和传递过程,和处理者解耦
  • 扩展性强,满足开闭原则,可以根据需要增加新的请求处理类
  • 灵活性强。可以动态地改变链内的成员或者改变链的次序来适应流程的变化
  • 简化对象之间的连接。每个对象只需保持一个指向其后继者的引用
  • 责任分担。每个类只需要处理自己该处理的工作,不该处理的传递给下一个对象完成,明确各类的责任范围,符合单一职责原则
    缺点:
  • 不能保证每个请求一定被处理,该请求可能一直传到链的末端都得不到处理
  • 如果责任链过长,请求的处理可能涉及多个处理对象,系统性能将受到一定影响
  • 责任链建立的合理性要靠客户端来保证,增加了客户端的复杂性,可能会由于责任链拼接次序错误而导致系统出错,比如循环调用

命令模式 Command

通过execute去调用方法,封装 “方法调用”
优点:

  • 降低系统的耦合度,将 “行为请求者” 和 ”行为实现者“ 解耦
  • 扩展性强。增加或删除命令非常方便,并且不会影响其他类
  • 封装 “方法调用”,方便实现 Undo 和 Redo 操作
  • 灵活性强,可以实现宏命令
    缺点:
  • 会产生大量命令类。增加了系统的复杂性

解释器模式 Interpreter

部分:
1.抽象表达式(Abstract Expression):定义解释器的接口,约定解释器的解释操作,主要包含解释方法 interpret()
2.终结符表达式(Terminal Expression):是抽象表达式的子类,用来实现文法中与终结符相关的操作,不可拆分的最小单元
3.非终结符表达式(Nonterminal Expression):也是抽象表达式的子类,用来实现文法中与非终结符相关的操作,可被拆分的表达式
4.环境(Context):通常包含各个解释器需要的数据或是公共的功能,一般用来传递被所有解释器共享的数据
5.客户端(Client):将需要分析的句子或表达式转换成使用解释器对象描述的抽象语法树,然后调用解释器的解释方法,当然也可以通过环境角色间接访问解释器的解释方法

迭代器模式 Iterator

提供一种方法访问一个容器对象中各个元素,而又不需暴露该对象的内部细节

中介者模式 Mediator

定义一个中介对象来封装一系列对象之间的交互,使原有对象之间的耦合松散,且可以独立地改变它们之间的交互
当类与类之间的关系呈现网状时,引入一个中介者,可以使类与类之间的关系变成星形。将每个类与多个类的耦合关系简化为每个类与中介者的耦合关系,将多对多关系简化成多对一、一对多关系的设计模式
缺点:中介类需要处理所有类之间的协调工作,这可能会使中介者演变成一个超级类

备忘录模式 Memento

在不破坏封装的条件下,通过备忘录对象存储另外一个对象内部状态的快照,在将来合适的时候把这个对象还原到存储起来的状态
优点:

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

观察者模式 Observer

定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新
Observer, Observable, subscribe / register, update

状态模式 State

关于多态的设计模式,如果一个对象有多种状态,并且每种状态下的行为不同,可以为每种状态创建一个状态对象,使用状态对象,使得状态控制更加灵活,扩展性也更好
当一个对象的内在状态改变时允许改变其行为,这个对象看起来像是改变了其类
优点:将与特定状态相关的行为封装到一个状态对象中,使用多态代替 if-else 或者 switch-case 状态判断
缺点:导致类增加,这也是使用多态不可避免的缺点

策略模式 Strategy

定义了一系列算法,并将每一个算法封装起来,而且使它们还可以相互替换。策略模式让算法独立于使用它的客户而独立变化
优点:扩展性和灵活性。当有新的策略时,只需要增加一个策略类;要修改某个策略时,只需要更改具体的策略类,其他地方的代码都无需做任何调整
缺点:调用者需要了解策略类,增加了类与类之间的耦合 => 可以通过简单工厂模式与策略模式的结合,将不同的策略对象封装到工厂类中,用户只需要传递不同的策略类型,然后从工厂中拿到对应的策略对象即可

模板方法模式 Template method

关于继承的设计模式,被继承的父类是模板,它的某些步骤是稳定的,某些步骤被延迟到子类中实现
定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤
可以用方法的作用域控制权限:final - 不希望子类覆写;abstract - 子类必须覆写;protected 或 public - 没有特殊要求

访问者模式 Visitor

核心思想:将数据的结构和对数据的操作分离
访问者模式(Visitor Pattern)表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作
方法的接收者和方法的参数统称为方法的宗量。 根据分派基于多少个宗量,可以将分派分为单分派和多分派。单分派是指根据一个宗量就可以知道应该调用哪个方法,多分派是指需要根据多个宗量才能确定调用目标

小结 Summary

  • 责任链模式:处理职责相同,程度不同的对象,使其在一条链上传递
  • 命令模式:封装“方法调用”,将行为请求者和行为实现者解耦
  • 解释器模式:定义自己的语法规则
  • 迭代器模式:定义 next() 方法和 hasNext() 方法,让外部类使用这两个方法来遍历列表,以达到隐藏列表内部细节的目的
  • 中介者模式:通过引入中介者,将网状耦合结构变成星型结构
  • 备忘录模式:存储对象的状态,以便恢复
  • 观察者模式:处理一对多的依赖关系,被观察的对象改变时,多个观察者都能收到通知
  • 状态模式:关于多态的设计模式,每个状态类处理对象的一种状态
  • 策略模式:殊途同归,用多种方法做同一件事
  • 模板方法模式:关于继承的设计模式,父类是子类的模板
  • 访问者模式:将数据的结构和对数据的操作分离

资源