设计模式

设计模式

设计原则

单一职责原则

  • 一个类应该只有一个发生变化的原因(业务需求)

开闭原则

  • 软件中的对象,类,模块和函数对扩展应该是开放的,但对于修改是封闭的.这意味着应该用抽象定义结构,用具体实现扩展细节,以此确保软件系统开发和维护过程的可靠性
  • 面向抽象编程

里氏替换原则

  • 如果S是T的子类型,那么所有的T类型对象都可以在不破坏程序的情况下被S类型的对象替换
  • 当子类继承父类时,除添加新的方法且完成新增功能外,尽量不要重写父类的方法.
    • 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法
    • 子类可以增加自己特有的方法
    • 当子类的方法重载父类的方法时,方法的前置条件(即方法的输入参数)要比父类的方法更加宽松
    • 当子类的方法实现父类的方法时(重写,重载或实现抽象方法),方法的后置条件(即方法的输出或返回值)要比父类的方法更加严格或与父类的方法相同.
  • 作用
    • 里氏替换原则时实现开闭原则的重要方式之一
    • 解决了继承中重写父类造成的可复用性变差的问题
    • 是动作正确性的保证,即类的扩展不会给已有的系统引入新的错误,降低了代码出错的可能性
    • 加强程序的健壮性,同时变更时可以做到非常好的兼容性,提高程序的维护性,可扩展性,降低需求变更时引入的风险

迪米特法则

  • Law of Demeter 又称最少知道原则( Least Knowledge principle)
  • 一个对象类对于其他对象类来说,知道的越少越好.也就是说,两个类之间不要有过多的耦合关系,保持最少关联性

接口隔离原则

  • 一个类对另一个类的依赖应该建立在最小接口上,要求程序将臃肿庞大的接口拆分成更小和更具体的接口,让接口中只包含客户感兴趣的方法
  • 衡量规则
    • 接口尽量小,但是要有限度,一个接口只服务于一个子模块或业务逻辑
    • 为依赖接口的类定制服务,只提供调用者需要的方法,屏蔽不需要的方法
    • 了解环境,拒绝盲从。每个项目或产品都有选定的环境因素,环境不同,接口拆分的标准就不同。要深入了解业务逻辑
    • 提高内聚,减少对外交互。让接口用最少的方法完成最多的事情

依赖倒置原则

  • 在设计代码结构时,高层模块不应该依赖底层模块,二者都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象
  • 依赖倒置原则是实现开闭原则的重要途径之一,它降低了类之间的耦合,提高了系统的稳定性和可维护性,同时这样的代码一般更易读,且便于传承

设计模式

  • 模式:在某种情境下,针对某问题的某种解决方案
    • 情境:应用某个模式的情况,应该是会普遍出现的情况。
    • 问题:你想在某情境下达到的目标,也可以是某情境下的约束。
    • 解决方案:一个通用的设计,用来解决约束,达到目标。

分类

  • 根据模式类目中所采用的分类方式
    • 创建型:涉及将对象实例化,这类模式都提供一个方法,将客户从所需要实例化的对象中解耦
      • 工厂方法,抽象工厂
      • 原型模式
      • 单例模式
      • 建造者模式
    • 结构型:类或对象组合到更大的结构中
      • 适配器模式
      • 组合模式
      • 代理模式
      • 外观模式
      • 组合模式
      • 享元模式
      • 中介模式
    • 行为模式:涉及到类和对象如何交互及分配职责
      • 模板方法模式
      • 遍历器
      • 观察者
      • 状态模式
      • 命令模式
      • 策略模式

单例模式(Singleton)

  • 确保一个类只有一个实例,并提供全局访问

