设计模式---适配器模式

2021-11-15 15:25:07 浏览数 (1)

适配器模式

  • 适配器概念介绍
  • 介绍
  • 角色
  • 工作原理
  • 3种适配器模式
    • 类适配器模式演示
    • 对象适配器模式
      • 对象适配器的优点
    • 接口适配器模式
    • 综合小案例---使用类适配器模式
      • power--带转换的电压
      • adapter--适配器
      • FindAdapter--寻找合适的适配器
      • 测试
  • 适配器模式总结
    • 主要优点
    • 主要缺点
    • 适用场景
  • spring MVC中的适配器模式
    • springMVC处理请求流程
    • 请求处理方法中适配器模式部分源码探究
    • 总结
  • 参考文章

适配器概念介绍

1、不同国家的插座是有区别的,如果我们去国外旅游,需要带上国外的插头转换器,来能兼容国外的插座;

2、手机的耳机孔有圆头和扁头,如果扁头的耳机孔想接圆头的耳机就需要一个耳机的转换器;

上述所说的转换器,其实就是适配器;它是用来做兼容的;


介绍

适配器模式(Adapter Pattern):将一个接口转换成客户希望的另一个接口,使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)。适配器模式既可以作为类结构型模式,也可以作为对象结构型模式。

在适配器模式中,我们通过增加一个新的适配器类来解决接口不兼容的问题,使得原本没有任何关系的类可以协同工作。

根据适配器类与适配者类的关系不同,适配器模式可分为对象适配器和类适配器两种,在对象适配器模式中,适配器与适配者之间是关联(聚合)关系;在类适配器模式中,适配器与适配者之间是继承(或实现)关系。


角色

Target(目标抽象类):目标抽象类定义客户所需接口,可以是一个抽象类或接口,也可以是具体类。

Adapter(适配器类):适配器可以调用另一个接口,作为一个转换器,对Adaptee和Target进行适配,适配器类是适配器模式的核心,在对象适配器中,它通过继承Target并关联一个Adaptee对象使二者产生联系。

Adaptee(适配者类—适配接口):适配者即被适配的角色,它定义了一个已经存在的接口,这个接口需要适配,适配者类一般是一个具体类,包含了客户希望使用的业务方法,在某些情况下可能没有适配者类的源代码。

缺省适配器模式(Default Adapter Pattern):当不需要实现一个接口所提供的所有方法时,可先设计一个抽象类实现该接口,并为接口中每个方法提供一个默认实现(空方法),那么该抽象类的子类可以选择性地覆盖父类的某些方法来实现需求,它适用于不想使用一个接口中的所有方法的情况,又称为单接口适配器模式。


工作原理

  • 适配器模式:将一个类的接口转换成另一种接口,让原本接口不兼容的类可以兼容;
  • 从用户的角度看不到被适配者;
  • 用户调用适配器转化出来的目标接口方法,适配器再调用被适配者的相关接口方法;

3种适配器模式

  • 类适配器模式
  • 对象适配器模式
  • 接口适配器模式

类适配器模式演示

以生活中充电器为例,充电器本身相当于适配者 (Adapter),220V 交流电相当于被适配者,我们的目标(target) 想把220V交流电转成5V直流电

要适配的类,即需要将220v电压转化为5v电压

代码语言:javascript复制
//中国的电压220V
public class ChinaPower
{
    private final Integer outPut=220;
    public Integer getOutPut() {
        return outPut;
    }
}

适配器接口,只负责定义转化需要使用的业务逻辑方法,具体实现交由适配器类完成

代码语言:javascript复制
//将电压转化为5v---适配接口
public interface TransTo5V
{
    Integer transTo5V();
}

适配器类,继承了ChinaPower,并实现了适配器接口,负责实现讲220v电压转化为5v的具体业务逻辑代码实现

