java---设计模式(二)

1,795 阅读14分钟

5、建造者模式

建造者模式 是将一个复杂对象的构建过程和他的表示进行分离,通过配置的方式来使得相同的构建过程来创建不同的表示。相对于用户而言就只需要知道指定配置就可以获得具有相应功能的对象实例,而不用去了解其具体建造过程和细节。

 建造者模式适用于创建对象需要很多的步骤,但是每步不一定都是需要进行显示指定的。如果一个对象有非常复杂的内部属性,就可以将对象的创建和使用进行分离。

  • 模式中主要存在四个角色

    产品:需要创建的对象

    建造者接口:主要规范组件的构建,实现由子类及逆行规范。

    实际建造者:建造者的具体实现,根据不同的业务逻辑具体化对象各个组成部分的创建

    调用者:调用建造者完成对象的创建,只提供对象的配置。

 玩具公司先推出一种可由用户喜好进行设计的玩偶,默认是圆形黄色的小皮球,形状可选方案为圆形、正方形、长方形,颜色可选方案为黄色、黑色、白色,材料可选方案为真皮、毛绒、塑料。用户只需要拿着响应的需求供应过来就可以产出对应的玩具,不用去了解怎样实现,这样就可以使用建造者模式进行结构设计。

  • 产品

    @Data
    public class Doll {
        private String shape;
        private String color;
        private String material;
    
        public Doll() {}
    
        @Override
        public String toString() {
            return "玩偶: " +
                    "形状 = " + shape +
                    ", 颜色 = " + color +
                    ", 材料 = " + material;
        }
    }
    
  • 建造者接口

    public interface IBuilder<T> {
        T build();
        IBuilder setComponent(Map<String, String> component);
    }
    
  • 实际建造者

    public class DollBuilder implements IBuilder<Doll>{
        private Doll doll;
        
        @Override
        public Doll build() {
            return doll;
        }
    
        @Override
        public IBuilder setComponent(Map<String, String> component) {
            this.doll = new Doll();
            return this.addShape(component);
        }
    
        private IBuilder addShape(Map<String, String> component) {
            String shape = component.getOrDefault("shape", "圆形");
            //  其余检查逻辑
            this.doll.setShape(shape);
            return this.addColor(component);
        }
    
        private IBuilder addColor(Map<String, String> component) {
            String color = component.getOrDefault("color", "黄色");
            //  其余检查逻辑
            this.doll.setColor(color);
            return this.addMaterial(component);
        }
    
        private IBuilder addMaterial(Map<String, String> component) {
            String material = component.getOrDefault("material", "皮质");
            //  其余检查逻辑
            this.doll.setMaterial(material);
            return this;
        }
    }
    

 调用者只需要拿着对应的参数设置来调用建造者就可以拿到想要配置的产品实例。优点是封装性好,创建和使用分离,在一定程度上解耦应用。缺点是会额外产生一个 builder 对象来创建支持对象的创建,而且一旦一个产品的内部创建流程发生改变,就需要修改整个建造者来匹配新的情况,成本较大。

  • 建造者工厂模式 都是屏蔽产品的具体实现,那他们之间的区别是什么,什么场景下使用?
    1. 创建对象的方式不同。建造者适用于创建内部结构比较复杂的对象;工厂模式创建出来的对象都是一样的。
    2. 创建着重点不一样。建造者可以根据具体的来进行组件的增删;工厂模式只会根据需要创建出一个对象即可。

6、代理模式

 代理模式是指为目标对象提供一种代理,以控制对这个对象的访问,属于结构性模式。代理对象主要在客户端和目标对象之间起到中介作用,客户端通过代理对象来实现对目标对象的调用。

 代理模式一般包含三种角色,抽象对象,实际对象,代理对象。

