IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    设计模式之美-课程笔记32-适配器模式

    10k发表于 2023-08-31 00:00:00
    love 0

    适配器模式

    • 两种实现:类适配器和对象适配器
    • 5种应用场景
    • slf4j日志框架中的适配器模式
    • 代理、桥接、装饰器、适配器的对比

    原理与实现

    1. 用来做适配的,将不兼容的接口转换为可兼容的接口;
    2. 两种实现方式:
      1. 类适配器通过使用继承关系实现
      2. 对象适配器使用组合关系
    3. 看个例子: ITarget是我们想要最终接口的样子;Adaptee是不兼容ITarget接口定义的接口;Adaptor将Adaptee转化成一组符合ITarget接口定义的接口。
    // 类适配器: 基于继承
    public interface ITarget {
      void f1();
      void f2();
      void fc();
    }
    
    public class Adaptee {
      public void fa() { //... }
      public void fb() { //... }
      public void fc() { //... }
    }
    
    1. 基于继承
    public class Adaptor extends Adaptee implements ITarget {
      public void f1() {
        super.fa();
      }
      
      public void f2() {
        //...重新实现f2()...
      }
      
      // 这里fc()不需要实现,直接继承自Adaptee,这是跟对象适配器最大的不同点
    }
    
    1. 基于组合
    // 对象适配器:基于组合
    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();
      }
    }
    
    1. 如何选择?
      • 如果被适配的Adaptee接口不多,那两种都可以,不论是继承还是组合都不会实现或者重写很多的接口代码。
      • 如果接口比较多,但是只有小部分接口需要适配,那可以使用继承即类适配器,这时候改动代码要少些;
      • 如果接口比较多而且需要适配的接口也很多,这时使用组合即对象适配器会好些,组合这时候比较灵活。

    应用场景

    1. 如果在设计初期避免接口不兼容,就不会用到适配器模式,但是我们不知道后面怎么发展,也不知道别人会怎么用。
    2. 适配器模式是对这些不兼容使用的补偿。

    1. 封装”有缺陷“的接口设计

    我们以来的外部系统在接口设计方面不是很好(比如包含很多静态方法),引入后会影响自身代码的可测性。为了隔离缺陷,我们希望对外部接口二次封装,抽象出更好的接口,这个时候可以用适配器模式。

    public class CD { //这个类来自外部sdk,我们无权修改它的代码
      //...
      public static void staticFunction1() { //... }
      
      public void uglyNamingFunction2() { //... }
    
      public void tooManyParamsFunction3(int paramA, int paramB, ...) { //... }
      
      public void lowPerformanceFunction4() { //... }
    }
    
    // 使用适配器模式进行重构
    public interface 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...
      }
    }
    

    2. 统一多个类的接口设计

    某个功能依赖多个外部类,通过适配器模式将他们的接口适配为统一的接口定义。这样的好处是统一之后我们可以复用代码逻辑。

    假设我们的系统要对用户输入的文本内容做敏感词过滤,为了提高过滤的召回率,我们引入了多款第三方敏感词过滤系统,依次对用户输入的内容进行过滤,过滤掉尽可能多的敏感词。

    但是,每个系统提供的过滤接口都是不同的。这就意味着我们没法复用一套逻辑来调用各个系统。这个时候,我们就可以使用适配器模式,将所有系统的接口适配为统一的接口定义,这样我们可以复用调用敏感词过滤的代码。

    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;
      }
    }
    

    3. 替换依赖的外部系统

    当我们将项目中依赖的一个外部系统替换为另一个外部系统的时候,适配器模式可以减少对代码的改动。

    // 外部系统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 implemnts 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()));
    

    4. 兼容老版本接口

    1. 在做版本升级的时候,对于一些药费气的接口我们不直接将其删除,而是暂时保留,并且标注为deprecated,并将其内部逻辑委托为新的接口实现。让使用他的项目有个过渡期。
    2. 一个例子:JDK1.0中有个Enumeration类,2.0对他进行了重构改名为Iterator,并对其实现做了优化。如果将Enumeration直接在2.0删除,则使用Enumeration的地方都会报错。
    3. 我们需要修改所有使用到他的地方改为Iterator,如果使用的项目比较少还好改,但是很多的地方如果使用了,那就不太好弄。
    4. 所以我们需要暂时保留Enumeration以兼容使用低版本的代码。-》 不兼容升级。
    public class Collections {
      public static Emueration emumeration(final Collection c) {
        return new Enumeration() {
          Iterator i = c.iterator();
          
          public boolean hasMoreElments() {
            return i.hashNext();
          }
          
          public Object nextElement() {
            return i.next():
          }
        }
      }
    }
    

    5. 适配不同的数据

    1. 例如Java中的Arrays.asList可以看作是是一种数据适配器,将数组类型转换为集合容器类型。

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

    1. 各种日志框架由于没有统一的规范,接口都不是很统一。
    2. 比如项目中使用的某个组件使用log4j来打印日志,而项目本身使用的是logback。将组件引入项目后有两套不同接口的日志框架,需要不同的配置,管理变得复杂。我们需要统一日志打印框架。
    3. Slf4j相当于定义了统一的日志规范,只有接口,配合其他框架使用。
    4. 由于他出现晚于其他日志框架,所以他提供了针对不同日志框架的适配器。对不同日志框架的接口二次封装,适配成统一的Slf4j的接口定义。
    // slf4j统一的接口定义
    package org.slf4j;
    public interface Logger {
      public boolean isTraceEnabled();
      public void trace(String msg);
      public void trace(String format, Object arg);
      public void trace(String format, Object arg1, Object arg2);
      public void trace(String format, Object[] argArray);
      public void trace(String msg, Throwable t);
     
      public boolean isDebugEnabled();
      public void debug(String msg);
      public void debug(String format, Object arg);
      public void debug(String format, Object arg1, Object arg2)
      public void debug(String format, Object[] argArray)
      public void debug(String msg, Throwable t);
    
      //...省略info、warn、error等一堆接口
    }
    
    // log4j日志框架的适配器
    // Log4jLoggerAdapter实现了LocationAwareLogger接口,
    // 其中LocationAwareLogger继承自Logger接口,
    // 也就相当于Log4jLoggerAdapter实现了Logger接口。
    package org.slf4j.impl;
    public final class Log4jLoggerAdapter extends MarkerIgnoringBase
      implements LocationAwareLogger, Serializable {
      final transient org.apache.log4j.Logger logger; // log4j
     
      public boolean isDebugEnabled() {
        return logger.isDebugEnabled();
      }
     
      public void debug(String msg) {
        logger.log(FQCN, Level.DEBUG, msg, null);
      }
     
      public void debug(String format, Object arg) {
        if (logger.isDebugEnabled()) {
          FormattingTuple ft = MessageFormatter.format(format, arg);
          logger.log(FQCN, Level.DEBUG, ft.getMessage(), ft.getThrowable());
        }
      }
     
      public void debug(String format, Object arg1, Object arg2) {
        if (logger.isDebugEnabled()) {
          FormattingTuple ft = MessageFormatter.format(format, arg1, arg2);
          logger.log(FQCN, Level.DEBUG, ft.getMessage(), ft.getThrowable());
        }
      }
     
      public void debug(String format, Object[] argArray) {
        if (logger.isDebugEnabled()) {
          FormattingTuple ft = MessageFormatter.arrayFormat(format, argArray);
          logger.log(FQCN, Level.DEBUG, ft.getMessage(), ft.getThrowable());
        }
      }
     
      public void debug(String msg, Throwable t) {
        logger.log(FQCN, Level.DEBUG, msg, t);
      }
      //...省略一堆接口的实现...
    }
    

    所以在使用的时候我们可以通过Java的SPI动态指定日志框架实现。

    而且Slf4j提供了反向适配器,如果想从JCL框架切换到log4J,可以现将JCL切换为Slf4j,然后再将SLf4j切换为log4J。

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

    1. 都可称为Wrapper模式,即通过Wrapper类的二次封装原始类。他们主要区别还是在于使用场景或要解决的问题。
    2. 代理模式: 在不改变原始类接口的条件下,为原始类定义一个代理类。主要目的是控制访问,而非加强功能。
    3. 桥接模式:将接口部分和实现部分分离,从而让他们可以较为容易、也相对独立的加以改变。
    4. 装饰器模式:在不改变原始类接口的情况下,对原始类进行功能增强,并支持多个装饰器嵌套。
    5. 适配器模式:事后补救策略。适配器提供跟原始类不同的接口。


沪ICP备19023445号-2号
友情链接