结构型模式 ————顺口溜:适装桥组享代外
目录
1、适配器模式
适配器模式(Adapter Pattern)是作为两个不兼容的接口之间的桥梁。这种类型的设计模式属于结构型模式,它结合了两个独立接口的功能。
这种模式涉及到一个单一的类,该类负责加入独立的或不兼容的接口功能。举个真实的例子,读卡器是作为内存卡和笔记本之间的适配器。您将内存卡插入读卡器,再将读卡器插入笔记本,这样就可以通过笔记本来读取内存卡。
- 意图:将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
- 主要解决:主要解决在软件系统中,常常要将一些"现存的对象"放到新的环境中,而新环境要求的接口是现对象不能满足的。
- 何时使用: 1、系统需要使用现有的类,而此类的接口不符合系统的需要。 2、想要建立一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作,这些源类不一定有一致的接口。 3、通过接口转换,将一个类插入另一个类系中。(比如老虎和飞禽,现在多了一个飞虎,在不增加实体的需求下,增加一个适配器,在里面包容一个虎对象,实现飞的接口。)
- 如何解决:继承或依赖(推荐)。
- 关键代码:适配器继承或依赖已有的对象,实现想要的目标接口。
1.1 适配器模式UML图
图一 类适配器UML类图
图二 对象适配器UML类图
1.2 日常生活中看装饰器模式
- 在现实生活中,经常出现两个对象因接口不兼容而不能在一起工作的实例,这时需要第三者进行适配。例如,用直流电的笔记本电脑接交流电源时需要一个电源适配器,用计算机访问照相机的 SD 内存卡时需要一个读卡器等。
- 在软件设计中也可能出现:需要开发的具有某种业务功能的组件在现有的组件库中已经存在,但它们与当前系统的接口规范不兼容,如果重新开发这些组件成本又很高,这时用适配器模式能很好地解决这些问题
1.3 应用实例
- 美国电器 110V,中国 220V,就要有一个适配器将 110V 转化为 220V。
- JAVA JDK 1.1 提供了 Enumeration 接口,而在 1.2 中提供了 Iterator 接口,想要使用 1.2 的 JDK,则要将以前系统的 Enumeration 接口转化为 Iterator 接口,这时就需要适配器模式。
- 在 LINUX 上运行 WINDOWS 程序。
- JAVA 中的 jdbc。
1.4 Java代码实现
有两种方式解决,第一种就是类适配器模式,主要的思想就是靠继承来实现适配,即在原有的组件基础上继承新业务功能类,第二种就是对象适配器模式,主要思想是将新业务功能类的对象注入到原有的组件中,然后达到适配的作用
1.4.1 类适配器模式
/**
* @author 26530
* HDMI接口
*/
public interface HDMIPort
{
void useHDMIPort();
}
/**
* @author 26530
* 数据转换线
*/
public class DataConversionLine
{
public void vgaToHdmi()
{
System.out.println("将VGA数据传输转为HDMI");
}
}
/**
* @author 26530
* 笔记本电脑
*/
public class LaptopComputer extends DataConversionLine implements HDMIPort
{
@Override
public void useHDMIPort()
{
System.out.println("使用的HDMI接口");
}
}
/**
* @author 26530
* 投影仪
*/
public class Projector
{
public void dataLine()
{
System.out.println("投影仪使用的是VGA接口的数据线");
}
}
public class Test
{
public static void main(String[] args)
{
//笔记本想连接投影仪,但是笔记本自身的接口是HDMI,投影仪的接口是VGA的。
//为解决这个问题,我们需要一个适配器,把vga的接口转为hdmi,通过这个适配器就可以实现笔记本电脑和投影仪连接了
Projector projector = new Projector();
projector.dataLine();
LaptopComputer laptopComputer = new LaptopComputer();
laptopComputer.vgaToHdmi();
laptopComputer.useHDMIPort();
/*
投影仪使用的是VGA接口的数据线
将VGA数据传输转为HDMI
使用的HDMI接口
*/
//这样就实现了笔记本和投影仪相连接
}
}
1.4.2 对象适配器模式
/**
* @author 26530
* HDMI接口
*/
public interface HDMIPort
{
void useHDMIPort();
}
/**
* @author 26530
* 数据转换线
*/
public class DataConversionLine
{
public void vgaToHdmi()
{
System.out.println("将VGA数据传输转为HDMI");
}
}
/**
* @author 26530
* 笔记本电脑
*/
public class LaptopComputer implements HDMIPort
{
private DataConversionLine dataConversionLine;
public LaptopComputer(DataConversionLine dataConversionLine) {
super();
this.dataConversionLine = dataConversionLine;
}
public LaptopComputer() {
super();
// TODO Auto-generated constructor stub
}
@Override
public void useHDMIPort()
{
System.out.println("使用的HDMI接口");
}
public DataConversionLine getDataConversionLine() {
return dataConversionLine;
}
public void setDataConversionLine(DataConversionLine dataConversionLine) {
this.dataConversionLine = dataConversionLine;
}
}
/**
* @author 26530
* 投影仪
*/
public class Projector
{
public void dataLine()
{
System.out.println("投影仪使用的是VGA接口的数据线");
}
}
public class Test
{
public static void main(String[] args)
{
//笔记本想连接投影仪,但是笔记本自身的接口是HDMI,投影仪的接口是VGA的。
//为解决这个问题,我们需要一个适配器,把vga的接口转为hdmi,通过这个适配器就可以实现笔记本电脑和投影仪连接了
Projector projector = new Projector();
projector.dataLine();
DataConversionLine dataConversionLine = new DataConversionLine();
LaptopComputer laptopComputer = new LaptopComputer(dataConversionLine);
laptopComputer.getDataConversionLine().vgaToHdmi();
laptopComputer.useHDMIPort();
/*
投影仪使用的是VGA接口的数据线
将VGA数据传输转为HDMI
使用的HDMI接口
*/
//这样就实现了笔记本和投影仪相连接
}
}
2、适配器模式在源码中的应用
2.1 Spring源码中适配器模式体现
AOP和MVC中,都有用到适配器模式。
2.1.1 AOP中的应用
在Spring的Aop中,使用Advice(通知)来增强被代理类的功能,Advice的类型有:BeforeAdvice、AfterReturningAdvice、ThreowSadvice。
每种Advice都有对应的拦截器,MethodBeforeAdviceInterceptor、AfterReturningAdviceInterceptor、ThrowsAdviceInterceptor。
各种不同类型的Interceptor,通过适配器统一对外提供接口,如下类图所示:client ---> target ---> adapter ---> interceptor ---> advice。最终调用不同的advice来实现被代理类的增强
2.1.2 MVC中的应用
DispatcherServlet中的doDispatch方法,是将请求分发到具体的controller,因为存在很多不同类型的controller,常规处理是用大量的if...else...,来判断各种不同类型的controller,如下这样:
if(mappedHandler.getHandler() instanceof MultiActionController){
((MultiActionController)mappedHandler.getHandler()).xxx
}else if(mappedHandler.getHandler() instanceof XXX){
...
}else if(...){
...
}
如果还需要添加另外的controller,就需要再次添加if...else...,程序就会难以维护,也违反了开闭原则 -- 对扩展开放,对修改关闭。
因此,spring定义了一个适配器接口,使得每一种Controller有一种对应的适配器实现类,让适配器代替controller执行相应的方法。这样在扩展Controller 时,只需要增加一个适配器类就完成了SpringMVC的扩展了。
下图展示了DispatcherServlet类的关系图,因为使用了adapter,代码结构非常清晰有没有~~
public interface HandlerAdapter {
boolean supports(Object handler);
ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;
}
supports()方法传入处理器(宽泛的概念Controller,以及HttpRequestHandler,Servlet,等等)判断是否与当前适配器支持如果支持则从DispatcherServlet中的HandlerAdapter实现类中返回支持的适配器实现类。handler方法就是代理Controller来执行请求的方法并返回结果。
在DispatchServlert中的doDispatch方法中HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
此代码通过调用DispatchServlert 中getHandlerAdapter传入Controller(宽泛的概念Controller,以及HttpRequestHandler,Servlet,等等),来获取对应的HandlerAdapter 的实现子类,从而做到使得每一种Controller有一种对应的适配器实现类
返回后就能通过对应的适配实现类代理Controller(宽泛的概念Controller,以及HttpRequestHandler,Servlet,等等)来执行请求的方法
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
其中一个处理器适配器就是我们常说的最熟悉的Controller类适配器
2.2 MyBatis源码中适配器模式体现
MyBatis框架对适配器的使用,主要体现在Log日志这一块。
Mybatis 可以集成多种第三方日志系统:如log4j,log4j2,commons-logging,slf4j等等,日志模块提供的接口各不相同,mybatis使用适配器模式,为每一个日志系统实现一个适配器。
/**
* MyBatis定义的日志接口规范
*/
public interface Log {
boolean isDebugEnabled();
boolean isTraceEnabled();
void error(String s, Throwable e);
void error(String s);
void debug(String s);
void trace(String s);
void warn(String s);
}
该接口定义了Mybatis直接使用的日志方法,而Log接口具体由谁来实现呢?Mybatis提供了多种日志框架的实现,这些实现都匹配这个Log接口所定义的接口方法,最终实现了所有外部日志框架到Mybatis日志包的适配。
MyBatis就是这么刚,提供了如此之多实现方式。 啥意思呢,就是Log接口适配了这么多的日志框架, 我们在使用时,只需要随便引入其中一种日志框架,那么就可以使用Log接口规范去操作日志了。
下面我们来看一个具体的实现。 毕竟接口是啥事都干不了的,干事还得靠实现啦!
以log4j为例
//适配器Log4jImpl,实现了Log接口,并封装了 org.apache.log4j.Logger对象,Log4jAdapter 的功能完全有logger实现
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
public class Log4jAdapter implements Log {
private static final String FQCN = Log4jImpl.class.getName();
private final Logger log;
public Log4jImpl(String clazz) {
//适配器的关键实现,在构造器器中调用了Log4j
//的静态方法,初始化了Logger log 字段
this.log = Logger.getLogger(clazz);
}
public boolean isDebugEnabled() {
return log.isDebugEnabled();
}
public boolean isTraceEnabled() {
return log.isTraceEnabled();
}
public void error(String s, Throwable e) {
log.log(FQCN, Level.ERROR, s, e);
}
public void error(String s) {
log.log(FQCN, Level.ERROR, s, null);
}
public void debug(String s) {
log.log(FQCN, Level.DEBUG, s, null);
}
public void trace(String s) {
log.log(FQCN, Level.TRACE, s, null);
}
public void warn(String s) {
log.log(FQCN, Level.WARN, s, null);
}
}
//常用的方法 private static Log logger=LogFactory.getLog(类.class)
import org.apache.log4j.Logger;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public class LogFactory {
private static Constructor<? extends Log> logConstructor;
//静态代码块初始化logConstructor
static {
tryImplementation(new Runnable() {
public void run() {//注意是run()
useLog4JLogging();
}
});
}
private static void tryImplementation(Runnable runnable) {
if (logConstructor == null) {
try {
runnable.run();
} catch (Throwable t) {
// ignore
}
}
}
public static synchronized void useLog4JLogging() {
setImplementation(Log4jImpl.class);
}
private static void setImplementation(Class<? extends Log> implClass) {
try {
Constructor<? extends Log> candidate = implClass.getConstructor(String.class);
Log log = candidate.newInstance(LogFactory.class.getName());
if (log.isDebugEnabled()) {
log.debug("Logging initialized using '" + implClass + "' adapter.");
}
logConstructor = candidate;//
} catch (Throwable t) {
// throw new LogException("Error setting Log implementation. Cause: " + t, t);
}
}
private LogFactory() {
// disable construction
}
//给外部调用
public static Log getLog(Class<?> aClass) {
return getLog(aClass.getName());
}
private static Log getLog(String logger) {
try {
//返回一个Log对象
return logConstructor.newInstance(logger);
} catch (Throwable t) {
//throw new LogException("Error creating logger for logger " + logger + ". Cause: " + t, t);
}
return null;
}
}
3、适配器模式的优缺点
3.1 优点
- 可以让任何两个没有关联的类一起运行。
- 提高了类的复用。
- 增加了类的透明度。
- 灵活性好。
3.2 缺点
- 过多地使用适配器,会让系统非常零乱,不易整体进行把握。比如,明明看到调用的是 A 接口,其实内部被适配成了 B 接口的实现,一个系统如果太多出现这种情况,无异于一场灾难。因此如果不是很有必要,可以不使用适配器,而是直接对系统进行重构。
- 由于 JAVA 至多继承一个类,所以至多只能适配一个适配者类,而且目标类必须是抽象类。
- 对类适配器来说,更换适配器的实现过程比较复杂。
3.3 使用场景
- 有动机地修改一个正常运行的系统的接口,这时应该考虑使用适配器模式。
- 系统需要复用现有类,而该类的接口不符合系统的需求,可以使用适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作
- 多个组件功能类似,但接口不统一且可能会经常切换时,可使用适配器模式,使得客户端可以以统一的接口使用它们
3.4 注意事项
注意事项:适配器不是在详细设计时添加的,而是解决正在服役的项目的问题。
建议尽量使用对象的适配器模式,多用合成/聚合、少用继承。
4、适配器模式与装饰器模式、门面模式(外观模式)的异同
适配器模式将一个类的接口,转化成客户期望的另一个接口,适配器让原本接口不兼容的类可以合作无间。
装饰者模式:动态的将责任附加到对象上(因为利用组合而不是继承来实现,而组合是可以在运行时进行随机组合的)。若要扩展功能,装饰者提供了比继承更富有弹性的替代方案(同样地,通过组合可以很好的避免类暴涨,也规避了继承中的子类必须无条件继承父类所有属性的弊端)。
特点:
- 1. 装饰者和被装饰者拥有相同的超类型(可能是抽象类也可能是接口)
- 2. 可以用多个装饰类来包装一个对象,装饰类可以包装装饰类或被装饰对象
- 3. 因为装饰者和被装饰者拥有相同的抽象类型,因此在任何需要原始对象(被包装)的场合,都可以用装饰过的对象来替代它。
- 4. 装饰者可以在被装饰者的行为之前或之后,加上自己的附加行为,以达到特殊目的
- 5. 因为对象可以在任何的时候被装饰,所以可以在运行时动态地、不限量地用你喜欢的装饰者来装饰对象
java.io库是最好的例子
OutputStream
ByteArrayOutputStream
FileOutputStream
PipedOutputStream
FilterOutputStream
BufferedOutputStream
DataOutputStream
小结:装饰者模式——动态地将责任附加到对象上。想要扩展功能,装饰者提供了有别于继承的另外一种选择。是一个很好的符合了开闭原则的设计模式。
适配器模式主要是为了接口的转换,而装饰者模式关注的是通过组合来动态的为被装饰者注入新的功能或行为(即所谓的责任)
适配器将一个对象包装起来以改变其接口;装饰者将一个对象包装起来以增强新的行为和责任;而外观将一群对象包装起来以简化其接口
参考文章:
https://blog.youkuaiyun.com/atu1111/article/details/105458658
https://www.cnblogs.com/zuokun/p/10718570.html
https://blog.youkuaiyun.com/yuan882696yan/article/details/105602359
https://www.cnblogs.com/z-qinfeng/p/12215988.html
https://blog.youkuaiyun.com/xl3379307903/article/details/80266050