前言

适配器模式是最为普遍的设计模式之一,它不仅广泛应用于代码开发,在日常生活里也很常见。比如笔记本上的电源适配器,可以使用在110~ 220V之间变化的电源,而笔记本还能正常工作,这就是适配器模式最直接的例子,同时也是其思想的体现,简单的说,适配器模式就是把一个类(接口)转换成其他的类(接口)。

适配器模式

1、定义

适配器模式,也叫包装模式,指的是将一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作。我们可以通过增加一个适配器类来解决接口不兼容的问题,而这个适配器类就相当于笔记本的适配器。

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

2、UML类图



适配器模式包含了几个角色,它们是:

Target(目标角色):该角色定义其他类转化成何种接口,可以是一个抽象类或接口,也可以是具体类。

Adaptee(源角色):你想把谁转换成目标角色,这个“谁”就是源角色,它是已经存在的、运行良好的类或对象。

Adapter(适配器角色):适配器模式的核心角色,职责就是通过继承或是类关联的方式把源角色转换为目标角色。

3、实战例子

知道有哪些角色和UML类图后,我们就可以写一下代码了,为了方便理解,我们用生活中充电器的例子来讲解适配器,现在有一个手机要充电,所需要的额定电压是5V,而家用交流电的电压是标准的220V,这种情况下要充电就需要有个适配器来做电压转换。

把三者代入我们上面画的类图不难得出,充电器本身相当于Adapter,220V交流电相当于Adaptee,我们的目标Target是5V直流电。

Target目标接口

public interface Voltage5 {

    int output5V();
}

目标接口的实现类

public class ConcreteVoltage5 implements Voltage5{
@Override
public int output5V() {
int src = 5;
System.out.println("目标电压:" + src + "V");
return src;
}
}

Adaptee类

public class Voltage220 {

	// 输出220V的电压
public int output220V() {
int src = 220;
System.out.println("源电压是:" + src + "V");
return src;
}
}

Adapter类:完成220V-5V的转变

public class VoltageAdapter extends Voltage220 implements Voltage5 {
@Override
public int output5V() {
// 获取到源电压
int adaptee = super.output220V();
// 开始适配操作
System.out.println("对象适配器工作,开始适配电压");
int dst = adaptee / 44;
System.out.println("适配完成后输出电压:" + dst + "V");
return dst;
}
}

通过适配器类的转换,我们就可以把220V的电压转成我们需要的5V电压了,写个场景类测试下:

/**
* 适配器模式
*/
public class Client { public static void main(String[] args) {
Voltage5 voltage5 = new ConcreteVoltage5();
voltage5.output5V();
// 创建一个适配器对象
VoltageAdapter2 voltageAdapter = new VoltageAdapter2();
// 转换电压
voltageAdapter.output5V();
}
}

结果输出

目标电压:5V
源电压是:220V
对象适配器工作,开始适配电压
适配完成后输出电压:5V

前面说了,适配器模式分为两种,我们上面介绍的通过继承的方式实现的是类适配器,还有一种对象适配器,它是通过关联适配器与适配者的方式实现的,它的通用类图如下所示:

代码方面也比较简单,参考上面的例子把继承的写法改为关联即可,代码就不贴了,读者可以自己写一下。

4、总结

下面对适配器模式做一下总结吧

1)优点

  • 能实现目标类和愿角色类的解耦。适配器模式可以让两个没有任何关系的类在一起运行,只要适配器这个角色能够搞定他们就成。
  • 增加了类的透明性。将具体的业务实现过程封装在适配者类中,对于客户端类而言是透明的,而且提高了适配者的复用性,同一个适配者类可以在多个不同的系统中复用。
  • 灵活性和扩展性都非常好。某一天,突然不想要适配器,没问题,删除掉这个适配器就可以了,其他的代码都不用 修改,基本上就类似一个灵活的构件,想用就用,不想就卸载。

2)缺点

  • 类适配器:采用继承方式,对Java这种不支持多继承的语言来说,一次只能适配一个适配器类,不太方便

  • 对象适配器:与类适配器模式相比,要在适配器中置换适配者类的某些方法比较麻烦。如果一定要置换掉适配者类的一个或多个方法,可以先做一个适配者类的子类,将适配者类的方法置换掉,然后再把适配者类的子类当做真正的适配者进行适配,实现过程较为复杂。

