返回 导航

其他

hangge.com

Java - GoF设计模式详解6(适配器模式)

作者:hangge | 2023-03-16 10:41

六、适配器模式

1,基本介绍

(1)适配器模式(Adapter)可以解决系统间接口不相容的问题。通过适配器可以把类的接口转化为用户所希望的接口,从而提高复用性。

(2)该模式中包含的角色及其职责如下:
  • 目标接口Target):客户所期待的接口。目标可以是具体的或抽象的类,也可以是接口。
  • 被适配者Adaptee):需要适配的类。
  • 适配器Adapter):它是一个转换器,通过继承或引用适配者的对象,把原接口转换成目标接口。

(3)适配器模式的实现主要分为三类:类适配器模式、对象适配器模式、接口适配器模式(或又称作缺省适配器模式)。
  • 类适配器模式:该模式通过创建一个继承原有 Adaptee 类(需要扩展的类)并实现新接口的适配器类来实现。
  • 对象适配器模式:该模式不再继承 Adaptee 类,而是持有 Adaptee 类的实例,以解决兼容性问题。
  • 接口适配器模式(缺省适配器模式):在不希望实现一个接口中所有的方法时,可以创建一个抽象类 AbstractAdapter 实现所有方法,这样继承了该抽象类的子类就可以选择性的重写其中的方法即可。

2,类适配器模式

(1)假设我们有一个待适配的类 Thermometer,它可以返回当前的摄氏温度:
public class Thermometer {
  public double getCelsius() {
    return 105L;
  }
}

(2)而我们系统需要的是华氏温度,这个可以使用适配器模式在不修改原有类的情况下实现。首先定义一个目标接口 TemperatureTarget,并在接口中定义获取两种温度的方法:
public interface TemperatureTarget {
  double getCelsius();
  double getFahrenheit();
}

(3)然后定义一个适配器类 TemperatureAdapter 继承 Thermometer 类并实现 TemperatureTarget 接口,适配后的类既可以获取摄氏温度,也可以获取华氏温度。
public class TemperatureAdapter extends Thermometer implements TemperatureTarget{
  @Override
  public double getFahrenheit() {
    return super.getCelsius() * 1.8 + 32;
  }
}

(4)最后测试一下:
public class Test {
  public static void main(String[] args) {
    TemperatureTarget target = new TemperatureAdapter();
    System.out.println("当前摄氏温度:" + target.getCelsius());
    System.out.println("当前华氏温度:" + target.getFahrenheit());
  }
}

3,对象适配器模式

(1)该模式适配器类不再继承 Adaptee 类,而是持有 Adaptee 类的实例,具体代码如下(待适配的类和目标接口仍和上面一样不变)。
public class TemperatureAdapter implements TemperatureTarget{
  private Thermometer thermometer;

  public TemperatureAdapter(Thermometer thermometer) {
    this.thermometer = thermometer;
  }

  @Override
  public double getCelsius() {
    return this.thermometer.getCelsius();
  }

  @Override
  public double getFahrenheit() {
    return this.thermometer.getCelsius() * 1.8 + 32;
  }
}

(2)测试一下,效果同上面是一样的:
public class Test {
  public static void main(String[] args) {
    Thermometer adaptee = new Thermometer();
    TemperatureTarget target = new TemperatureAdapter(adaptee);
    System.out.println("当前摄氏温度:" + target.getCelsius());
    System.out.println("当前华氏温度:" + target.getFahrenheit());
  }
}

4,接口适配器模式(缺省适配器模式)

(1)如果不希望实现一个接口中所有的方法时,可以创建一个适配器抽象类实现所有方法,具体代码如下(待适配的类和目标接口仍和上面一样不变)
public abstract class AbsTemperatureAdapter implements TemperatureTarget {
  @Override
  public double getCelsius() {
    return 0;
  }

  @Override
  public double getFahrenheit() {
    return 0;
  }
}

(2)接着继承抽象类定义一个具体的适配器实现类,里面只实现需要的方法:
public class FahrenheitTemperatureAdapter extends AbsTemperatureAdapter {
  private Thermometer thermometer;

  public FahrenheitTemperatureAdapter(Thermometer thermometer) {
    this.thermometer = thermometer;
  }