实现方式

  • 饿汉式:采用饿汉式实现方式,将耗时的初始化操作,提前到程序启动的时候完成,这样就可以避免在程序运行的时候,再去初始化导致的性能问题。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class IdGenerator {
    private AtomicLong id = new AtomicLong(0);
    private static final IdGenerator instance = new IdGenerator();// 饿汉式的精髓
    private IdGenerator() {}
    public static IdGenerator getInstance() {
    return instance;
    }
    public long getId() {
    return id.incrementAndGet();
    }
    }
  • 懒汉式:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class IdGenerator {
    private AtomicLong id = new AtomicLong(0);
    private static IdGenerator instance; // 懒汉式,调用getInstance时才实例化
    private IdGenerator() {}
    public static synchronized IdGenerator getInstance() {
    if (instance == null) {
    instance = new IdGenerator();
    }
    return instance;
    }
    public long getId() {
    return id.incrementAndGet();
    }
    }
  • DLC
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public class IdGenerator {
    private AtomicLong id = new AtomicLong(0);
    // 因为指令重排序,可能会导致 IdGenerator 对象被 new 出来,并且赋值给 instance 之后,还没来得及初始化(执行构造函数中的代码逻辑),就被另一个线程使用了。高版本的 Java 已经在 JDK 内部实现中解决了这个问题(解决的方法很简单,只要把对象 new 操作和初始化操作设计为原子操作,就自然能禁止重排序)。
    private static IdGenerator instance;
    private IdGenerator() {}
    public static IdGenerator getInstance() {
    if (instance == null) { //1. 第一次检测
    synchronized(IdGenerator.class) { // 此处为类级别的锁
    if (instance == null) { //2. 第二次检测
    instance = new IdGenerator();
    }
    }
    }
    return instance;
    }
    public long getId() {
    return id.incrementAndGet();
    }
    }
  • 静态内部类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public class IdGenerator {
    private AtomicLong id = new AtomicLong(0);
    private IdGenerator() {}

    private static class SingletonHolder{
    private static final IdGenerator instance = new IdGenerator();
    }

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

    public long getId() {
    return id.incrementAndGet();
    }
    }

    //SingletonHolder 是一个静态内部类,当外部类 IdGenerator 被加载的时候,并不会创建 SingletonHolder 实例对象。
    //只有当调用 getInstance() 方法时,SingletonHolder 才会被加载,这个时候才会创建 instance。instance 的唯一性、创建过程的线程安全性,都由 JVM 来保证。
  • 枚举
    1
    2
    3
    4
    5
    6
    7
    8
    public enum IdGenerator {
    INSTANCE;
    private AtomicLong id = new AtomicLong(0);

    public long getId() {
    return id.incrementAndGet();
    }
    }

单例存在的问题

  • 单例对 OOP 特性的支持不友好
  • 单例会隐藏类之间的依赖关系 单例无法通过构造函数、参数传递的方式来进行创建,无法通过函数的定义来查看类的依赖关系,具体实现逻辑都是隐藏在内部的,需要仔细查看代码实现才能知道这个类到底依赖哪些类。
  • 单例对代码的扩展性不友好
  • 单例对代码的可测试性不友好 单例类这种硬编码式的使用方式,会导致在编写单元测试的时候很难实现 对依赖的外部资源进行mock 替换
  • 单例不支持有参数的构造函数 单例不支持有参数的构造函数

原型模式

  • 如果对象的创建成本比较大,而同一个类的不同对象之间差别不大(大部分字段都相同),在这种情况下,我们可以利用对已有对象(原型)进行复制(或者叫拷贝)的方式来创建新对象,以达到节省创建时间的目的。
  • 实现方式: 深拷贝和浅拷贝
  • 构造函数不会执行,必须实现Cloneable 接口

工厂模式(factory)

  • 定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个。工厂方法让类把实例化推迟到子类。
  • 优点:避免创建者与具体的产品逻辑耦合。满足单一职责,每一个业务逻辑的实现都在自己所属的类中完成;满足开闭原则,无须更改调用方式就可以在程序中引入新的产品类型。
  • 依赖倒置原则
    • 应避免依赖具体类,尽量依赖抽象。

简单工厂模式

  • 又称为静态工厂方法(Static Factory Method)模式,在简单工厂模式中,可以根据参数的不同返回不同类的实例。简单工厂模式专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类。

工厂方法

  • 使用多态的方式创建对象,减少了if-else语句的使用

抽象工厂模式

  • 提供一个接口用于创建相关或依赖对象的家族,而不需要明确指定具体类
  • 相同点和不同点
    • 工厂方法通过继承类来创建对象,抽象通过接口实现
    • 工厂方式使用继承,把对象的创建委托给子类,由子类实现工厂方法来实现对象的创建。
    • 抽象工厂使用对象组合,对象的创建被实现在工厂的接口所暴露出来的方法中。

建造者模式(Builder Pattern)

  • 又叫生成器模式
  • 封装一个产品的构造过程,并允许按步骤构造。Builder 设计模式的目的是将复杂对象的构造与其表示分离。通过这样做,相同的构建过程可以创建不同的表示

享元模式(Flyweight Pattern)

  • 享元(Flyweight)的核心思想很简单:如果一个对象实例一经创建就不可变,那么反复创建相同的实例就没有必要,直接向调用方返回一个共享的实例就行,这样即节省内存,又可以减少创建对象的过程,提高运行速度。
  • 在实际应用中,享元模式主要应用于缓存,即客户端如果重复请求某些对象,不必每次查询数据库或者读取文件,而是直接返回内存中缓存的数据。
  • 例子: Java中的Integer -128- 127 提前创建对象缓存
  • 享元模式与单例的区别
    • 在单例模式中,一个类只能创建一个对象,而在享元模式中,一个类可以创建多个对象,每个对象被多处代码引用共享
    • 应用享元模式是为了对象复用,节省内存,而应用多例模式是为了限制对象的个数。
  • 享元模式和缓存的区别
    • 在享元模式的实现中,我们通过工厂类来“缓存”已经创建好的对象。这里的“缓存”实际上是“存储”的意思,跟我们平时所说的“数据库缓存”“CPU 缓存”“MemCache 缓存”是两回事。我们平时所讲的缓存,主要是为了提高访问效率,而非复用。
  • 享元模式和对象池的区别
    • 池化技术中的“复用”可以理解为“重复使用”,主要目的是节省时间(比如从数据库池中取一个连接,不需要重新创建)。在任意时刻,每一个对象、连接、线程,并不会被多处使用,而是被一个使用者独占,当使用完成之后,放回到池中,再由其他使用者重复利用。享元模式中的“复用”可以理解为“共享使用”,在整个生命周期中,都是被所有使用者共享的,主要目的是节省空间。

