二、结构型模式

代理模式

代理模式(Proxy Design Pattern)它在不改变原始类(或叫被代理类)代码的情况下,通过引入代理类来给原始类附加功能。

  • 静态代理 代理类和原始类需要实现相同的接口(组合+委托)
    代理类继承原始类,然后扩展附加功能

  • 动态代理
    动态代理底层依赖的就是 Java 的反射语法

public class MetricsCollectorProxy {
    
    
    private MetricsCollector metricsCollector;

    public MetricsCollectorProxy() {
    
    
        this.metricsCollector = new MetricsCollector();
    }

    public Object createProxy(Object proxiedObject) {
    
    
//        获取所有接口的Class
        Class<?>[] interfaces = proxiedObject.getClass().getInterfaces();
        InvocationHandler handler = new DynamicProxyHandler(proxiedObject);
        return Proxy.newProxyInstance(proxiedObject.getClass().getClassLoader(), interfaces, handler);
    }

    //    代理类
    private class DynamicProxyHandler implements InvocationHandler {
    
    
        //        被代理的对象,实际的方法执行者
        private Object proxiedObject;

        public DynamicProxyHandler(Object proxiedObject) {
    
    
            this.proxiedObject = proxiedObject;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    
    

            long startTimestamp = System.currentTimeMillis();
            Object result = method.invoke(proxiedObject, args);
            long endTimeStamp = System.currentTimeMillis();
            long responseTime = endTimeStamp - startTimestamp;
            String apiName = proxiedObject.getClass().getName() + ":" + method.getName;
            RequestInfo requestInfo = new RequestInfo(apiName, responseTime, startTimestamp);
            metricsCollector.recordRequest(requestInfo);
            return result;
        }
    }
}

//MetricsCollectorProxy使用举例
//MetricsCollectorProxy proxy = new MetricsCollectorProxy();
//IUserController userController = (IUserController) proxy.createProxy(new UserController());
  • 应用场景:
    1、业务系统中开发一些非功能性需求,比如:监控、统计、鉴权、限流、事务、幂等、日志。我们将这些附加功能与业务功能解耦,放到代理类中统一处理。
    2、 代理模式在 RPC、缓存中的应用(原方法不走缓存,代理方法判断是否走缓存,请求中带有支持缓存的字段便从缓存中获取数据直接返回)

桥接模式

这个模式有两种不同的理解方式:

1、“将抽象和实现解耦,让它们可以独立变化。”
2、一个类存在两个(或多个)独立变化的维度,我们通过组合的方式,让这两个(或多个)维度可以独立进行扩展。

  • JDBC 驱动是桥接模式的经典应用
// com.mysql.jdbc.Driver 的代码实现
package com.mysql.jdbc;

import java.sql.SQLException;

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    
    
    static {
    
    
        try {
    
    
            java.sql.DriverManager.registerDriver(new Driver());
        } catch (SQLException E) {
    
    
            throw new RuntimeException("Can't register driver!");
        }
    }

    /**
     * Construct a new driver and register it with DriverManager
     * @throws SQLException if a database error occurs.
     */
    public Driver() throws SQLException {
    
    
// Required for Class.forName().newInstance()
    }
}

当执行Class.forName(“com.mysql.jdbc.Driver”) 这条语句的时候,实际上是做了两件事情。第一件事情是要求 JVM 查找并加载指定的 Driver 类,第二件事情是执行该类的静态代码,也就是将 MySQL
Driver 注册到 DriverManager 类中。