代码语言:javascript复制
//适配器类---实现适配器接口
public class ChinaAdapter extends ChinaPower implements TransTo5V
{
    //将220v电压转换为5v的
    @Override
    public Integer transTo5V()
    {
        //获得被适配类,即我们需要将220v电压转化为5v返回
        Integer output=super.getOutPut();
        //进行电压转换操作
        return output/44;
    }
}

Phone类,需要用到适配器进行兼容,这样才可以充电

代码语言:javascript复制
//手机需要5v的电压进行充电
public class Phone
{
    //通过适配器获得5v的电压
    public void charging(ChinaAdapter chinaAdapter)
    {
        if(5==chinaAdapter.transTo5V())
        {
            System.out.println("得到5v,充电中...");
        }
        else
        {
            System.out.println("电压过高,手机压力过大");
        }

    }
}

充电测试

代码语言:javascript复制
public class test
{
    @Test
    public void test()
    {
        Phone p=new Phone();
        p.charging(new ChinaAdapter());
    }
}

对象适配器模式

还是以上面的例子为例,这一次适配器类不再继承ChinaPower ,而是以聚合的方式来代替继承,符合设计模式中的"合成复用原则";java是单继承机制,这样可以保留对象继承权;

我们只需要修改适配器类即可:

代码语言:javascript复制
//适配器类---实现适配器接口
public class ChinaAdapter implements TransTo5V
{
    private ChinaPower chinaPower;
    //通过构造器,完成赋值
    public ChinaAdapter(ChinaPower chinaPower)
    {
     this.chinaPower=chinaPower;   
    }
    //将220v电压转换为5v的
    @Override
    public Integer transTo5V()
    {
        //获得被适配类,即我们需要将220v电压转化为5v返回
        Integer output=chinaPower.getOutPut();
        //进行电压转换操作
        return output/44;
    }
}

测试

代码语言:javascript复制
public class test
{
    @Test
    public void test()
    {
        Phone p=new Phone();
        p.charging(new ChinaAdapter(new ChinaPower()));
    }
}

对象适配器的优点

对象适配器和类适配器其实算是同一种思想,只不过实现方式不同。根据合成复用原则,使用组合替代继承, 所以它解决了类适配器必须继承被适配者的局限性问题;


接口适配器模式

  • 接口适配器模式(Default Adapter Pattern),也叫缺省适配器模式;
  • 核心思路:当不需要全部实现接口提供的方法时,可先设计一个抽象类实现接口,并为该接口中每个方法提供一个默认实现(空方法),该抽象类的子类可有选择地覆盖父类的某些方法来实现需求;
  • 适用于一个接口不想使用其所有的方法的情况;

定义一个适配器接口:

代码语言:javascript复制
public interface InterfaceTest {
	public void m1();
	public void m2();
	public void m3();
	public void m4();
}

抽象类 AbsAdapter 将 InterfaceTest 的方法进行默认实现,当子类需要使用适配器接口中的某个方法,而不是全部方法时,就可以通过继承抽象类,来完成对需要使用的特定方法重写操作即可,无需实现适配器接口里面的全部方法

代码语言:javascript复制
public abstract class AbsAdapter implements InterfaceTest {

	//默认实现
	public void m1() {}
	public void m2() {}
	public void m3() {}
	public void m4() {}
}

Client 调用接口,重写适配器抽象类方法

代码语言:javascript复制
public class Client {
	public static void main(String[] args) {
		
		AbsAdapter absAdapter = new AbsAdapter() {
			//只需要去覆盖我们 需要使用 接口方法
			@Override
			public void m1() {
				System.out.println("使用了m1的方法");
			}
		};
		
		absAdapter.m1();
	}
}

综合小案例—使用类适配器模式

power–带转换的电压

一个顶层接口Power

代码语言:javascript复制
public interface Power
{
     Integer getOutPut();
}

分支一: 中国的220v电压

代码语言:javascript复制
//中国的电压220V
public class ChinaPower implements Power
{
    private final Integer outPut=220;
    @Override
    public Integer getOutPut() {
        return outPut;
    }
}