组合模式(Composite)

  • 允许你将对象组合成树形结构来表先”整体/部分”层次结构。组合能让客户以一致的方式处理个别对象以及对象组合。
  • 使用组合模式我们能把相同的操作应用在组合和个别对象上,就是可以忽略对象组合和个别对象之间的差别。
  • 使用组合模式的前提在于,你的业务场景必须能够表示成树形结构。所以,组合模式的应用场景也比较局限,它并不是一种很常用的设计模式。

代理模式(proxy)

  • 为另一个对象提供替身或占位符以控制对这个对象的访问。被代理的对象可以是远程对象,创建开销大的对象或需要安全控制的对象。
  • 代理也会造成项目中类的数量的增加。

远程代理(stub-proxy)

  • stub是服务器,proxy是客户端的接口。proxy持有ReadSubject的引用,二者拥有共同的方法。Proxy 的接口供客户端程序调用,然后它内部会把信息包装好,以某种方式(比如 RMI)传递给 Stub,而后者通过对应的接口作用于服务端系统,从而完成了“远程调用”。

虚拟代理(Virtual Proxy)

  • 虚拟代理作为创建开销大的对象的代理。当对象在创建前和创建中时,由虚拟代理来扮演对象的替身。对象创建后,代理就会将请求直接委托给对象。

保护代理(动态代理)

  • 根据访问权限绝对是否可以访问对象的代理(拦截器)。

其他代理模式

  • 防火墙代理(FireWall proxy)
    • 控制网络资源的访问,保护主题免于侵害
  • 职能引用代理(smart Reference Proxy)
    • 当主题被引用时,进行额外的动作,例如计算一个对象被引用的次数
  • 缓存代理(Caching Proxy)
    • 为开销大的运算结果提供暂时存储,也允许多个用户共享结果,以减少计算或网络延迟。
  • 同步代理(Synchronization Proxy)
    • 为分布式环境提供同步控制访问
  • 复杂隐藏代理(Complexity Hiding Porxy)
    • 用来隐藏一个类的复杂集合的复杂度,并进行访问控制。又是也成为外观代理(Facade Proxy),和外观模式的区别就是代理还会控制访问。
  • 写入时复制代理(Cope-On-Write Po)
    • 用来控制对象的复制,方法时延迟对象的复制,知道客户真的需要为止。这是虚拟代理的变体。实例:JAVA的CopyOnWriteArrayList

桥接模式(Bridge Pattern)

  • 不只改变你的实现,也改变你的抽象。
  • 将实现和抽解耦,应用实例: JDBC

装饰者模式(Decorator Pattern)

  • 定义:动态地将责任附加到对象上,想要扩展功能,装饰者提供有别于继承的另一种选择。
  • 缺点:需要管理更多的对象

适配器模式(Adapter Pattern)

  • 将一个类的接口,转换成客户期望的另一个接口。适配器让原本接口不兼容的类可以兼容。
  • 适配器模式包含类适配器(多重继承)和对象适配器
  • 适配器模式是改配类的接口来适配需求
  • 典型应用: DDD结构中的gateway层

外观模式(Facade Design Pattern)

  • 提供一个统一的接口,用来访问子系统中的一群接口,外观定义了一个高层接口,让子系统更容易使用。
  • 最小知识原则

中介模式(Mediator Design Pattern)

  • 中介模式定义了一个单独的(中介)对象,来封装一组对象之间的交互。将这组对象之间的交互委派给与中介对象交互,来避免对象之间的直接交互
  • 中介模式的设计思想跟中间层很像,通过引入中介这个中间层,将一组对象之间的交互关系(或者依赖关系)从多对多(网状关系)转换为一对多(星状关系)。
  • 经典例子: 航空管制
    • 为了让飞机在飞行的时候互不干扰,每架飞机都需要知道其他飞机每时每刻的位置,这就需要时刻跟其他飞机通信。
    • 飞机通信形成的通信网络就会无比复杂。这个时候,我们通过引入“塔台”这样一个中介,让每架飞机只跟塔台来通信,发送自己的位置给塔台,由塔台来负责每架飞机的航线调度。这样就大大简化了通信网络。