当我们把具体的 Driver 实现类(比如,com.mysql.jdbc.Driver)注册到 DriverManager 之后,后续所有对 JDBC 接口的调用,都会委派到对具体的 Driver 实现类来执行。而 Driver
实现类都实现了相同的接口(java.sql.Driver ),这也是可以灵活切换 Driver 的原因。

  • 在 JDBC 这个例子中,什么是“抽象”?什么是“实现”呢?

    JDBC 本身就相当于“抽象”。注意,这里所说的“抽象”,指的并非“抽象类”或“接口”,而是跟具体的数据库无关的、被抽象出来的一套“类库”。具体的 Driver(比如,com.mysql.jdbc.Driver)就相当于“实现”。注意,这里所说的“实现”,也并非指“接口的实现类”,而是跟具体数据库相关的一套“类库”。JDBC 和 Driver 独立开发,通过对象之间的组合关系,组装在一起。JDBC 的所有逻辑操作,最终都委托给 Driver 来执行。
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jxpVaCl7-1663489165586)(…/src/main/resources/pic/桥接模式JDBC.png)]

装饰器模式

装饰器模式主要解决继承关系过于复杂的问题,通过组合来替代继承。它主要的作用是给原始类添加增强功能。可以对原始类嵌套使用多个装饰器。为了满足这个应用场景,在设计的时候,装饰器类需要跟原始类继承相同的抽象类或者接口。

  • Java IO类库源码学习装饰器模式

用组合替代继承
从 Java IO 的设计来看,装饰器模式相对于简单的组合关系,还有两个比较特殊的地方:

第一个比较特殊的地方是:装饰器类和原始类继承同样的父类,这样我们可以对原始类“嵌套”多个装饰器类。比如,下面这样一段代码,我们对 FileInputStream 嵌套了两个装饰器类:BufferedInputStream 和 DataInputStream,让它既支持缓存读取,又支持按照基本数据类型来读取数据。

 InputStream in = new FileInputStream("/user/wangzheng/test.txt");
 InputStream bin = new BufferedInputStream(in);
 DataInputStream din = new DataInputStream(bin);
 int data = din.readInt();

第二个比较特殊的地方是:装饰器类是对功能的增强,这也是装饰器模式应用场景的一个重要特点。

适配器模式

适配器模式: 它将不兼容的接口转换为可兼容的接口,让原本由于接口不兼容而不能一起工作的类可以一起工作。

  • 两种实现方式

    • 类适配器(类适配器使用继承关系来实现)
      // 类适配器: 基于继承
      public interface ITarget {
          
          
        void f1();
      
        void f2();
      
        void fc();
      }
      
      public class Adaptee {
          
          
        public void fa() {
          
          
          //... 
        }
      
        public void fb() {
          
          
          //... 
        }
      
        public void fc() {
          
          
          //... 
        }
      }
      
      public class Adaptor extends Adaptee implements ITarget {
          
          
        public void f1() {
          
          
          super.fa();
        }
      
        public void f2() {
          
          
      //...重新实现f2()...
        }
      // 这里fc()不需要实现,直接继承自Adaptee,这是跟对象适配器最大的不同点
      }
    
    • 对象适配器(对象适配器使用组合关系来实现)
    // 对象适配器:基于组合
    public interface ITarget {
          
          
        void f1();
    
        void f2();
    
        void fc();
    }
    
    public class Adaptee {
          
          
        public void fa() {
          
           
            //... 
        }
    
        public void fb() {
          
           
            //... 
        }
    
        public void fc() {
          
           
            //... 
        }
    }
    
    public class Adaptor implements ITarget {
          
          
      private Adaptee adaptee;
      public Adaptor(Adaptee adaptee) {
          
          
        this.adaptee = adaptee;
      }
      public void f1() {
          
          
        adaptee.fa(); //委托给Adaptee
      }
      public void f2() {
          
          
    //...重新实现f2()...
      }
      public void fc() {
          
          
        adaptee.fc();
      }
    }
    
    • 在实际的开发中,到底该如何选择使用哪一种呢?判断的标准主要有两个,一个是 Adaptee 接口的个数,另一个是 Adaptee 和 ITarget 的契合程度。

    1、如果 Adaptee 接口并不多,那两种实现方式都可以。
    2、如果 Adaptee 接口很多,而且 Adaptee 和 ITarget 接口定义大部分都相同,那使用类适配器,因为 Adaptor 复用父类 Adaptee 的接口,比起对象适配器的实现方式,Adaptor 的代码量要少一些。
    3、如果 Adaptee 接口很多,而且 Adaptee 和 ITarget 接口定义大部分都不相同,那使用对象适配器,因为组合结构相对于继承更加灵活。

  • 适配器模式应用场景

  • 封装有缺陷的接口设计
