设计模式解析二 结构模式三剑客

  • 设计模式解析一 工厂模式的不同
  • 设计模式解析二 结构模式三剑客
  • 设计模式解析三 行为模式三剑客
  • 设计模式解析四 模板方法模式和外观模式
  • 设计模式解析五 观察者模式和桥接模式
  • 设计模式解析六 单例模式

一. 前言

设计模式通常被分为三类

  • 创建型模式
  • 行为型模式
  • 结构型模式

前一章的三种工厂模式就属于创建型模式,今天说的几种设计模式则是属于结构型模式。
结构型模式是描述如何将类对象结合在一起,形成一个更大的结构。


二. 适配器模式

适配器是什么,我们一定不难理解,因为在生活中到处都是,比如苹果电脑,本身没有USB接口,但有些设备又必须插在USB口上才能使用,于是我们很简单的就能在网上买到一些转接口,可以将USB口转换成苹果电脑支持的TYPE-C口,这就是适配器:


适配器

适配器

适配器

适配器模式是一个标准的对扩展开放,对修改关闭的例子,现有的系统不需要改变,厂商的接口也不需要改变,只需要增加一个适配器就能将两者进行结合,而不需要修改现有的代码。
简单用代码来实现一个适配器, 我们有一个手机,手机需要充电,手机的充电线只支持插在USB接口上,但我们现在电脑上没有USB口,只有typec口,怎么来设计这个适配器呢?首先是我们的手机:

public class Phone {
    public void charge(UsbInterface usbInterface) {
        usbInterface.usbCharge();
    }
}
public interface UsbInterface {
    void usbCharge();
}

手机充电只支持实现了这个接口的充电实现类,而电脑提供的充电方法是这个:

public class TypecInterface {
    public void typeCcharge() {
        System.out.println("开始使用 typec 接口充电");
    }
}

接下来的适配器就清晰了,我们需要有一个实现了UsbInterface接口的适配器,适配器能够访问TypecInterface的方法:

public class TypecToUsbAdapter implements UsbInterface {
    private TypecInterface typecInterface;
    public TypecToUsbAdapter(TypecInterface typecInterface) {
        this.typecInterface = typecInterface;
    }
    @Override
    public void usbCharge() {
        typecInterface.typeCcharge();
    }
}

接下来运行:

    public static void main(String[] args) {
        TypecInterface typecInterface = new TypecInterface();
        UsbInterface usbInterface = new TypecToUsbAdapter(typecInterface);
        Phone phone = new Phone();
        phone.charge(usbInterface);
    }

执行结果:

开始使用 typec 接口充电

接下来看一下类图:


设计模式解析二 结构模式三剑客_第1张图片
image.png

定义

适配器模式是将一个类的接口,转换成客户期望的另一个接口,适配器让原本接口不兼容的类可以相互协作。
上面的例子就是将typec的接口转换成了usb的接口提供给了手机使用。

二. 装饰器模式

我们经常去一些奶茶店购买奶茶,奶茶店员总是会问你很多很多,加冰吗?要多少甜?要加珍珠吗?总是会有各种各样的调料,如果我们使用一个类来管理这些调料的花会是怎样?

public interface Drink {
    // 饮品主体
    void setMainDrink();
    // 糖
    void setSugar();
    // 冰
    void setIce();
    // 珍珠
    void setPearl();
    // 热的
    void setHot();
    // 获取这杯饮料
    String get();
}

看起来还好,但有个问题,如果奶茶店又设计出了一种调料怎么办?只能修改这个类,再增加一个方法,但这就和设计模式中对修改关闭,对扩展开放的思路相违背了,那么我们是不是可以利用组合的方式把这些饮品的调料组合在一起,最后再利用对象之间的管理得到最终的饮料呢?
答案是肯定的,我们饮品主体、糖、冰、热度、珍珠这些抽象出来,形成一个统一的接口:

public interface Season {
    String get();
}

然后定义一个饮品的主体:

public class MilkTea implements Season {
    @Override
    public String get() {
        return "奶茶";
    }
}

在定义两种调料:

public class Ice implements Season {

    private Season season;

    public Ice(Season season) {
        this.season = season;
    }

    @Override
    public String get() {
        return "加冰-" + season.get();
    }
}
public class Sugar implements Season {

    private Season season;

    public Sugar(Season season) {
        this.season = season;
    }