命令模式(Command)

  • 将请求封装成对象,这可以让你使用不同的请求,队列或者日志来参数化其他对象,命令模式也可以支持撤销操作。
  • 当需要将发出请求的对象与执行请求的对象解耦的时候,使用命令模式
  • 命令模式的主要作用和应用场景,是用来控制命令的执行,比如,异步、延迟、排队执行命令、撤销重做命令、存储命令、给命令记录日志等等,这才是命令模式能发挥独一无二作用的地方。

责任链模式(Chain of Responsibility Pattern)

  • 定义:将请求的发送和接收解耦,让多个接收对象都有机会处理这个请求。将这些接收对象串成一条链,并沿着这条链传递这个请求,直到链上的某个接收对象能够处理它为止
  • 让一个以上对象有机会能够处理某个请求的时候,就是用责任链模式
  • 职责链模式最常用来开发框架的过滤器和拦截器

迭代器模式(Iterator)

  • 提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露其内部的表示。

观察者模式(Observer)

  • 定义
    • 在对象之间定义一对多依赖,这样一来,当一个对象改变状态,依赖他的对象都会收到通知,并自动更新。
  • 优点
    • 交互对象之间的松耦合,有弹性的OO系统,使对象之间的相互依赖降到了最低。
  • java内置的观察者模式
    • 缺点:被观察者要继承父类而不是实现接口

策略模式(Strategy Pattern)

  • 定义:定义了算法族,分别封装起来,让他们之间可以相互替换,此模式让算法的变化独立于使用算法的客户。
  • 理解
    • 将一些容易变化的行为或者属性抽象为接口,独立出来实现,方便维护。

状态模式(State)

  • 允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类。
  • 与策略模式的差异
    1. 策略模式中,客户是有意识的进行对象转换,而状态模式,客户对此不会关心及察觉
    2. 策略模式是除继承外一种弹性的替代方式。状态模式是为了将行为包装到状态中,从而替代在context的很多条件判断。

有限状态机

  • 状态机有 3 个组成部分:状态(State)事件(Event)动作(Action)
  • 状态机的实现方式
    • 分支逻辑法:
      • 参照状态转移图,将每一个状态转移,原模原样地直译成代码。
      • 利用 if-else 或者 switch-case 分支逻辑,参照状态转移图,将每一个状态转移原模原样地直译成代码。对于简单的状态机来说,这种实现方式最简单、最直接,是首选。
    • 查表法:
      • 状态机还可以用二维表来表示,在二维表中,第一维表示当前状态,第二维表示事件
      • 对于状态很多、状态转移比较复杂的状态机来说,查表法比较合适。通过二维数组来表示状态转移图,能极大地提高代码的可读性和可维护性。
    • 状态模式
      • 对于状态并不多、状态转移也比较简单,但事件触发执行的动作包含的业务逻辑可能比较复杂的状态机来说,我们首选这种实现方式。

模板方法模式(Template Method Design Pattern)

  • 在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况,重新定义算法中的某些步骤。
  • 钩子
    • 子类必须提供算法中的某个方法或者步骤的实现时,使用抽象方法,如果是可选的,使用钩子。
    • 钩子还可以允许子类在内部列表重新组织后执行某些动作

复合模式

OO设计

基础

  • 抽象
  • 封装
  • 多态
  • 继承

原则

  • 封装变化
  • 多用接口,少用继承
  • 针对接口编程,不针对实现编程
  • 为交互对象之间的松耦合设计而努力
  • 对扩展开放,对修改关闭
  • 依赖抽象,不依赖具体类
  • 一个类应该只有一个引起变化的原因

中英文对照表

中文 英文
工厂方法模式 Factory Method Pattern
抽象工厂模式 Abstract Factory Pattern
建造者模式 Builder Pattern
原型模式 Prototype Pattern
单例模式 Singleton Pattern
适配器模式 Adapter Pattern
桥梁模式/桥接模式 Bridge Pattern
组合模式 Composite Pattern
装饰模式 Decorator Pattern
门面模式/外观模式 Facade Pattern
享元模式 Flyweight Pattern
代理模式 Proxy pattern
责任链模式 Chain of Responsibility Pattern
命令模式 Command Pattern
解释器模式 Interpreter Pattern
迭代器模式 Iterator Pattern
中介者模式 Mediator Pattern
备忘录模式 Memento Pattern
观察者模式 Observer Pattern
状态模式 State Pattern
策略模式 Strategy Pattern
模板方法模式 Template Method Pattern
访问者模式 Visitor Pattern

设计模式
https://x-leonidas.github.io/2022/02/01/06设计模式/设计模式/
作者
听风
发布于
2022年2月1日
更新于
2025年5月8日
许可协议