八、常见的设计模式

杨大大...大约 10 分钟

1.软件设计原则有哪些?

2.什么是设计模式?

设计模式(Design pattern)代表了最佳的实践,通常被有经验的⾯向对象的软件开发⼈员所采⽤。设计模式是软件开发⼈员在软件开发过程中⾯临的⼀般问题的解决⽅案。这些解决⽅案是众多软件开发⼈员经过相当⻓的⼀段时间的试验和错误总结出来的。

分为三大类:

创建型: 在创建对象的同时隐藏创建逻辑,不使⽤ new 直接实例化对象,程序在判断需要创建哪些对象时更灵活。包括⼯⼚/抽象⼯⼚/单例/建造者/原型模式。 结构型: 通过类和接⼝间的继承和引⽤实现创建复杂结构的对象。包括适配器/桥接模式/过滤器/组合/装饰器/外观/享元/代理模式。 行为型: 通过类之间不同通信⽅式实现不同⾏为。包括责任链/命名/解释器/迭代器/中介者/备忘录/观察者/状态/策略/模板/访问者模式。

3.单例模式

单例模式属于创建型模式,⼀个单例类在任何情况下都只存在⼀个实例,构造⽅法必须是私有的、由自己创建⼀个静态变量存储实例,对外提供⼀个静态公有方法获取实例。

双重检查锁(DCL, 即 double-checked locking) 实现代码如下:

public class Singleton {
 
    // 1、私有化构造⽅法

    private Singleton() {

    }

    // 2、定义⼀个静态变量指向⾃⼰类型

    private volatile static Singleton instance;

    // 3、对外提供⼀个公共的⽅法获取实例

    public static Singleton getInstance() {

        // 第⼀重检查是否为 null

        if (instance == null) {

            // 使⽤ synchronized 加锁

            synchronized (Singleton.class) {

                // 第⼆重检查是否为 null
                if (instance == null) {

                    // new 关键字创建对象不是原⼦操作

                    instance = new Singleton();
                 }
             }
          }
            return instance;
        }
}

优点:懒加载,线程安全,效率较⾼缺点:实现较复杂 这⾥的双重检查是指两次⾮空判断,锁指的是 synchronized 加锁,为什么要进⾏双重判断,其实很简单,第⼀重判断,如果实例已经存在,那么就不再需要进⾏同步操作,⽽是直接返回这个实例,如果没有创建,才会进⼊同步块,同步块的⽬的与之前相同,⽬的是为了防⽌有多个线程同时调⽤时,导致⽣成多个实例,有了同步块,每次只能有⼀个线程调⽤访问同步块内容,当第⼀个抢到锁的调⽤获取了实例之后,这个实例就会被创建,之后的所有调⽤都不会进⼊同步块,直接在第⼀重判断就返回了单例。关于内部的第⼆重空判断的作⽤,当多个线程⼀起到达锁位置时,进⾏锁竞争,其中⼀个线程获取锁,如果是第⼀次进⼊则为 null,会进⾏单例对象的创建,完成后释放锁,其他线程获取锁后就会被空判断拦截,直接返回已创建的单例对象。

4.工厂模式

4.1说⼀说简单⼯⼚模式:

简单⼯⼚模式指由⼀个⼯⼚对象来创建实例,客户端不需要关注创建逻辑,只需提供传⼊⼯⼚的参数。

适⽤于⼯⼚类负责创建对象较少的情况,缺点是如果要增加新产品,就需要修改⼯⼚类的判断逻辑,违背开闭原则,且产品多的话会使⼯⼚类⽐较复杂。

Spring 中的 BeanFactory 使⽤简单⼯⼚模式,根据传⼊⼀个唯⼀的标识来获得 Bean 对象。

4.2⼯⼚⽅法模式了解吗:

和简单⼯⼚模式中⼯⼚负责⽣产所有产品相⽐,⼯⼚⽅法模式将⽣成具体产品的任务分发给具体的产品⼯⼚。

也就是定义⼀个抽象⼯⼚,其定义了产品的⽣产接⼝,但不负责具体的产品,将⽣产任务交给不同的派⽣类⼯⼚。这样不⽤通过指定类型来创建对象了。

4.3抽象⼯⼚模式了解吗:

简单⼯⼚模式和⼯⼚⽅法模式不管⼯⼚怎么拆分抽象,都只是针对⼀类产品,如果要⽣成另⼀种产品,就⽐较难办了!抽象⼯⼚模式通过在 AbstarctFactory 中增加创建产品的接⼝,并在具体⼦⼯⼚中实现新加产品的创建,当然前提是⼦⼯⼚⽀持⽣产该产品。否则继承的这个接⼝可以什么也不⼲。

5.装饰器模式

5.1什么是装饰器模式?

定义:指在不改变现有对象结构的情况下,动态地给该对象增加一些职责(即增加其额外功能)的模式。

5.2装饰器模式结构

装饰(Decorator)模式中的角色:

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

5.3使用场景

  • 当不能采用继承的方式对系统进行扩充或者采用继承不利于系统扩展和维护时。不能采用继承的情况主要有两类:

    第一类是系统中存在大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长;

    第二类是因为类定义不能继承(如final类)

  • 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。

  • 当对象的功能要求可以动态地添加,也可以再动态地撤销时。