    @Override
    public String get() {
        return "5分糖-" + season.get();
    }

}

然后我们开始组装这杯奶茶:

    public static void main(String[] args) {
        Season tea = new MilkTea();
        Season ice = new Ice(tea);
        Season sugar = new Sugar(ice);
        System.out.println(sugar.get());
    }

最后执行结果:

5分糖-加冰-奶茶

由于所有的调料都遵循了Season接口,所以我们在调用的过程中可以不必关系实际调料是什么,只需要任意组装,最后通过这层层的装饰关系就能得到最后这杯奶茶。

定义

装饰者模式动态的将责任附加到对象上,若要扩展功能,装饰着提供了比继承更富有弹性的替代方案。

三. 代理模式

代理模式分为静态代理和动态代理两部分,首先说静态代理,还记得上面的手机充电吗功能吗?现在有新的需求来了,我们希望能够在手机充电完成前后加一些提醒信息,能够提醒手机开始充电了,充电完成后也能提醒。
这种情况我们就可以用到代理模式,先创建一个同样实现了UsbInterface的代理类:

public class UsbInterfaceProxy implements UsbInterface {
    private UsbInterface usbInterface;

    public UsbInterfaceProxy(UsbInterface usbInterface) {
        this.usbInterface = usbInterface;
    }

    @Override
    public void usbCharge() {
        System.out.println("提醒:充电开始");
        usbInterface.usbCharge();
        System.out.println("提醒:充电完成");
    }

}

接下来看如何使用这个代理类:

    public static void main(String[] args) {
        TypecInterface typecInterface = new TypecInterface();
        UsbInterface usbInterface = new TypecToUsbAdapter(typecInterface);
        UsbInterfaceProxy proxy = new UsbInterfaceProxy(usbInterface);
        Phone phone = new Phone();
        phone.charge(proxy);
    }
提醒:充电开始
开始使用 typec 接口充电
提醒:充电完成

类图:


设计模式解析二 结构模式三剑客_第2张图片
image.png

如果抛开这里面适配器,我们会发现似乎代理模式和适配器模式还有装饰器模式很像,都是持有被代理或者被装饰或者被适配的对象,访问者访问代理或者访问装饰者或者访问适配器,然后由代理或者装饰者或者适配器去访问对应的对象,那么他们的区别到底在哪呢?先看代理模式的定义。

定义

代理模式为另一个对象提供一个替身或者占位以控制对这个对象的访问。

三种设计模式的不同

三种模式的类图和结构很相似,实际应用中实际很容易搞混,那么他们三个的区别到底在哪呢?实际上设计模式是设计来解决某些问题,而这三种设计模式也是分别解决不同的问题。

  • 适配器模式的目标是把A接口适配成B的接口
  • 装饰者模式是所有对象都实现A接口,它的意图是为被装饰者扩展功能,或者说附加责任
  • 而代理模式和装饰者结构类似,但它的意图是为了控制对象的访问,它需要实现被代理对象的所有接口,然后在代理中控制对这个对象的所有访问。

设计模式在使用中,每种设计模式的意图很重要,有时候也许结构相似,但意图不同,也是属于不同的设计模式。

四. 动态代理

动态代理和静态代理模式的结构一模一样,目标也一样,他们都属于代理模式,只是区别在于,静态代理模式中的代理类是需要手动来实现,如果遇到需要对很多类进行代理控制他们的访问,一个个手动的来实现就太复杂了,所以java提供了一种动态代理的方式,它能够让我们不需要实现代理类,而是在程序运行时自动生成。
先看一下动态代理,还是上面的充电接口。
首先我们需要新建一个类实现InvocationHandler,来定义我们的代理行为,其中构造函数则放入我们期望的被代理对象,要注意区分,这里的类跟上面的UsbInterfaceProxy类是完全不同的,这里的InvocationHandler是把上面的监控行为的过程和代理过程进行了分离,InvocationHandler包含的只是行为,而代理的过程还在后面。

public class DymicProxyHandler implements InvocationHandler {
    private UsbInterface usbInterface;

    public DymicProxyHandler(UsbInterface usbInterface) {
        this.usbInterface = usbInterface;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("动态代理,方法" + method.getName() + "执行前");
        Object o = method.invoke(usbInterface, args);
        System.out.println("动态代理,方法" + method.getName() + "执行后");
        return o;
    }

}