分支二: 日本的110v电压

代码语言:javascript复制
//日本电压110v
public class JapenPower implements Power
{
    private final Integer output=110;
    @Override
    public Integer getOutPut() {
        return output;
    }
}

adapter–适配器

失配器接口–DC5Adapter

代码语言:javascript复制
//适配器接口
public interface DC5Adapter
{
    boolean support(Power power);
    Integer transTo5V(Power power);
}

适配器类—ChinaAdapter–只负责将220v电压进行转换的工作

代码语言:javascript复制
//该适配器负责将中国的220v电压转化为5v的电压
public class ChinaAdapter implements DC5Adapter
{
    //当前适配器只负责将220v电压转化为5v的功能实现
    private  static Integer voltage=220;
    //判断当前适配器能否胜任传入power电压的转化职责
    @Override
    public boolean support(Power power)
    {
      if(power.getOutPut().equals(voltage))
      return true;
      return false;
    }
    //将220v电压转换为5v的
    @Override
    public Integer transTo5V(Power power)
    {
        //获得被适配类,即我们需要将220v电压转化为5v返回
        Integer output=power.getOutPut();
        //进行电压转换操作
        return output/44;
    }
}

适配器类—JapenAdapter–只负责将110v电压进行转换的工作

代码语言:javascript复制
//该适配器负责将日本的110v电压转化为5v的电压
public class JapenAdapter implements DC5Adapter
{
    //当前适配器只负责将110v电压转化为5v的功能实现
    private  static Integer voltage=110;
    //判断是否支持将日本110v电压转化为5v电压的操作
    @Override
    public boolean support(Power power) {
        if(power.getOutPut()==voltage)
            return true;
        return false;
    }
    //将110v电压转换为5v的
    @Override
    public Integer transTo5V(Power power)
    {
        //获得被适配类,即我们需要将110v电压转化为5v返回
        Integer output=power.getOutPut();
        //进行电压转换操作
        return output/22;
    }
}

FindAdapter–寻找合适的适配器

代码语言:javascript复制
//手机需要5v的电压进行充电
public class FindAdapter
{
    //存放所有适配器的set集合
    private static final Set<DC5Adapter> DC5Adapters=new HashSet<>();
    //通过静态代码块进行初始化操作
    static
    {
        DC5Adapters.add(new ChinaAdapter());
        DC5Adapters.add(new JapenAdapter());
    }
    // 根据电压找合适的变压器
    public DC5Adapter getPowerAdapter(Power power)
    {
        DC5Adapter dc5Adapter=null;
        for(DC5Adapter da:DC5Adapters)
        {
            //如果遍历到当前电压合适的变压器就直接退出遍历
            if(da.support(power))
            {
                dc5Adapter=da;
                break;
            }
        }
        //如果遍历完所有的变压器都没有找到合适的,就抛出异常
        if(dc5Adapter==null)
        {
            throw  new IllegalArgumentException("未能找到合适的变压器");
        }
        //返回找到的合适的变压器
        return dc5Adapter;
    }
}

测试

代码语言:javascript复制
public class test
{
    @Test
    public void test()
    {
        //找寻合适的变压器是第一步
        FindAdapter fa=new FindAdapter();
        //找寻可以将220v转化为5v的变压器,即适配器
        DC5Adapter adapter = fa.getPowerAdapter(new ChinaPower());
        //输出当前变压器转化之后的电压
        System.out.println(adapter.transTo5V(new ChinaPower()));
    }
}

适配器模式总结

主要优点

  • 将目标类和适配者类解耦,通过引入一个适配器类来重用现有的适配者类,无须修改原有结构。
  • 增加了类的透明性和复用性,将具体的业务实现过程封装在适配者类中,对于客户端类而言是透明的,而且提高了适配者的复用性,同一个适配者类可以在多个不同的系统中复用。
  • 灵活性和扩展性都非常好,通过使用配置文件,可以很方便地更换适配器,也可以在不修改原有代码的基础上增加新的适配器类,完全符合“开闭原则”。