  @Override
  public double getFahrenheit() {
    return this.thermometer.getCelsius() * 1.8 + 32;
  }
}

(3)测试一下:
public class Test {
  public static void main(String[] args) {
    Thermometer adaptee = new Thermometer();
    AbsTemperatureAdapter target = new FahrenheitTemperatureAdapter(adaptee);
    System.out.println("当前华氏温度:" + target.getFahrenheit());
  }
}

(4)当然我们也可以不定义一个具体的抽象适配器的子类,而是用匿名内部类来对想要的接口方法进行改造,即可完成接口适配。
public class Test {
  public static void main(String[] args) {
    Thermometer adaptee = new Thermometer();

    AbsTemperatureAdapter target = new AbsTemperatureAdapter() {
      @Override
      public double getFahrenheit() {
        return adaptee.getCelsius() * 1.8 + 32;
      }
    };

    System.out.println("当前华氏温度:" + target.getFahrenheit());
  }
}

附一:JDK 中的适配器模式

1,InputStreamReader 和 OutputStreamWriter 类

(1)java.io.InputStreamReaderjava.io.OutputStreamWriter 类使用适配器模式来将字节流转换为字符流,这样可以在读写文件的时候更方便的使用字符流的 API,同时又可以利用字节流的优势,如更高的效率和对二进制数据的支持。
  • InputStreamReader 将字节输入流转换为字符输入流。它接受一个 InputStream 对象作为参数,并使用指定的字符集将其转换为字符流。这样可以在读取字节的同时进行字符编码转换。
  • OutputStreamWriter 类也是一个适配器,用于将字符输出流转换为字节输出流。它接受一个 OutputStream 对象作为参数,并使用指定的字符集将其转换为字节流。这样可以在写入字符的同时进行字符编码转换。

(2)下面是一个简单的使用样例:
  • 当我们需要读取一个文件并将其转换为字符串时,可以使用 InputStreamReader 将字节输入流转换为字符输入流,然后使用 BufferedReader readLine() 方法读取文件中的每一行,最后使用 StringBuilder 将读取到的每一行拼接起来。
  • 同样的,当我们需要将字符串写入文件时,可以使用 OutputStreamWriter 将字符输出流转换为字节输出流,然后使用 OutputStreamWriter write() 方法将字符串写入文件。
public class Test {
  public static void main(String[] args) throws IOException {
    // 使用 InputStreamReader 从文件中读取数据
    InputStreamReader isr = new InputStreamReader(new FileInputStream("input.txt"), "UTF-8");
    BufferedReader br = new BufferedReader(isr);

    // 使用 OutputStreamWriter 向文件中写入数据
    OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("output.txt"), "UTF-8");

    String line;
    StringBuilder sb = new StringBuilder();
    while((line = br.readLine()) != null) {
      sb.append(line);
      sb.append("\n");
    }
    String fileContent = sb.toString();
    System.out.println("原文件内容: \n" + fileContent);

    // 将修改后的内容写入新文件
    osw.write("新的文件内容: \n" + fileContent);
    osw.close();
    br.close();
  }
}

2,Arrays.asList() 方法

(1)java.util.Arrays asList() 方法是一个静态方法,它可以将一个数组转换为一个 List。该方法使用适配器模式来实现这个功能,因为 List 接口和数组之间没有继承关系。具体来说,该方法会创建一个 Arrays.ArrayList 类的实例,该类实现了 List 接口。这个 ArrayList 类包装了传入的数组,并重写了 List 接口的方法,使得其可以像普通的 List 一样进行操作。
注意:通过 Arrays.asList() 方法得到的 List 是一个只读的 List,不能在上面进行增加、删除、修改等操作,否则会抛出 UnsupportedOperationException 异常。如果需要在 List 上进行这些操作,可以将其转换为 ArrayList 等可变集合。
(2)下面是一个简单的使用样例:
String[] arr = {"a", "b", "c"};
List<String> list = Arrays.asList(arr);

3,Collections.enumeration() 方法

