23种设计模式

1. 六大设计原则是什么?【SOLID】

我们只需要重点关注三个常用的原则即可:  单一职责原则,开闭原则,依赖倒置原则。

2. 提高代码质量的方法论包含:

  • 面向对象思想 (基础)

  • 设计原则 (指导方针)

  • 设计模式 (设计原则的具体实现)

  • 编程规范 (提高代码可读性)

  • 重构 (面向对象设计思想、设计原则、设计模式、编码规范的融合贯通)

3. 大部分设计模式要解决的都是代码的可重用性、可扩展性问题。

一、创建型模式(5种)

1.1 单例模式

(1)饿汉式

 实例在类加载时实例化,有JVM保证线程安全,不支持延迟加载。

(2)懒汉式

支持延迟加载,存在线程安全问题,用synchronized 锁住又可能会出现并发度低。

(3)双重检测

既支持延迟加载、又支持高并发的单例实现方式。

//单例模式-双重校验
public class Singleton_04 {

    //使用 volatile保证变量的可见性
    private volatile static Singleton_04 instance = null;

    private Singleton_04(){}

    //对外提供静态方法获取对象
    public static Singleton_04 getInstance(){
        //第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实例
        if(instance == null){
            synchronized (Singleton_04.class){
                //抢到锁之后再次进行判断是否为null
                if(instance == null){
                    instance = new Singleton_04();
                }
            }
        }
        return instance;
    }
}

实现步骤:

  • (1)在声明变量时使用了volatile关键字的作用有两个:①保证变量的可见性;②屏蔽指令重排序。
  • (2)将同步方法改为同步代码块. 在同步代码块中使用二次检查,以保证其不被重复实例化,同时在调用getInstance()方法时不进行同步锁,效率高。

在双重检查锁模式中为什么需要使用 volatile 关键字?

        在java内存模型中,volatile 关键字作用可以是保证可见性或者禁止指令重排。这里是因为 singleton = new Singleton() ,它并非是一个原子操作,事实上,在 JVM 中上述语句至少做了以下这 3 件事:

  • 第一步是给 singleton 分配内存空间;

  • 第二步开始调用 Singleton 的构造函数等,来初始化 singleton;

  • 第三步将 singleton 对象指向分配的内存空间(执行完这步 singleton 就不是 null 了)。

        这里需要留意一下 1-2-3 的顺序,因为存在指令重排序的优化,也就是说第 2 步和第 3 步的顺序是不能保证的,最终的执行顺序,可能是 1-2-3,也有可能是 1-3-2。

        如果是 1-3-2,那么在第 3 步执行完以后,singleton 就不是 null 了,可是这时第 2 步并没有执行,singleton 对象未完成初始化,它的属性的值可能不是我们所预期的值。假设此时线程 2 进入 getInstance 方法,由于 singleton 已经不是 null 了,所以会通过第一重检查并直接返回,但其实这时的 singleton 并没有完成初始化,所以使用这个实例的时候会报错。

详细流程如下图所示:

        线程 1 首先执行新建实例的第一步,也就是分配单例对象的内存空间,由于线程 1 被重排序,所以执行了新建实例的第三步,也就是把 singleton 指向之前分配出来的内存地址,在这第三步执行之后,singleton 对象便不再是 null。

        这时线程 2 进入 getInstance 方法,判断 singleton 对象不是 null,紧接着线程 2 就返回 singleton 对象并使用,由于没有初始化,所以报错了。最后,线程 1 “姗姗来迟”,才开始执行新建实例的第二步——初始化对象,可是这时的初始化已经晚了,因为前面已经报错了。

        使用了 volatile 之后,相当于是表明了该字段的更新可能是在其他线程中发生的,因此应确保在读取另一个线程写入的值时,可以顺利执行接下来所需的操作。在 JDK 5 以及后续版本所使用的 JMM 中,在使用了 volatile 后,会一定程度禁止相关语句的重排序,从而避免了上述由于重排序所导致的读取到不完整对象的问题的发生。

(4)静态内部类

原理:根据静态内部类的特性(外部类的加载不影响内部类),同时解决了按需加载、线程安全的问题,同时实现简洁。

  • 在静态内部类里创建单例,在装载该内部类时才会去创建单例
  • 线程安全:类是由 JVM加载,而JVM只会加载1遍,保证只有1个单例