3)适用场景

  • 需要修改到已上线接口时,适配器模式可能是最适合的模式。
  • 系统扩展了,需要使用一个已有或新建的类,但类不符合系统的接口支持,这时就可以引入适配器类来做转换。

SpringMVC底层的适配器模式

这里扩展一下适配器模式的知识点,想必做过Java开发的都知道SpringMVC,这套框架可以帮助我们把前端的请求访问到后台对应的controller的方法上,然后再把处理结果返回给后端,它的底层其实就用到了适配器模式。

SpringMVC中的适配器模式主要用于执行目标Controller中的请求处理方法。在它的底层处理中,DispatcherServlet作为用户,HandlerAdapter作为期望接口,具体的适配器实现类用于对目标类进行适配,Controller作为需要适配的类。

为什么要在 Spring MVC 中使用适配器模式?Spring MVC 中的 Controller 种类众多,不同类型的 Controller 通过不同的方法来对请求进行处理。如果不利用适配器模式的话,DispatcherServlet 直接获取对应类型的 Controller,那样每增加一个类型的Controller就需要使用增加一个if else判断instance of,这违反了设计模式的开闭原则 —— 对扩展开放,对修改关闭。

那么SpringMVC是怎么处理的呢?我们来简单看一下源码,首先是适配器接口HandlerAdapter

public interface HandlerAdapter {
boolean supports(Object var1); ModelAndView handle(HttpServletRequest var1, HttpServletResponse var2, Object var3) throws Exception; long getLastModified(HttpServletRequest var1, Object var2);
}

该接口的适配器类对应着 Controller,每自定义一个Controller需要定义一个实现HandlerAdapter的适配器。举个例子,有一个适配器类HttpRequestHandlerAdapter ,该类就是实现了HandlerAdapter接口,这是它的源码:

public class HttpRequestHandlerAdapter implements HandlerAdapter {

   @Override
public boolean supports(Object handler) {
return (handler instanceof HttpRequestHandler);
} @Override
public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception { ((HttpRequestHandler) handler).handleRequest(request, response);
return null;
} @Override
public long getLastModified(HttpServletRequest request, Object handler) {
if (handler instanceof LastModified) {
return ((LastModified) handler).getLastModified(request);
}
return -1L;
} }

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

public class DispatcherServlet extends FrameworkServlet {

	protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {

		//.........................

		//找到匹配当前请求对应的适配器
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); // Process last-modified header, if supported by the handler.
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (logger.isDebugEnabled()) {
String requestUri = urlPathHelper.getRequestUri(request);
logger.debug("Last-Modified value for [" + requestUri + "] is: " + lastModified);
}
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
} if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
} try {
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
}finally {
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
}
}
// 遍历集合,找到合适的匹配器
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;
}
}
throw new ServletException("No adapter for handler [" + handler +
"]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
}
}

这样一来,所有的controller就都统一交给HandlerAdapter处理,免去了大量的 if-else 语句判断,同时增加controller类型只需增加一个适配器即可,不需要修改到Servlet的逻辑,符合开闭原则。

关于适配器模式的介绍就到这里了,有不足之处还望指出。

参考

《设计模式之禅》

https://blog.csdn.net/wwwdc1012/article/details/82780560