(1)java.util.Collectionsenumeration() 方法是一个静态方法,它可以将一个 Collection 转换为一个 Enumeration。具体来说,Collections.enumeration() 方法会创建一个 Enumeration 接口的实现类,该类包装了传入的 Collection,并重写了 Enumeration 接口的方法,使得其可以像普通的 Enumeration 一样进行操作。
说明java.util.Enumeration JDK 1.0 提供用于遍历容器类的接口,但是一个公认的设计失误,所以 JDK 1.2 对其进行了重构,新增 Iterator 接口去迭代容器类。 JDK 为了保证向后兼容,就在容器工具类 java.util.Collectionsenumeration 方法中使用了适配器模式。

(2)下面是一个简单样例:
public class Test {
  public static void main(String[] args) throws IOException {
    List<String> list = Arrays.asList("a","b","c");
    Enumeration<String> enumeration = Collections.enumeration(list);
    while (enumeration.hasMoreElements()) {
      System.out.println(enumeration.nextElement());
    }
  }
}

附二:Spring 中的适配器模式

1,Spring AOP 的适配器模式

(1)Spring AOP 的实现是基于代理模式,但是 Spring AOP 的增强或通知(Advice)使用到了适配器模式,与之相关的接口是 AdvisorAdapter,其代码如下:
public interface AdvisorAdapter {
  boolean supportsAdvice(Advice var1);

  MethodInterceptor getInterceptor(Advisor var1);
}

(2)AdvisorAdapter 有三个具体实现类:MethodBeforeAdviceAdapterAfterReturningAdviceAdapter ThrowsAdviceAdapter。比如 MethodBeforeAdviceAdapter 代码如下:
class MethodBeforeAdviceAdapter implements AdvisorAdapter, Serializable {
  MethodBeforeAdviceAdapter() {
  }

  public boolean supportsAdvice(Advice advice) {
    return advice instanceof MethodBeforeAdvice;
  }

  public MethodInterceptor getInterceptor(Advisor advisor) {
    MethodBeforeAdvice advice = (MethodBeforeAdvice)advisor.getAdvice();
    return new MethodBeforeAdviceInterceptor(advice);
  }
}

(3)Advice 常用的类型有:BeforeAdvice(目标方法调用前,前置通知)、AfterAdvice(目标方法调用后,后置通知)、AfterReturningAdvice(目标方法执行结束后,return 之前)等等。每个类型 Advice(通知)都有对应的拦器:MethodBeforeAdviceInterceptorAfterReturningAdviceAdapterAfterReturningAdviceInterceptor。比如 MethodBeforeAdviceInterceptor 代码如下:
public class MethodBeforeAdviceInterceptor implements MethodInterceptor, BeforeAdvice, Serializable {
  private final MethodBeforeAdvice advice;

  public MethodBeforeAdviceInterceptor(MethodBeforeAdvice advice) {
    Assert.notNull(advice, "Advice must not be null");
    this.advice = advice;
  }

  public Object invoke(MethodInvocation mi) throws Throwable {
    this.advice.before(mi.getMethod(), mi.getArguments(), mi.getThis());
    return mi.proceed();
  }
}

(4)Spring 预定义的通知要通过对应的适配器,适配成 MethodInterceptor 接口(方法拦截器)类型的对象,才能被添加到代理链中,从而实现对目标方法的增强。

2,Spring MVC 中使用的适配器模式

(1)适配器模式在 Spring MVC 中的使用主要包括两个方面:
  • 一是通过处理器适配器(Handler Adapter)将多种不同类型的处理器(Handler)适配到统一的接口,以便在 Spring MVC 中进行统一处理;
  • 二是通过处理器映射器(Handler Mapping)将请求与处理器之间建立映射关系,以便确定请求对应的处理器,并进行相应的处理。

(2)接下来讲解下具体流程。Spring MVC 中,DispatcherServlet 是一个核心的 Servlet,它负责接收请求,并将请求分发给相应的处理器进行处理。当 DispatcherServlet 接收到请求时,它会根据请求信息调用 HandlerMapping 来解析请求对应的处理器。在 HandlerMapping 中,会根据配置的映射规则来确定请求对应的处理器,然后返回给 DispatcherServlet
处理器映射器说明:
  • 处理器映射器(Handler Mapping)指的是用于建立请求与处理器之间映射关系的对象。处理器映射器的作用是根据配置的映射规则,将请求映射到相应的处理器,从而确定请求对应的处理器。
  • Spring MVC 中,处理器映射器通常实现为一个接口,该接口规定了如何建立请求与处理器之间的映射关系。处理器映射器的具体实现类可以根据不同的需求采用不同的方式来建立映射关系,例如根据请求的 URL、请求的参数等信息来建立映射关系。
  • 通过使用处理器映射器,可以让不同类型的请求都能够找到相应的处理器,从而确保请求能够被正确处理。此外,处理器映射器还能够提高 Spring MVC 的可扩展性,使开发人员能够轻松地添加新的请求和处理器,并让它们能够在 Spring MVC 中顺利协作工作。