public class Singleton_05 {

    private static class SingletonHandler{
        private static Singleton_05 instance = new Singleton_05();
    }

    private Singleton_05(){}

    public static Singleton_05 getInstance(){
        return SingletonHandler.instance;
    }
}

扩展:反射&序列化对单例的破坏

解决方法一: 在单例类的构造方法中 添加判断 instance != null 时,直接抛出异常,不够优雅。

解决方法二: Singleton中定义readResolve方法,并在该方法中指定要返回的对象的生成策略,就可以防止单例被破坏。

//只要在Singleton类中定义readResolve就可以解决该问题
private Object readResolve() {
	return singleton;
}

(5)枚举(推荐方式)

特点: 满足单例模式所需的 创建单例、线程安全、实现简洁的需求。

在使用枚举时,构造方法会被自动调用,利用这一特性也可以实现单例;默认枚举实例的创建是线程安全的,即使反序列化也不会生成新的实例,任何情况下都是一个单例(暴力反射对枚举方式无效)。

public enum Singleton_06{

    INSTANCE;

    private Object data;

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

    public static Singleton_06 getInstance(){
        return INSTANCE;
    }
}

单例模式总结

1.2 工厂方法模式★★★

简单工厂模式,更多内容见讲义,它不是23种设计模式之一。

在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。

工厂方法模式的目的很简单,就是封装对象创建的过程,提升创建对象方法的可复用性

工厂方法模式的主要角色:

  • 抽象工厂:提供了创建产品的接口,调用者通过它访问具体工厂的工厂方法来创建产品。

  • 具体工厂:主要是实现抽象工厂中的抽象方法,完成具体产品的创建。

  • 抽象产品:定义了产品的规范,描述了产品的主要特性和功能。

  • 具体产品:实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间一一对应。

我们直接来看看工厂方法模式的 UML 图:

优点:

  • 用户只需要知道具体工厂的名称就可得到所要的产品,无须知道产品的具体创建过程;

  • 在系统增加新的产品时只需要添加具体产品类和对应的具体工厂类,无须对原工厂进行任何修改,满足开闭原则;

缺点:

  • 每增加一个产品就要增加一个具体产品类和一个对应的具体工厂类,这增加了系统的复杂度。

什么时候使用工厂方法模式

  • 需要使用很多重复代码创建对象时,比如,DAO 层的数据对象、API 层的 VO 对象等。

  • 创建对象要访问外部信息或资源时,比如,读取数据库字段,获取访问授权 token 信息,配置文件等。

  • 创建需要统一管理生命周期的对象时,比如,会话信息、用户网页浏览轨迹对象等。

  • 创建池化对象时,比如,连接池对象、线程池对象、日志对象等。这些对象的特性是:有限、可重用,使用工厂方法模式可以有效节约资源。

  • 希望隐藏对象的真实类型时,比如,不希望使用者知道对象的真实构造函数参数等。

1.3 抽象工厂模式

         略。

1.4 建造者模式

建造者模式 (builder pattern), 也被称为生成器模式。

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

建造者模式与工厂模式区别&建造者模式的优缺点 (见讲义)

应用场景:

建造者(Builder)模式创建的是复杂对象,其产品的各个部分经常面临着剧烈的变化,但将它们组合在一起的算法却相对稳定,所以它通常在以下场合使用。

  • 创建的对象较复杂,由多个部件构成,各部件面临着复杂的变化,但构件间的建造顺序是稳定的。

  • 创建复杂对象的算法独立于该对象的组成部分以及它们的装配方式,即产品的构建过程和最终的表示是独立的。

1.5 原型模式(用的少)

定义: 原型模式(Prototype Design Pattern)用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型对象相同的新对象。

原型模式主要解决的问题:

如果创建对象的成本比较大,比如对象中的数据是经过复杂计算才能得到,或者需要从RPC接口或者数据库等比较慢的IO中获取,这种情况我们就可以使用原型模式,从其他已有的对象中进行拷贝,而不是每次都创建新对象,进行一些耗时的操作.