角色作用
Subject为实际对象和代理对象提供公共接口规则
RealSubject负责执行系统真正逻辑的业务对象
Proxy内部持有实际对象的引用,在调用具体业务方法的前后添加一些额外的处理代码,也就是不处理真正核心的业务逻辑

 代理实际上就是在真正的业务逻辑执行的前后增加一些代码逻辑,使得目标对象的功能得到增强,而且不影响实际业务的处理流程,也可以理解为是代码的增强,分为静态代理和动态代理。

 6.1、静态代理

 静态代理就是代理类和目标类都是实现同一套规则,然后代理类持有目标类的一个引用,并且拓展属于代理类的增强方法,然后在调用目标对象的实际业务逻辑时,讲增强方法进行织入,达到增强目标类的目的。

 明星在还没有出名之前都是自己一个人处理自己的业务,显得非常的游刃有余,但是某天突然间就被大众熟知,流量大增,各类活动层出不穷。这时候明星一个人就有点处理不过来了,于是就雇了一个经纪人,在上台表演之前给明星指定计划,结束之后送明星回家,明星就可以只关注表演的事情。

  • 公共方法接口

    public interface IPerform {
        void Performance();
    }
    
  • 明星目标类

    public class Star implements IPerform{
        @Override
        public void Performance() {
            //  明星表演逻辑
        }
    }
    
  • 经纪人代理类

    public class Broker implements IPerform{
        private IPerform star;
    
        public Broker(IPerform star) {
            this.star = star;
        }
    
        @Override
        public void Performance() {
            this.formulate();
            this.star.Performance();
            this.driver();
        }
    
        private void formulate() {
            //  帮助明星制定计划
        }
    
        private void driver() {
            //  送明星回家
        }
    }
    

 这样存在的问题就是一个代理类只能代理一类的目标对象,而且代码的增强是硬编码,无法完成功能的拓展,因此引出了动态代理的需求。而最常用的动态代理分为两中实现,一是基于反射的 JDK proxy ,二十基于字节码的 CGLIB proxy

 6.2、JDK 动态代理

 本质上和静态代理差不多,也需要目标对象实现一个规则类来进行扩展,但是具有更好的拓展性,代理功能更加的强大。现在有一种业务场景就是需要统计某些业务类的逻辑执行时间,业务类之间的实现规则不一样,如果使用静态代理的话将会产生大量的代理类,使得系统结构变得复杂,使用动态代理的能更好的完成功能的扩展。

  • 服务接口

    public interface IOrder {
        void order();
    }
    
    public interface IBuy {
        void buy();
    }
    
  • 具体服务

    public class Buy implements IBuy {
        @Override
        public void buy() {
            //  买东西
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    public class Order implements IOrder{
        @Override
        public void order() {
            //  下订单
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
  • 代理类

    public class StatisticTime implements InvocationHandler {
        private Object target;
    
        public StatisticTime(Object target) {
            this.target = target;
        }
    
        public Object getProxy() {
            Class<?> clazz = this.target.getClass();
            return Proxy.newProxyInstance(clazz.getClassLoader(), clazz.getInterfaces(), this);
        }
    
        @Override
        public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
            Date start = new Date();
            Object result = method.invoke(target, objects);
            statisticTime(method, start);
            return result;
        }
    
        public void statisticTime(Method method, Date start) {
            Date current = new Date();
            long time = current.getTime() - start.getTime();
            System.out.println(method.getName() + " 业务耗时: " + time / 1000 + "s");
        }
    }
    

 将需要进行业务执行时间统计的服务类传进来进行代理返回后再执行其具体业务方法,就可以完成执行时间的统计了,一个代理类就可以解决相似功能的增强,在 Spring AOP 中就有对动态代理的使用,不过它里面使用执行链的模式来进行代理增强,也就是说具体增强的方法可以进行改变,不过也只是局限在执行链的执行顺序中。

 6.3、JDK 动态代理原理

 作为一名技术人员,我们当然不能只停留在会使用技术的层面,我们还要知道为什么这么用,下面就来实现一下 JDK 动态代理的代码,主要分为五步实现。

  1. 获取目标对象引用,并且获取所有接口。
  2. 代理生成新的类,新的类要实现目标类的实现的所有接口。
  3. 动态添加 java 代码。
  4. 编译字节码文件
  5. 加载运行。
  • 代理接口,需要进行代理的类实现该接口

    public interface BODInvocationHandler {
        Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
    }
    
  • 代理类,负责生成代理对象

    public class BODProxy {
        public static final String SEPARATE = "\r\n";
    
        private static Map<Class, Class> mapping = new HashMap<>();
    
        static {
            mapping.put(int.class, Integer.class);
        }
    
        /**
         * 代理生成
         * @param classLoader 类加载器
         * @param interfaces 实现接口
         * @param handler 方法增强
         * @return
         */
        public static Object newProxyInstance(BODClassLoader classLoader, Class<?> [] interfaces, BODInvocationHandler handler) {
            try {
                String javaSource = generateSource(interfaces);
                //  生成的 java 代码写入磁盘
                String path = BODProxy.class.getResource("").getPath();
                File file = new File(path + "$BODProxy.java");
                FileWriter writer = new FileWriter(file);
                writer.write(javaSource);
                writer.flush();
                writer.close();
    
                //  编译字节码文件
                JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
                StandardJavaFileManager manager = compiler.getStandardFileManager(null, null, null);
                Iterable<? extends JavaFileObject> iterable = manager.getJavaFileObjects(file);
                JavaCompiler.CompilationTask task = compiler.getTask(null, manager, null, null, null, iterable);
                task.call();
                manager.close();
    
                //  加载字节码文件返回重组对象
                Class<?> proxy = classLoader.findClass("$BODProxy");
                Constructor<?> constructor = proxy.getConstructor(BODInvocationHandler.class);
                file.delete();
                return constructor.newInstance(handler);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }
    
        private static String getReturnEmpty(Class<?> returnClass) {
            if (mapping.containsKey(returnClass)) {
                return "return 0";
            } else if (returnClass == void.class) {
                return "";
            } else {
                return "return null";
            }
        }
    
        private static String getCaseCode(String code, Class<?> returnClass) {
            if (mapping.containsKey(returnClass)) {
                return "((" + mapping.get(returnClass).getName() + ")" +
                        code + ")." + returnClass.getSimpleName() + "Value()";
            }
            return code;
        }
    
        private static boolean hasReturnValue(Class<?> clazz) {
            return clazz != void.class;
        }
    
        private static String toLowerFirstCase(String name) {
            char[] chars = name.toCharArray();
            chars[0] += 32;
            return String.valueOf(chars);
        }
    
        private static String generateSource(Class<?>[] interfaces) {
            StringBuffer buffer = new StringBuffer();
            buffer.append("package com.beordie.designModel.proxy.jdk;" + SEPARATE);
            buffer.append("import java.lang.reflect.*;" + SEPARATE);
            buffer.append("public class $BODProxy implements " + interfaces[0].getName() + " {" + SEPARATE);
            buffer.append("BODInvocationHandler handler;" + SEPARATE);
            buffer.append("public $BODProxy(BODInvocationHandler handler) {" + SEPARATE);
            buffer.append("this.handler = handler;" + SEPARATE);
            buffer.append("}" + SEPARATE);
            //  复写方法
            for (Method method : interfaces[0].getMethods()) {
                Class<?>[] parameters = method.getParameterTypes();
                StringBuffer paramNames = new StringBuffer();
                StringBuffer paramValues = new StringBuffer();
                StringBuffer paramClasses = new StringBuffer();
    
                for (int i = 0; i < parameters.length; i++) {
                    Class<?> parameter = parameters[i];
                    String type = parameter.getName();
                    String paramName = toLowerFirstCase(parameter.getSimpleName());
                    paramNames.append(type + " " + paramName);
                    paramValues.append(paramName);
                    paramClasses.append(parameter.getName() + ".class");
                    if (i > 0 && i < parameters.length - 1) {
                        paramNames.append(",");
                        paramValues.append(",");
                        paramClasses.append(",");
                    }
                }
                buffer.append("public " + method.getReturnType().getName() + " " + method.getName() + " (" +
                        paramNames.toString() + ") {" + SEPARATE);
                buffer.append("try {" + SEPARATE);
                buffer.append("Method m = " + interfaces[0].getName() + ".class.getMethod(\"" +
                        method.getName() +"\", new Class[]{" + paramClasses.toString() + "});" + SEPARATE);
                buffer.append(hasReturnValue(method.getReturnType()) ? "return " : "");
                buffer.append(getCaseCode("this.handler.invoke(this, m, new Object[]{" + paramValues +
                        "})", method.getReturnType()) +";" + SEPARATE);
                buffer.append("} catch (Error e) { }" + SEPARATE);
                buffer.append("catch (Throwable e) {" + SEPARATE);
                buffer.append("throw new UndeclaredThrowableException(e);" + SEPARATE);
                buffer.append("}");
                buffer.append(getReturnEmpty(method.getReturnType()));
                buffer.append("}" + SEPARATE);
            }
            buffer.append("}" + SEPARATE);
            return buffer.toString();
        }
    }
    
  • 类加载器,负责将自己文件加载到内存中

    public class BODClassLoader extends ClassLoader{
        private File classPathFile;
        public BODClassLoader() {
            String classPath = BODClassLoader.class.getResource("").getPath();
            this.classPathFile = new File(classPath);
        }
    
        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            String className = BODClassLoader.class.getPackage().getName() + "." + name;
            if (classPathFile != null) {
                File file = new File(classPathFile, name.replaceAll("\\.", "/") + ".class");
                if (file.exists()) {
                    FileInputStream inputStream = null;
                    ByteArrayOutputStream outputStream = null;
                    try {
                        inputStream = new FileInputStream(file);
                        outputStream = new ByteArrayOutputStream();
                        byte[] buffer = new byte[1024];
                        int len = 0;
                        while ((len = inputStream.read(buffer)) != -1) {
                            outputStream.write(buffer, 0, len);
                        }
                        return defineClass(className, outputStream.toByteArray(), 0, outputStream.size());
                    } catch (Exception e) {
                        e.printStackTrace();
                    } finally {
                        if (inputStream != null) {
                            try {
                                inputStream.close();
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        }
                        if (outputStream != null) {
                            try {
                                outputStream.close();
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                }
            }
            return null;
        }
    }
    

 下面就只需要将目标类进行 JDK代理 的正常操作即可(为了方便,只针对一个接口进行实现)。

 6.4、CGLIB 动态代理

CGLIB 不需要目标类实现接口,只需要传入目标对象的类型即可进行代码的增强返回,使用比较简单。

public class Advise implements MethodInterceptor {
    public Object getInstance(Class<?> clazz) throws Exception {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(clazz);
        enhancer.setCallback(this);
        return enhancer.create();
    }
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        before();
        Object result = methodProxy.invokeSuper(o, objects);
        return result;
    }

    private void before() {
        System.out.println("正在加强");
    }
}

 6.5、CGLIB动态代理原理

 通过 System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY,'D://temp/') 代码来写入代理时的内存字节文件。我们会发现在目录下会生成三个字节码文件。

![](E:\file\object file\全栈项目\summary\design pattern\cglib-class.jpg)

 其中有一个带有 EnhancerByCGLIB 字符的文件,这个就是代理生成的代理类,通过反编译发现代理类继承了目标类。

public class Buy$$EnhancerByCGLIB$$81908922 extends Buy implements Factory {
	......
    final void CGLIB$buy$0() {
        Buy$$EnhancerByCGLIB$$81908922.super.buy();
    }

    public final void buy() {
        MethodInterceptor methodInterceptor = this.CGLIB$CALLBACK_0;
        if (methodInterceptor == null) {
            CGLIB$BIND_CALLBACKS(this);
            methodInterceptor = this.CGLIB$CALLBACK_0;
        }
        if (methodInterceptor != null) {
            methodInterceptor.intercept(this, CGLIB$buy$0$Method, CGLIB$emptyArgs, CGLIB$buy$0$Proxy);
        } else {
            Buy$$EnhancerByCGLIB$$81908922.super.buy();
        }
    }
	......
}

 通过断点执行我们可以发现,执行流程是先调用代理类的 代理方法(Buy)--> 拦截器(intercept)--> methodProxy.invokeSuper(o, objects) --> CGLIBbuybuy0() --> 目标类的目标方法 。前面的两步骤是我们知道的,但是这个 invokeSuper 的内部处理流程我们还不清楚,通过追踪流程可知道在方法里面加载了代理类的对象,也就是生成的三个字节码中名儿最长的那个,现在整个代理过程就清楚了。

CGLIB 和 JDK 动态代理的区别

JDK 动态代理CGLIB 动态代理
代理对象实现了目标对象的接口继承了目标对象
生成字节码直接写 class 字节码通过 ASM 框架写 class 字节码
实现原理反射调用FastClass 机制直接调用
  • 代理模式优点
    1. 在能将代理对象与真实被调用目标对象分离。
    2. 一定程度上较低了系统部件之间的耦合性。
    3. 增强目标的同时保护了原始的目标对象。
  • 代理模式缺点
    1. 生成代理对象会使系统中的类增多,增加了系统的复杂度。
    2. 目标方法之间增加了一层调用关系,比原来的直接调用方式要慢。

7、适配器模式

适配器 的功能和他的名字一样,用于将一个接口转换为适合客户端所需要的另一种接口,从而使其能够在一起工作,属于结构性设计模式。

 适用于已经存在的类不匹配具体的需求,需要进行转换,往往出现在维护时期,由于产品的更换而导致相关接口不匹配,这个时候就需要进行适配,就像手机的充电器适配一样,常用的家庭电压 220V 充进手机扛不住,因此需要一个充电头来进行降压,这里的充电头和适配器就是一个概念作用。

 适配器模式中一般包含三个角色:

角色职责
目标角色期望适配的接口
源角色已存在的对象,但是接口不匹配
适配器将源角色转换为目标角色

 7.1、类适配器

 通过继承目标角色来实现适配,实现适配器在子类中进行具体的适配转换。就以手机充电为例,现在手机的电压范围在 22V ,但是家用电压太高,不能够直接充,于是就去商店买了一个充电器,充电器能将电压限制到 22V,于是乎就可以为手机充电了。

  • 源角色

    public class NormalVoltage {
        public int output220() {
            return 220;
        }
    }
    
  • 适配器

    public interface Voltage22C {
        public int output22();
    }
    
  • 目标角色

    public class PowerAdapter extends NormalVoltage implements Voltage22C{
        @Override
        public int output22() {
            return super.output220() / 11;
        }
    }
    

 7.2、对象适配器

 和类适配器的实现差不多,只是从继承变成了拿到源角色的引用,用聚合关系来代替继承。代码只需要修改目标角色即可,在成员变量中添加源角色引用,直接进行输出调用。

  • 目标角色

    public class PowerAdapter implements Voltage22C{
        private NormalVoltage normalVoltage;
        
        public PowerAdapter(NormalVoltage normalVoltage) {
            this.normalVoltage = normalVoltage;
        }
        @Override
        public int output22() {
            return normalVpltage.output220() / 11;
        }
    }
    

 7.3、接口适配器

 接口适配主要解决的问题和类适配不一样,它主要关注对于接口方法特别多的实现,但是子类并不需要这么多的实现,即使只是实现一个空方法也会显得类很臃肿,因此需要一个中间的适配器来实现接口,而实际的对象只需要继承这个适配器来调用具体的功能方法就可以了。

  • 接口定义

    public interface VoltageAdaptation {
        public int output22();
        public int output44();
        public int output88();
        public int output200();
    }
    
  • 接口适配

    public class PowerAdapter implements VoltageAdaptation{
        private NormalVoltage normalVoltage;
    
        public PowerAdapter(NormalVoltage normalVoltage) {
            this.normalVoltage = normalVoltage;
        }
    
        @Override
        public int output22() {
            return normalVoltage.output220() / 11;
        }
    
        @Override
        public int output44() {
            return 0;
        }
    
        @Override
        public int output88() {
            return 0;
        }
    
        @Override
        public int output200() {
            return 0;
        }
    }
    
  • 实际适配器

    public class PowerNeed extends PowerAdapter{
        public PowerNeed(NormalVoltage normalVoltage) {
            super(normalVoltage);
        }
    
        @Override
        public int output22() {
            return super.output22();
        }
    }
    

 适配器能够实现对已有类的复用,在不改变原有类的情况下进行功能的拓展,在很多场景下都符合开闭原则,但是进行适配后的类会使功能变得复杂,降低了代码的可阅读性,可能会增加系统的复杂性。


8、桥接模式

 当一个类内部具有多维度的变化,而且相同类都有同样的变换,这时候我们就可以使用桥接模式去解耦这些变换,让每个类都可以去接入,使得高层的代码架构更加稳定。

  1. 在抽象和具体实现之间需要增加更多的灵活场景;
  2. 一个类存在多种变换维度;
  3. 不希望使用继承来实现使得系统的类数量增加。

 桥接模式主要包含四个角色:

角色作用
抽象持有一个具体实现的引用
修正抽象实现抽象,提供真正的业务处理
实现目标的基本操作定义接口或抽象
具体实现目标对象的具体实现

 在我们正常的开发周期中都需要进行日志的打印,用于系统的分析,一般分为正常、警告、调试、错误日志,而保存日志的方式也有很多,有文件、关系数据库、非关系数据库等保存。这样的一个类就是在内部存在着多维度的变化,就可以使用桥接模式来完成维度的变化,例如将日志保存方式设置为桥接,日志对象经过不同的接入即可完成不同方式的存储。

log.jpg

  • 实现、具体实现

    public interface ILog {
        public void recordLog(String log);
    }
    
    public class InfoLog implements ILog{
        @Override
        public void recordLog(String log) {
            //  正常日志
        }
    }
    
    public class WarningLog implements ILog{
        @Override
        public void recordLog(String log) {
            //  警告日志
        }
    }
    
  • 抽象、修正抽象

    public interface ILogBridge {
        public default void save(String log) {
            saveLog(log);
        }
        public void saveLog(String log);
    }
    
    public class FileLog implements ILogBridge{
        private ILog log;
    
        public FileLog(ILog log) {
            this.log = log;
        }
        @Override
        public void saveLog(String log) {
            this.log.recordLog(log);
            //  保存日志
            System.out.println("保存日志到文件 " + log);
        }
    }
    

 定义完成之后不同保存方式只需要经过不同的桥接器,然后进行调用保存即可,不会影响到原始目标对象的功能使用。

  • 优点
    1. 分离抽象和实现
    2. 符合开闭原则
    3. 提高系统的可扩展性
  • 缺点
    1. 增加系统的理解与设计难度
    2. 需要正确的识别系统两个独立变换的维度