具体来说,类适配器模式还有如下优点:

  • 由于适配器类是适配者类(适配器接口或适配器接口实现的抽象类)的子类,因此可以在适配器类中置换一些适配者(适配器接口或适配器接口实现的抽象类)的方法,使得适配器的灵活性更强。
  • 一个对象适配器可以把多个不同的适配者(适配器接口或适配器接口实现的抽象类)适配到同一个目标;
  • 可以适配一个适配者的子类,由于适配器和适配者(适配器接口或适配器接口实现的抽象类)之间是关联关系,根据“里氏代换原则”,适配者(适配器接口或适配器接口实现的抽象类)的子类也可通过该适配器进行适配。

主要缺点

  • 对于Java、C#等不支持多重类继承的语言,一次最多只能适配一个适配者类(适配器接口或适配器接口实现的抽象类),不能同时适配多个适配者;
  • 适配者类不能为最终类,如在Java中不能为final类,C#中不能为sealed类;
  • 在Java、C#等语言中,类适配器模式中的目标抽象类只能为接口,不能为类,其使用有一定的局限性。
  • 与类适配器模式相比,要在适配器中置换适配者类的某些方法比较麻烦。如果一定要置换掉适配者类的一个或多个方法,可以先做一个适配者类的子类,将适配者类的方法置换掉,然后再把适配者类的子类当做真正的适配者进行适配,实现过程较为复杂。

适用场景

  • 系统需要使用一些现有的类,而这些类的接口(如方法名)不符合系统的需要,甚至没有这些类的源代码。
  • 想创建一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作。

spring MVC中的适配器模式

springMVC处理请求流程

  • 第1步 用户发送请求至前端控制器DispatcherServlet;
  • 第2,3步DispatcherServlet收到请求,根据请求url调用HandlerMapping处理器映射器找到具体的处理器;生成处理器对象及处理器拦截器(如果有则生成)一并返回给DispatcherServlet;
  • 第4步 DispatcherServlet通过HandlerAdapter处理器适配器调用具体的处理器;(这一步用到适配者模式)
  • 第5,6步 执行处理器(Controller,也叫后端控制器),返回ModelAndView;
  • 第7步 HandlerAdapter将controller执行结果ModelAndView返回给DispatcherServlet
  • 第8步DispatcherServlet将ModelAndView传给ViewReslover视图解析器(但是如果加上@responsebody注解,则返回值不通过viewResolver,而是直接返回object);
  • 第9步 ViewReslover解析后返回具体View;
  • 第10步 DispatcherServlet对View进行渲染视图(即将模型数据填充至视图中);
  • 第11步 DispatcherServlet响应用户。

SpringMVM 中的 HandlerAdapter(上图的第4步), 就使用了适配器模式;


请求处理方法中适配器模式部分源码探究

Spring MVC中的适配器模式主要用于执行目标 Controller 中的请求处理方法。

在Spring MVC中,DispatcherServlet 作为用户,HandlerAdapter 作为期望接口(适配器接口),具体的适配器实现类用于对目标类进行适配,Controller 作为需要适配的类。

为什么要在 Spring MVC 中使用适配器模式?Spring MVC 中的 Controller 种类众多,不同类型的 Controller 通过不同的方法来对请求进行处理。如果不利用适配器模式的话,DispatcherServlet 直接获取对应类型的 Controller,需要的自行来判断,像下面这段代码一样:

代码语言:javascript复制
if(mappedHandler.getHandler() instanceof MultiActionController){  
   ((MultiActionController)mappedHandler.getHandler()).xxx  
}else if(mappedHandler.getHandler() instanceof XXX){  
    ...  
}else if(...){  
   ...  
}  