设计模式:与SpringMVC底层息息相关的适配器模式的更多相关文章

  1. 深入源码分析SpringMVC底层原理(二)

    原文链接:深入源码分析SpringMVC底层原理(二) 文章目录 深入分析SpringMVC请求处理过程 1. DispatcherServlet处理请求 1.1 寻找Handler 1.2 没有找到 ...

  2. SpringMVC底层数据传输校验的方案(修改版)

    团队的项目正常运行了很久,但近期偶尔会出现BUG.目前观察到的有两种场景:一是大批量提交业务请求,二是生成批量导出文件.出错后,再执行一次就又正常了. 经过跟踪日志,发现是在Server之间进行jso ...

  3. JAVA设计模式详解(五)----------适配器模式

    各位朋友好,本章节我们继续讲第五个设计模式. 在生活中,我们都知道手机内存卡是无法直接接电脑的,因为内存卡的卡槽比较小,而电脑只有USB插孔,此时我们需要用到读卡器.这个读卡器就相当于是适配器.这是生 ...

  4. 分享知识-快乐自己:SpringMVC 底层执行原理解析

    底层实现原理图: 观看底层代码: 1):打开 web.xml 文件  2):按住 Ctrl + 鼠标左键 进入底层查看源码   3):按住 Ctrl+o 找到对应的方法doDispatch   5): ...

  5. (一)springMvc 底层运作流程

    目录 什么是 springMvc SpringMVC的底层运作流程 什么是 springMvc springMvc 是spring 框架的一个模块,这也就意味着二者不需要通过整合层(整合包)进行整合 ...

  6. SpringMVC底层执行原理

    一个简单的HelloSpringMVC程序 先在web,xml中注册一个前端控制器(DispatcherServlet) <?xml version="1.0" encodi ...

  7. 设计模式(七):Adapter 适配器模式 -- 结构型模式

    1. 概述: 接口的改变,是一个需要程序员们必须(虽然很不情愿)接受和处理的普遍问题.程序提供者们修改他们的代码;系统库被修正;各种程序语言以及相关库的发展和进化.  例子1:iphone4,你即可以 ...

  8. SpringMVC底层数据传输校验的方案

    团队的项目正常运行了很久,但近期偶尔会出现BUG.目前观察到的有两种场景:一是大批量提交业务请求,二是生成批量导出文件.出错后,再执行一次就又正常了. 经过跟踪日志,发现是在Server之间进行jso ...

  9. 设计模式PHP篇(三)————适配器模式

    简单的适配器模式: interface Adaptor { public function read(); public function write(); } class File implemen ...

随机推荐

  1. 跟着大彬读源码 - Redis 1 - 启动服务,程序都干了什么?

    一直很羡慕那些能读 Redis 源码的童鞋,也一直想自己解读一遍,但迫于 C 大魔王的压力,解读日期遥遥无期. 相信很多小伙伴应该也都对或曾对源码感兴趣,但一来觉得自己不会 C 语言,二来也不知从何入 ...

  2. 高并发架构系列:Redis缓存和MySQL数据一致性方案详解

    一.需求起因 在高并发的业务场景下,数据库大多数情况都是用户并发访问最薄弱的环节.所以,就需要使用redis做一个缓冲操作,让请求先访问到redis,而不是直接访问MySQL等数据库. 这个业务场景, ...

  3. Egret入门学习日记 --- 第四篇

    第四篇(学习篇) 好了,今天继续把昨天的问题解决了. 今天见鬼了. 现在界面又出来了.唯一我动过的地方,应该就是这里: 是的,我点了一下刷新.之后,不管我怎么创建新的EXML文件,放在src目录,还是 ...

  4. Oracle数据库---PLSQL

    SET SERVEROUTPUT ONBEGIN --打印输出 DBMS_OUTPUT.PUT_LINE('hello everyone!');END; DECLARE v_name VARCHAR2 ...

  5. Leetcode 195 Tenth Line

    Given a text file file.txt, print just the 10th line of the file. Example: Assume that file.txt has ...

  6. 1. Python 魔法方法

    Python 魔法方法 基础: 如果你想... 所以,你写... Python调用... 初始化一个实例 x = MyClass() x.__init__() 作为一个字符串的"官方&quo ...

  7. Skyline WEB端开发2——添加一个定位点、文本标签

    Skyline 添加定位点 sgworld.Creator.CreatePosition CreatePosition( X, //兴趣点的东西方向坐标,即经度 Y, //兴趣点的南北方向坐标,即纬度 ...

  8. Apache struts2 namespace远程命令执行_CVE-2018-11776(S2-057)漏洞复现

    Apache struts2 namespace远程命令执行_CVE-2018-11776(S2-057)漏洞复现 一.漏洞描述 S2-057漏洞产生于网站配置xml的时候,有一个namespace的 ...

  9. java8中stream常用方法详解

    map: 用作类型转换 如把集合里面的字符串转为大写,或者一个对象的集合取几个字段转为新的对象集合filter: 过滤 符合条件的集合元素保存下来,不符合条件的去掉flatMap:合并集合,比如Lis ...

  10. 如何进行高效的源码阅读:以Spring Cache扩展为例带你搞清楚

    摘要 日常开发中,需要用到各种各样的框架来实现API.系统的构建.作为程序员,除了会使用框架还必须要了解框架工作的原理.这样可以便于我们排查问题,和自定义的扩展.那么如何去学习框架呢.通常我们通过阅读 ...