其实现在不推荐大家用Cloneable接口,实现比较麻烦,现在借助Apache Commons或者springframework可以直接实现:

  • 浅克隆:BeanUtils.cloneBean(Object obj);BeanUtils.copyProperties(S,T);

  • 深克隆:SerializationUtils.clone(T object);

BeanUtils是利用反射原理获得所有类可见的属性和方法,然后复制到target类。 SerializationUtils.clone()就是使用我们的前面讲的序列化实现深克隆,当然你要把要克隆的类实现Serialization接口。

使用场景

原型模式常见的使用场景有以下六种。

  • 资源优化场景。也就是当进行对象初始化需要使用很多外部资源时,比如,IO 资源、数据文件、CPU、网络和内存等。

  • 复杂的依赖场景。 比如,F 对象的创建依赖 A,A 又依赖 B,B 又依赖 C……于是创建过程是一连串对象的 get 和 set。

  • 性能和安全要求的场景。 比如,同一个用户在一个会话周期里,可能会反复登录平台或使用某些受限的功能,每一次访问请求都会访问授权服务器进行授权,但如果每次都通过 new 产生一个对象会非常烦琐,这时则可以使用原型模式。

  • 同一个对象可能被多个修改者使用的场景。 比如,一个商品对象需要提供给物流、会员、订单等多个服务访问,而且各个调用者可能都需要修改其值时,就可以考虑使用原型模式。

  • 需要保存原始对象状态的场景。 比如,记录历史操作的场景中,就可以通过原型模式快速保存记录。

二、结构型模式(7种)

结构型模式:介绍如何将对象和类组装成较大的结构,并同时保持结构的灵活和高效。

常用的有:代理模式、桥接模式、装饰者模式、适配器模式。

不常用的有:门面模式、组合模式、享元模式。

2.1 代理模式

2.1.1 JDK动态代理

静态代理与动态代理的区别:

  • 静态代理在编译时就已经实现了,编译完成后代理类是一个实际的class文件;
  • 动态代理是在运行时动态生成的,即编译完成后没有实际的class文件,而是在运行时动态生成类字节码,并加载到JVM中。
(1)JDK动态代理实现  

(2)类是如何动态生成的★★★
(3)代理类的调用过程★★★

2.1.2 cglib动态代理

(1)cglib动态代理实现

cglib (Code Generation Library ) 是一个第三方代码生成类库,运行时在内存中动态生成一个子类对象从而实现对目标对象功能的扩展。cglib 为没有实现接口的类提供代理,为JDK的动态代理提供了很好的补充。

cglib代理类,需要实现MethodInterceptor接口,并指定代理目标类target。具体代码请看讲义,简洁明晰。

2.1.3 总结

1. jdk代理和CGLIB代理:

  • 使用CGLib实现动态代理,CGLib底层采用ASM字节码生成框架,使用字节码技术生成代理类,在JDK1.6之前比使用Java反射效率要高。唯一需要注意的是,CGLib不能对声明为final的类或者方法进行代理,因为CGLib原理是动态生成被代理类的子类。
  • 在JDK1.6、JDK1.7、JDK1.8逐步对JDK动态代理优化之后,在调用次数较少的情况下,JDK代理效率高于CGLib代理效率,只有当进行大量调用的时候,JDK1.6和JDK1.7比CGLib代理效率低一点,但是到JDK1.8的时候,JDK代理效率高于CGLib代理。所以如果有接口使用JDK动态代理,如果没有接口使用CGLIB代理。

2. 代理模式优缺点

优点:

  • 代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用;

  • 代理对象可以扩展目标对象的功能;

  • 代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度;

缺点:

  • 增加了系统的复杂度;

3. 代理模式使用场景

  • 功能增强

    当需要对一个对象的访问提供一些额外操作时,可以使用代理模式

  • 远程(Remote)代理

    实际上,RPC 框架也可以看作一种代理模式,GoF 的《设计模式》一书中把它称作远程代理。通过远程代理,将网络通信、数据编解码等细节隐藏起来。客户端在使用 RPC 服务的时候,就像使用本地函数一样,无需了解跟服务器交互的细节。除此之外,RPC 服务的开发者也只需要开发业务逻辑,就像开发本地使用的函数一样,不需要关注跟客户端的交互细节。

  • 防火墙(Firewall)代理

    当你将浏览器配置成使用代理功能时,防火墙就将你的浏览器的请求转给互联网;当互联网返回响应时,代理服务器再把它转给你的浏览器。

  • 保护(Protect or Access)代理

    控制对一个对象的访问,如果需要,可以给不同的用户提供不同级别的使用权限。