public class CD {
     
      //这个类来自外部sdk,我们无权修改它的代码
//...
public static void staticFunction1() {
     
      //... }
public void uglyNamingFunction2() {
     
      //... }
public void tooManyParamsFunction3(int paramA, int paramB, ...) {
     
      //... }
public void lowPerformanceFunction4() {
     
      //... }
}
// 使用适配器模式进行重构
public class ITarget {
     
     
void function1();
void function2();
void fucntion3(ParamsWrapperDefinition paramsWrapper);
void function4();
//...
}
// 注意:适配器类的命名不一定非得末尾带Adaptor
public class CDAdaptor extends CD implements ITarget {
     
     
//...
public void function1() {
     
     
super.staticFunction1();
}
public void function2() {
     
     
super.uglyNamingFucntion2();
}
  public void function3(ParamsWrapperDefinition paramsWrapper) {
     
     
    super.tooManyParamsFunction3(paramsWrapper.getParamA(), ...);
  }
  public void function4() {
     
     
//...reimplement it...
  }
}
  • 统一多个类的接口设计
public class ASensitiveWordsFilter {
     
      // A敏感词过滤系统提供的接口
    //text是原始文本,函数输出用***替换敏感词之后的文本
    public String filterSexyWords(String text) {
     
     
// ...
    }

    public String filterPoliticalWords(String text) {
     
     
// ...
    }
}

public class BSensitiveWordsFilter {
     
      // B敏感词过滤系统提供的接口
    public String filter(String text) {
     
     
//...
    }
}

public class CSensitiveWordsFilter {
     
      // C敏感词过滤系统提供的接口
    public String filter(String text, String mask) {
     
     
        //...
    }
}

// 未使用适配器模式之前的代码:代码的可测试性、扩展性不好
public class RiskManagement {
     
     
    private ASensitiveWordsFilter aFilter = new ASensitiveWordsFilter();
    private BSensitiveWordsFilter bFilter = new BSensitiveWordsFilter();
    private CSensitiveWordsFilter cFilter = new CSensitiveWordsFilter();

    public String filterSensitiveWords(String text) {
     
     
        String maskedText = aFilter.filterSexyWords(text);
        maskedText = aFilter.filterPoliticalWords(maskedText);
        maskedText = bFilter.filter(maskedText);
        maskedText = cFilter.filter(maskedText, "***");
        return maskedText;
    }
}

// 使用适配器模式进行改造
public interface ISensitiveWordsFilter {
     
      // 统一接口定义
    String filter(String text);
}

public class ASensitiveWordsFilterAdaptor implements ISensitiveWordsFilter {
     
     
    private ASensitiveWordsFilter aFilter;

    public String filter(String text) {
     
     
        String maskedText = aFilter.filterSexyWords(text);
        maskedText = aFilter.filterPoliticalWords(maskedText);
        return maskedText;
    }
}

//...省略BSensitiveWordsFilterAdaptor、CSensitiveWordsFilterAdaptor...
// 扩展性更好,更加符合开闭原则,如果添加一个新的敏感词过滤系统,
// 这个类完全不需要改动;而且基于接口而非实现编程,代码的可测试性更好。
public class RiskManagement {
     
     
    private List<ISensitiveWordsFilter> filters = new ArrayList<>();

    public void addSensitiveWordsFilter(ISensitiveWordsFilter filter) {
     
     
        filters.add(filter);
    }

    public String filterSensitiveWords(String text) {
     
     
        String maskedText = text;
        for (ISensitiveWordsFilter filter : filters) {
     
     
            maskedText = filter.filter(maskedText);
        }
        return maskedText;
    }
}
  • 替换依赖的外部系统
// 外部系统A
public interface IA {
     
     
    //...
    void fa();
}

public class A implements IA {
     
     
    //...
    public void fa() {
     
     
        //... 
    }
}

// 在我们的项目中,外部系统A的使用示例
public class Demo {
     
     
    private IA a;

    public Demo(IA a) {
     
     
        this.a = a;
    }
//...
}
//Demo d = new Demo(new A());

// 将外部系统A替换成外部系统B
public class BAdaptor implements IA {
     
     
  private B b;
  public BAdaptor(B b) {
     
     
    this.b= b;
  }
  public void fa() {
     
     
//...
    b.fb();
  }
}
// 借助BAdaptor,Demo的代码中,调用IA接口的地方都无需改动,
// 只需要将BAdaptor如下注入到Demo即可。
//  Demo d = new Demo(new BAdaptor(new B()));
  • 兼容老版本接口
    在做版本升级的时候,对于一些要废弃的接口,我们不直接将其删除,而是暂时保留,并且标注为 deprecated,并将内部实现逻辑委托为新的接口实现。这样做的好处是,让使用它的项目有个过渡期,而不是强制进行代码修改。

  • 适配不同格式的数据
    在不同格式的数据之间的适配。比如,把从不同征信系统拉取的不同格式的征信数据,统一为相同的格式,以方便存储和使用。再比如,Java 中的 Arrays.asList() 也可以看作一种数据适配器,将数组类型的数据转化为集合容器类型。
    List stooges = Arrays.asList(“Larry”, “Moe”, “Curly”);

  • 配器模式在 Java 日志中的应用 Slf4j

Slf4j 不仅仅提供了从其他日志框架到 Slf4j 的适配器,还提供了反向适配器,也就是从 Slf4j 到其他日志框架的适配

门面模式

门面模式为子系统提供一组统一的接口,定义一组高层接口让子系统更易用。

为了保证接口的可复用性,我们需要将接口尽量设计得细粒度一点,但是,如果接口的粒度过小,在接口的使用者开发一个业务功能时,就会导致需要调用 n 多细粒度的接口才能完成。相反,如果接口粒度设计得太大,一个接口返回 n 多数据,要做 n 多事情,就会导致接口不够通用、可复用性不好。

  • 应用场景:
      1. 解决易用性问题

      系统 A,提供了 a、b、c、d 四个接口。系统 B 完成某个业务功能,需要调用 A 系统的 a、b、d 接口。利用门面模式,我们提供一个包裹 a、b、d 接口调用的门面接口 x,给系统 B 直接使用。

      1. 解决性能问题

      通过将多个接口调用替换为一个门面接口调用,减少网络通信成本,提高客户端的响应速度。

      1. 解决分布式事务问题

      在一个金融系统中,有两个业务领域模型,用户和钱包。这两个业务领域模型都对外暴露了一系列接口,比如用户的增删改查接口、钱包的增删改查接口。假设有这样一个业务场景:在用户注册的时候,我们不仅会创建用户(在数据库 User 表中),还会给用户创建一个钱包(在数据库的 Wallet 表中)。
      最简单的解决方案是,利用数据库事务或者 Spring 框架提供的事务,在一个事务中,执行创建用户和创建钱包这两个 SQL 操作。这就要求两个 SQL 操作要在一个接口中完成,所以,我们可以借鉴门面模式的思想,再设计一个包裹这两个操作的新接口,让新接口在一个事务中执行两个 SQL 操作。

组合模式

将一组对象组织(Compose)成树形结构,以表示一种“部分 - 整体”的层次结构。组合让客户端可以统一单个对象和组合对象的处理逻辑。

组合模式,将一组对象组织成树形结构,将单个对象和组合对象都看做树中的节点,以统一处理逻辑,并且它利用树形结构的特点,递归地处理每个子树,依次简化代码实现。使用组合模式的前提在于,你的业务场景必须能够表示成树形结构。所以,组合模式的应用场景也比较局限,它并不是一种很常用的设计模式。

享元模式

享元模式的意图是复用对象,节省内存,前提是享元对象是不可变对象。

  • 享元模式的实现:

享元模式的代码实现非常简单,主要是通过工厂模式,在工厂类中,通过一个 Map 或者 List 来缓存已经创建好的享元对象,以达到复用的目的。

  • 享元模式 vs 单例、缓存、对象池
  • 享元模式 vs 单例

在单例模式中,一个类只能创建一个对象,而在享元模式中,一个类可以创建多个对象,每个对象被多处代码引用共享。


享元模式有点类似于之前讲到的单例的变体:多例。

从代码实现上来看,享元模式和多例有很多相似之处,但从设计意图上来看,它们是完全不同的。应用享元模式是为了对象复用,节省内存,而应用多例模式是为了限制对象的个数

  • 享元模式 vs 缓存

在享元模式的实现中,我们通过工厂类来“缓存”已经创建好的对象。这里的“缓存”实际上是“存储”的意思,为了复用 ,跟我们平时所说的“数据库缓存”“CPU 缓存”“MemCache 缓存”是两回事。我们平时所讲的缓存,主要是为了提高访问效率,而非复用。

  • 享元模式 vs 对象池

虽然对象池、连接池、线程池、享元模式都是为了复用,但是,池化技术中的“复用”可以理解为“重复使用”,主要目的是节省时间(比如从数据库池中
取一个连接,不需要重新创建)。在任意时刻,每一个对象、连接、线程,并不会被多处使用,而是被一个使用者独占,当使用完成之后,放回到池中,再由其他使用者重复利用。享元模式中的“复用”可以理解为“共享使用”,在整个生命周期中,都是被所有使用者共享的,主要目的是节省空间

  • 享元模式在Java Integer、String中的应用

    • 享元模式在包装器类型(Java Integer)中的应用

      Integer 用到了享元模式来复用对象,当通过自动装箱,也就是调用 valueOf() 来创建 Integer 对象的时候,如果要创建的 Integer 对象的值在 -128 到 127 之间,会从 IntegerCache 类中直接返回,否则才调用 new 方法创建。
      JDK 也提供了方法来让我们可以自定义缓存的最大值:可以用如下方式,将缓存的最大值从 127 调整到 255

      方法一: -Djava.lang.Integer.IntegerCache.high=255
      方法二: -XX:AutoBoxCacheMax=255

    • 享元模式在 Java String 中的应用

    String 类利用享元模式来复用相同的字符串常量。JVM 会专门开辟一块存储区来存储字符串常量,这块存储区叫作“字符串常量池”。
    ava Integer)中的应用
    Integer 用到了享元模式来复用对象,当通过自动装箱,也就是调用 valueOf() 来创建 Integer 对象的时候,如果要创建的 Integer 对象的值在 -128 到 127 之间,会从 IntegerCache 类中直接返回,否则才调用 new 方法创建。
    JDK 也提供了方法来让我们可以自定义缓存的最大值:可以用如下方式,将缓存的最大值从 127 调整到 255

    方法一: -Djava.lang.Integer.IntegerCache.high=255
    方法二: -XX:AutoBoxCacheMax=255

    • 享元模式在 Java String 中的应用

    String 类利用享元模式来复用相同的字符串常量。JVM 会专门开辟一块存储区来存储字符串常量,这块存储区叫作“字符串常量池”。
    String 类的享元模式的设计,跟 Integer 类稍微有些不同。Integer 类中要共享的对象,是在类加载的时候,就集中一次性创建好的。但是,对于字符串来说,我们没法事先知道要共享哪些字符串常量,所以没办法事先创建好,只能在某个字符串常量第一次被用到的时候,存储到常量池中,当之后再用到的时候,直接引用常量池中已经存在的即可,就不需要再重新创建了。

猜你喜欢

转载自blog.csdn.net/weixin_46488959/article/details/126919143