设计模式:与SpringMVC底层息息相关的适配器模式
前言
适配器模式是最为普遍的设计模式之一,它不仅广泛应用于代码开发,在日常生活里也很常见。比如笔记本上的电源适配器,可以使用在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的逻辑,符合开闭原则。
关于适配器模式的介绍就到这里了,有不足之处还望指出。
参考
《设计模式之禅》