2.2 桥接模式

桥接模式(bridge pattern) 的定义是:将抽象部分与它的实现部分分离,使它们都可以独立地变化。

桥接模式原理的核心是: 首先有要识别出一个类所具有的的两个独立变化维度,将它们设计为两个独立的继承等级结构,为两个维度都提供抽象层,并建立抽象耦合.  

仔细看讲义,体悟其精髓。通过例子掌握4种角色。

 桥接模式使用场景:

  • 需要提供平台独立性的应用程序时。 比如,不同数据库的 JDBC 驱动程序、硬盘驱动程序等。
  • 需要在某种统一协议下增加更多组件时。 比如,在支付场景中,我们期望支持微信、支付宝、各大银行的支付组件等。这里的统一协议是收款、支付、扣款,而组件就是微信、支付宝等。
  • 基于消息驱动的场景。 虽然消息的行为比较统一,主要包括发送、接收、处理和回执,但其实具体客户端的实现通常却各不相同,比如,手机短信、邮件消息、QQ 消息、微信消息等。
  • 拆分复杂的类对象时。 当一个类中包含大量对象和方法时,既不方便阅读,也不方便修改。
  • 希望从多个独立维度上扩展时。 比如,系统功能性和非功能性角度,业务或技术角度等。

2.3 装饰器模式

装饰模式(decorator pattern) 的原始定义是:动态的给一个对象添加一些额外的职责。就扩展功能而言,装饰器模式提供了一种比使用子类更加灵活的替代方案。

在软件设计中,装饰器模式是一种用于替代继承的技术,它通过一种无须定义子类的方式给对象动态的增加职责,使用对象之间的关联关系取代类之间的继承关系。

装饰器模式的适用场景:

  • 快速动态扩展和撤销一个类的功能场景。 比如,有的场景下对 API 接口的安全性要求较高,那么就可以使用装饰模式对传输的字符串数据进行压缩或加密。如果安全性要求不高,则可以不使用。
  • 不支持继承扩展类的场景。 比如,使用 final 关键字的类,或者系统中存在大量通过继承产生的子类。

2.4 适配器模式

代理、桥接、装饰器、适配器 4 种设计模式的区别

代理、桥接、装饰器、适配器,这 4 种模式是比较常用的结构型设计模式。它们的代码结构非常相似,但其各自的用意却不同,简单说一下它们之间的关系:

  • 代理模式:代理模式在不改变原始类接口的条件下,为原始类定义一个代理类,主要目的是控制访问,而非加强功能,这是它跟装饰器模式最大的不同。

  • 桥接模式:桥接模式的目的是将接口部分和实现部分分离,从而让它们可以较为容易、也相对独立地加以改变。

  • 装饰器模式:装饰者模式在不改变原始类接口的情况下,对原始类功能进行增强,并且支持多个装饰器的嵌套使用。

  • 适配器模式:将一个类的接口转换为客户希望的另一个接口,适配器模式让那些不兼容的类可以一起工作。

2.5 其他模式

外观模式( Facade Pattern),也叫门面模式, 外观模式的原始定义是:为子系统中的一组接口提供统一的接口。它定义了一个更高级别的接口,使子系统更易于使用。

组合模式:略。

享元模式:本质上就是找到对象的不可变特征,并缓存起来,当类似对象使用时从缓存中读取,以达到节省内存空间的目的。

三、行为型模式(11种)

一句话概括行为型模式:负责对象间的高效沟通和职责传递委派。

  • 常用的有:观察者模式、模板模式、策略模式、责任链模式、迭代器模式、状态模式。
  • 不常用的有:访问者模式、备忘录模式、命令模式、解释器模式、中介模式。

行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。

行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性。

11 种行为型模式中,模板方法模式和解释器模式是类行为型模式,其他的全部属于对象行为型模式。