参考:https://blog.csdn.net/weixin_64366370/article/details/130494769open in new window

6.代理模式

6.1什么是代理模式?

代理模式的本质是⼀个中间件,主要⽬的是解耦合服务提供者和使⽤者。使⽤者通过代理间接的访问服务提供者,便于后者的封装和控制,是⼀种结构性模式。

6.2静态代理和动态代理的区别:

  1. 灵活性 :动态代理更加灵活,不需要必须实现接⼝,可以直接代理实现类,并且可以不需要针对每个⽬标类都创建⼀个代理类。另外,静态代理中,接⼝⼀旦新增加⽅法,⽬标对象和代理对象都要进⾏修改,这是⾮常麻烦的!

  2. JVM 层⾯ :静态代理在编译时就将接⼝、实现类、代理类这些都变成了⼀个个实际的 class ⽂件。⽽动态代理是在运⾏时动态⽣成类字节码,并加载到 JVM 中的。

6.3静态代理:

静态代理中,我们对目标对象的每个方法的增强都是手动完成的(后面会具体演示代码),非常不灵活(比如接口一旦新增加方法,目标对象和代理对象都要进行修改)且麻烦(需要对每个目标类都单独写一个代理类)。 实际应用场景非常非常少,日常开发几乎看不到使用静态代理的场景。

上面我们是从实现和应用角度来说的静态代理,从 JVM 层面来说, 静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。

6.4动态代理:

相比于静态代理来说,动态代理更加灵活。我们不需要针对每个目标类都单独创建一个代理类,并且也不需要我们必须实现接口,我们可以直接代理实现类 CGLIB 动态代理机制。

从 JVM 角度来说,动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。

说到动态代理,Spring AOP、RPC 框架应该是两个不得不提的,它们的实现都依赖了动态代理。

动态代理在我们日常开发中使用的相对较少,但是在框架中的几乎是必用的一门技术。学会了动态代理之后,对于我们理解和学习各种框架的原理也非常有帮助。

就 Java 来说,动态代理的实现方式有很多种,比如 JDK 动态代理CGLIB 动态代理等等。

6.4.1JDK 动态代理机制

在 Java 动态代理机制中 InvocationHandler 接口和 Proxy 类是核心。

Proxy 类中使用频率最高的方法是:newProxyInstance() ,这个方法主要用来生成一个代理对象。

    public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h)
        throws IllegalArgumentException
    {
        ......
    }


这个方法一共有 3 个参数:
    loader :类加载器,用于加载代理对象。
    interfaces : 被代理类实现的一些接口;
    h : 实现了 InvocationHandler 接口的对象;

要实现动态代理的话,还必须需要实现InvocationHandler 来自定义处理逻辑。 当我们的动态代理对象调用一个方法时,这个方法的调用就会被转发到实现InvocationHandler 接口类的 invoke 方法来调用。

public interface InvocationHandler {

    /**
     * 当你使用代理对象调用方法的时候实际会调用到这个方法
     */
    public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;
}

invoke() 方法有下面三个参数:

    proxy :动态生成的代理类
    method : 与代理类对象调用的方法相对应
    args : 当前 method 方法的参数

也就是说:你通过Proxy 类的 newProxyInstance() 创建的代理对象在调用方法的时候,实际会调用到实现InvocationHandler 接口的类的 invoke()方法。 你可以在 invoke() 方法中自定义处理逻辑,比如在方法执行前后做什么事情。

6.4.2CGLIB 动态代理机制

JDK 动态代理有一个最致命的问题是其只能代理实现了接口的类。

为了解决这个问题,我们可以用 CGLIB 动态代理机制来避免。

CGLIB(Code Generation Library)允许我们在运行时对字节码进行修改和动态生成。CGLIB 通过继承方式实现代理。例如 Spring 中的 AOP 模块中:如果目标对象实现了接口,则默认采用 JDK 动态代理,否则采用 CGLIB 动态代理。

在 CGLIB 动态代理机制中 MethodInterceptor 接口和 Enhancer 类是核心。

你需要自定义 MethodInterceptor 并重写 intercept 方法,intercept 用于拦截增强被代理类的方法。

public interface MethodInterceptor extends Callback{
    // 拦截被代理类中的方法
    public Object intercept(Object obj, java.lang.reflect.Method method, Object[] 								args,MethodProxy proxy) throws Throwable;
}

    obj : 被代理的对象(需要增强的对象)
    method : 被拦截的方法(需要增强的方法)
    args : 方法入参
    proxy : 用于调用原始方法

你可以通过 Enhancer类来动态获取被代理类,当代理类调用方法的时候,实际调用的是 MethodInterceptor 中的 intercept 方法。

6.4.3JDK 动态代理和 CGLIB 动态代理对比:

  • JDK 动态代理只能代理实现了接口的类或者直接代理接口,而 CGLIB 可以代理未实现任何接口的类。 另外, CGLIB 动态代理是通过生成一个被代理类的子类来拦截被代理类的方法调用,因此不能代理声明为 final 类型的类和方法。
  • 就二者的效率来说,大部分情况都是 JDK 动态代理更优秀,随着 JDK 版本的升级,这个优势更加明显。