(3)在 Spring MVC 中,处理器(Handler)通常指的是用于处理请求的对象。在 Spring MVC 中,处理器可以是控制器(Controller)、视图解析器(View Resolver)等类型的对象。但由于处理器种类放多,比如 Controller 就有如下这些:


(4)如果 DispatcherServlet 获取对应类型的 Handler 后直接调用 Handler 进行处理的话,由于不同 Handler 的方法不同,就需要进行类型判断,像下面这段代码一样。这样如果我们增加一个新的 Handler,就要在代码中加入一行 if 判断,这就使得程序难以维护,也违反了设计模式中的开闭原则(对扩展开放,对修改关闭)。
if(mappedHandler.getHandler() instanceof AbstractUrlViewController){  
   ((AbstractUrlViewController)mappedHandler.getHandler()).xxx  
}else if(mappedHandler.getHandler() instanceof XXX){  
    ...  
}else if(...){  
   ...  

(5)SringMVC 的做法是,首先定义一个处理器适配器接口 HandlerAdapter
public interface HandlerAdapter {
  boolean supports(Object var1);

  @Nullable
  ModelAndView handle(HttpServletRequest var1, HttpServletResponse var2, Object var3) throws Exception;

  long getLastModified(HttpServletRequest var1, Object var2);
}

(6)然后具体的处理器适配器会根据不同的需求来实现 HandlerAdapter 接口,并在处理请求时调用对应的处理器进行处理。比如 SimpleControllerHandlerAdapter 这个适配器代码如下:
public class SimpleControllerHandlerAdapter implements HandlerAdapter {
  public SimpleControllerHandlerAdapter() {
  }

  public boolean supports(Object handler) {
    return handler instanceof Controller;
  }

  @Nullable
  public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    return ((Controller)handler).handleRequest(request, response);
  }

  public long getLastModified(HttpServletRequest request, Object handler) {
    return handler instanceof LastModified ? ((LastModified)handler).getLastModified(request) : -1L;
  }
}
(7)当 Spring 容器启动后,会将所有定义好的适配器对象存放在一个 List 集合中,当一个请求来临时,DispatcherServlet 会通过 handler 的类型找到对应适配器,然后就可以统一通过适配器的 handle() 方法来调用处理器中的用于处理请求的方法。
public class DispatcherServlet extends FrameworkServlet {
    private List<HandlerAdapter> handlerAdapters;
    
    //初始化handlerAdapters
    private void initHandlerAdapters(ApplicationContext context) {
        //..省略...
    }
    
    // 遍历所有的 HandlerAdapters,通过 supports 判断找到匹配的适配器
    protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
		for (HandlerAdapter ha : this.handlerAdapters) {
			if (logger.isTraceEnabled()) {
				logger.trace("Testing handler adapter [" + ha + "]");
			}
			if (ha.supports(handler)) {
				return ha;
			}
		}
	}
	
	// 分发请求,请求需要找到匹配的适配器来处理
	protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
		HttpServletRequest processedRequest = request;
		HandlerExecutionChain mappedHandler = null;
 
		// Determine handler for the current request.
		mappedHandler = getHandler(processedRequest);
			
		// 确定当前请求的匹配的适配器.
		HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
 
		ha.getLastModified(request, mappedHandler.getHandler());
					
		mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
    }
	// ...省略...
}	

(8)通过使用适配器模式,可以提高 Spring MVC 的灵活性和可扩展性,使开发人员能够轻松地添加新的处理器和请求,并且能够让多种不同的处理器在 Spring MVC 中进行协作工作。
评论

全部评论(0)

回到顶部