3.1 观察者模式 ​​​​​​​

应用很多,如MQ等。

JDK中提供了Observable类以及Observer接口,它们构成了JDK对观察者模式的支持.

  • java.util.Observer 接口: 该接口中声明了一个方法,它充当抽象观察者,其中声明了一个update方法.

    • void update(Observable o, Object arg);

  • java.util.Observable 类: 充当观察目标类(被观察类) , 在该类中定义了一个Vector集合来存储观察者对象.下面是它最重要的 3 个方法。

    • void addObserver(Observer o) 方法:用于将新的观察者对象添加到集合中。

    • void notifyObservers(Object arg) 方法:调用集合中的所有观察者对象的 update方法,通知它们数据发生改变。通常越晚加入集合的观察者越先得到通知。

  • void setChange() 方法:用来设置一个 boolean 类型的内部标志,注明目标对象发生了变化。当它为true时,notifyObservers() 才会通知观察者。

用户可以直接使用Observer接口和Observable类作为观察者模式的抽象层,再自定义具体观察者类和具体观察目标类,使用JDK中提供的这两个类可以更加方便的实现观察者模式。

3.2 模板方法模式

模板方法模式是一种基于继承的代码复用技术,它是一种类行为模式. 模板方法模式其结构中只存在父类与子类之间的继承关系。模板方法的作用主要是提高程序的复用性和扩展性。

3.3 策略模式

面试问题: 如何用设计模式消除代码中的if-else

  • 物流行业中,通常会涉及到EDI报文(XML格式文件)传输和回执接收,每发送一份EDI报文,后续都会收到与之关联的回执(标识该数据在第三方系统中的流转状态)。
  • 这里列举几种回执类型:MT1101、MT2101、MT4101、MT8104,系统在收到不同的回执报文后,会执行对应的业务逻辑处理。

策略模式优点:

  • 策略类之间可以自由切换

    由于策略类都实现同一个接口,所以使它们之间可以自由切换。

  • 易于扩展

    增加一个新的策略只需要添加一个具体的策略类即可,基本不需要改变原有的代码,符合“开闭原则“

  • 避免使用多重条件选择语句(if else),充分体现面向对象设计思想。

策略模式缺点:

  • 客户端必须知道所有的策略类,并自行决定使用哪一个策略类。

  • 策略模式将造成产生很多策略类,可以通过使用享元模式在一定程度上减少对象的数量。

3.4 职责链模式

相关推荐

  1. 设计模式——23

    2024-04-03 18:36:03       24 阅读
  2. 23设计模式学习

    2024-04-03 18:36:03       24 阅读
  3. 23设计模式概述

    2024-04-03 18:36:03       19 阅读
  4. 23设计模式概述

    2024-04-03 18:36:03       19 阅读
  5. 【C++ 23设计模式

    2024-04-03 18:36:03       9 阅读
  6. GOF23设计模式

    2024-04-03 18:36:03       9 阅读

最近更新

  1. MATLAB中Simulink.defaultModelTemplate用法

    2024-04-03 18:36:03       0 阅读
  2. 如何实现YOLOv8保存目标检测后的视频文件

    2024-04-03 18:36:03       0 阅读
  3. 常见的SQL优化策略

    2024-04-03 18:36:03       0 阅读
  4. 软件架构演化方式的分类以及架构演化时期

    2024-04-03 18:36:03       0 阅读

热门阅读

  1. 设计模式 - Provider 模式

    2024-04-03 18:36:03       6 阅读
  2. dotnet依赖注入与IOC(包含Autofac的使用)

    2024-04-03 18:36:03       4 阅读
  3. TS小记--

    2024-04-03 18:36:03       6 阅读
  4. 什么是json?json可以存放哪几种数据类型

    2024-04-03 18:36:03       4 阅读
  5. 学习总结!

    2024-04-03 18:36:03       5 阅读
  6. Vue3中props和emits的使用总结

    2024-04-03 18:36:03       6 阅读
  7. IO和NIO的主要区别在哪里?

    2024-04-03 18:36:03       5 阅读
  8. CSS 滚动条样式修改

    2024-04-03 18:36:03       6 阅读