再来看创建动态代理类的过程:

public static void main(String[] args) {
        TypecInterface typecInterface = new TypecInterface();
        UsbInterface usbInterface = new TypecToUsbAdapter(typecInterface);
        DymicProxyHandler handler = new DymicProxyHandler(usbInterface);
        // 创建动态代理类
        UsbInterface usbInterfaceProxy = (UsbInterface)Proxy.newProxyInstance(UsbInterfaceProxy.class.getClassLoader(),
                                                                              new Class[] {UsbInterface.class},
                                                                              handler);
        Phone phone = new Phone();
        phone.charge(usbInterfaceProxy);
    }

先看结果:

动态代理,方法usbCharge执行前
开始使用 typec 接口充电
动态代理,方法usbCharge执行后

Proxy.newProxyInstance该方法能够帮我们生成一个指定接口的实现的动态代理类,这个类里有什么呢,实际上类里存了一些Method变量,以及我们的handler,当调用方调用指定方法时则将对应Method和调用参数一起传给handler执行。那么这个动态代理类到底怎么生成的呢?可以把类编译出来看一下,首先拿到代理类文件的字节码,并存储到文件中:

        String path = "/Users/cloud/Downloads/";
        FileOutputStream out = new FileOutputStream(new File(usbInterfaceProxy.getClass().getSimpleName()+".class"));
        byte[] bytes = ProxyGenerator.generateProxyClass(usbInterfaceProxy.getClass().getSimpleName(), new Class[]{
            UsbInterface.class
        });
        out.write(bytes);

用idea等工具反编译工具反编译该类:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

import cn.learn.test.design.factory.adapter.UsbInterface;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

public final class $Proxy0 extends Proxy implements UsbInterface {
    private static Method m1;
    private static Method m3;
    private static Method m2;
    private static Method m0;

    public $Proxy0(InvocationHandler var1) throws  {
        super(var1);
    }

    public final boolean equals(Object var1) throws  {
        try {
            return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    public final void usbCharge() throws  {
        try {
            super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final String toString() throws  {
        try {
            return (String)super.h.invoke(this, m2, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final int hashCode() throws  {
        try {
            return (Integer)super.h.invoke(this, m0, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m3 = Class.forName("cn.learn.test.design.factory.adapter.UsbInterface").getMethod("usbCharge");
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

动态代理运用的可以说是非常广了,大名鼎鼎的Spring AOP切面编程就是利用动态代理实现的,spring aop在使用中只需要加一些注解,并定义一些切面点就可以使用,实际上就是spring 利用动态代理的机制为我们做了这个动态代理的过程,它扫描我们的注解,并扫描我们配置的切面点的方法,然后一一生成动态代理。
还有市面上很多分布式服务框架,客户端实际上也是动态代理实现的,因为分布式服务的实现实际上并不在本地,而是在远程,那么要像调用本地服务一样调用远程服务,使用动态代理是最合适的。
比如dubbo,如果大家有去跟踪一下dubbo的源码调用的话就会发现,dubbo也是通过动态代理的方式去调用底层的netty(dubbo的动态代理实现使用的不是java jdk中提供的工具,而是利用java字节码技术来手动实现的,简而言之就是手动根据需要组装拼接的class类,jdk的代理是把所有方法都作为成员变量,而dubbo的代理类则是利用一个list成员变量来存储的所有Method,至于为什么dubbo要自己来实现这个动态代理的过程,我也不清楚了=.=),再跟远程dubbo服务进行通信的,当然这其中肯定还包含有dubbo的通信协议,序列化,凡序列话,注解扫描,服务发现,负载均衡等等很多方面的东西,不过只要理解这些,大家自己去做一个简易的分布式服务框架中间件也不是难事。
还有比如Spring cloud feign-client的客户端,也是通过动态代理的方式,动态代理去调用底层的RestTemplate,然后通过http的方式同远程服务进行沟通。

五. 结语

结构模式三剑客就讲到这里,为什么把他们三个拿在一起讲,其实就是因为这三个设计模式是我们实际应用中最常用,最多接触的结构型设计模式了,无论你懂还是不懂设计模式,你都一定有使用过。但是系统化的学习一次设计膜还是也还是能够让我们加深对设计模式的理解,从而在以后的工作中能够用对设计模式。

你可能感兴趣的:(设计模式解析二 结构模式三剑客)