这样假设如果我们增加一个 HardController,就要在代码中加入一行 if(mappedHandler.getHandler() instanceof HardController),这种形式就使得程序难以维护,也违反了设计模式中的开闭原则 – 对扩展开放,对修改关闭。

我们来看看源码,首先是适配器接口 HandlerAdapter

代码语言:javascript复制
//适配器接口
public interface HandlerAdapter 
{
    //判断当前的controller请求是否能被当前的适配器类处理
    boolean supports(Object var1);
      
      //只有当支持处理当前请求后,才会执行下面的处理请求方法,返回一个ModelAndView对象
     ModelAndView handle(HttpServletRequest var1, HttpServletResponse var2, Object var3) throws Exception;

    long getLastModified(HttpServletRequest var1, Object var2);
}

现该接口的适配器每一个 Controller 都有一个适配器与之对应,这样的话,每自定义一个 Controller 需要定义一个实现 HandlerAdapter 的适配器。

springmvc 中提供的 Controller 实现类有如下:

springmvc 中提供的 HandlerAdapter 实现类如下

HttpRequestHandlerAdapter 这个适配器代码如下:

代码语言:javascript复制
//不同的适配器类实现不同的功能
//当前的HttpRequestHandlerAdapter 适配器类,只负责处理关于HttpRequest相关的请求
public class HttpRequestHandlerAdapter implements HandlerAdapter {
    public HttpRequestHandlerAdapter() {
    }
    
    //判断当前的controller请求是否是HttpRequestHandler类型的
    //当前适配器只支持处理当前类型的handler 
    public boolean supports(Object handler) {
        return handler instanceof HttpRequestHandler;
    }

    //如果验证支持,会调用下面这个方法进行具体逻辑处理
    public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    //先进行强制类型转换,转换为指定的handler类型,然后就可以调用该类型处理对应请求的方法了
    //调用HttpRequestHandler的handleRequest处理对应的请求
        ((HttpRequestHandler)handler).handleRequest(request, response);
        return null;
    }

    public long getLastModified(HttpServletRequest request, Object handler) {
        return handler instanceof LastModified ? ((LastModified)handler).getLastModified(request) : -1L;
    }
}

当Spring容器启动后,会将所有定义好的适配器对象存放在一个List集合中,当一个请求来临时,DispatcherServlet 会通过 handler 的类型找到对应适配器,并将该适配器对象返回给用户,然后就可以统一通过适配器的 hanle() 方法来调用 Controller 中的用于处理请求的方法。

代码语言:javascript复制
public class DispatcherServlet extends FrameworkServlet {
//用于存放所有HandlerAdapter适配器类的list集合
    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;

		//找到能处理当前processedRequest,即request请求的handler
		mappedHandler = getHandler(processedRequest);
			
		// 确定当前handler匹配的适配器类.
		HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

		ha.getLastModified(request, mappedHandler.getHandler());
					
		mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
    }
	// ...省略...
}	

通过适配器模式我们将所有的 controller 统一交给 HandlerAdapter 处理,免去了写大量的 if-else 语句对 Controller 进行判断,也更利于扩展新的 Controller 类型。


总结

使用 HandlerAdapter 的原因分析:

如果处理器的类型不同,有多重实现方式,那么调用方式就不是确定的,如果直接调用 Controller 方法,就得不断使用 if else 来进行判断是哪一种子类然后执行。那么如果后面要扩展 Controller,就得修改原来的代码,这样违背了 OCP 原则;

说明:

  • Spring定义了一个适配接口,使得每一种Controller有一种对应的适配器实现类;
  • 适配器代替 controller执行相应的方法;
  • 扩展Controller时,只需要增加一个适配器类就完成了SpringMVC的扩展;

参考文章

设计模式 8 - 适配器模式与springmvc源码分析 设计模式 | 适配器模式及典型应用 适配器模式(SpringMVC源码分析) 设计模式 | 适配器模式及典型应用

0 人点赞