在正式开始之前,让我们先思考几个问题:
- 如果现有的新项目可以利用旧项目里大量的遗留代码,你打算从头开始完成新项目还是去了解旧项目的模块功能以及接口?
- 如果你了解过遗留代码之后,发现有几个重要的功能模块接口不同(因为它们可能来自多个旧项目),无法直接复用,你打算放弃使用遗留代码吗?
- 如果你不打算放弃(这样做应该是对的,毕竟遗留代码的正确性是经过实践检验的),那么是不是只能去改写剩余的n - 1个接口,甚至改写所有的n个接口?
- 如果不这样做,还有什么简单的方法吗?
一.什么是适配器模式?
首先,我们需要知道适配器是什么东西,嗯,笔记本电脑的电源适配器听说过吧?
它能够把220V的交流电转换为笔记本需要的15V直流电
太神奇了,一个小小的电源适配器解决了家庭用电与笔记本需要的电类型不匹配的问题
发现什么了么?
没错,我们既没有改变家庭用电(把它变成15V直流电),也没有改变笔记本(把它变成220V交流电),但我们确实解决了这个问题
-------
适配器模式——用来实现不同接口转换的设计模式
二.举个例子
假设我们有两个封装好的功能模块,但它们需要的参数不同(虽然参数的实质是同一种对象)
比如,我们的A模块(文本检查模块)是这样的:
package AdapterPattern; /** * @author ayqy * 文本检查模块(类似与MSOffice Word中的“拼写和语法检查”) */ public class TextCheckModule { FormatText text; public TextCheckModule(FormatText text){ this.text = text; } /* * 省略很多具体Check操作。。 */ }
A模块入口需要一个FormatText类型的参数,它的定义如下:
package AdapterPattern; /** * @author ayqy * 定义格式化文本 */ public interface FormatText { String text = null; /** * @return 文本逻辑行数 */ public abstract int getLineNumber(); /** * @param index 行号 * @return 第index行的内容 */ public abstract String getLine(int index); /* * 省略其它有用的方法 */ }
还有B模块(文本显示模块),它是这样的:
package AdapterPattern; /** * @author ayqy * 文本显示模块 */ public class TextPrintModule { DefaultText text; public TextPrintModule(DefaultText text){ this.text = text; } /* * 省略很多显示相关操作 * */ }
B模块入口所需的DefaultText:
package AdapterPattern; /** * @author ayqy * 定义默认文本 */ public interface DefaultText { String text = null; /** * @return 文本逻辑行数 */ public abstract int getLineCount(); /** * @param index 行号 * @return 第index行的内容 */ public abstract String getLineContent(int index); /* * 省略其它有用的方法 */ }
我们的新项目要求实现一个文字处理程序(像MSOffice Word那样的),我们需要调用A模块来实现文本检查功能,还需要调用B模块来实现文本显示功能
但问题是,两个模块的接口不匹配,导致我们无法直接复用现成的A和B。。
这时我们似乎只有有两个选择:
- 修改FormatText(或者DefaultText),以满足DefaultText(或者FormatText),还需要修改A(或者B)的内部实现
- 定义第三种接口MyText,修改A和B,让它们把MyText作为参数,以求接口的统一
当然,我们可能更倾向与第一种,毕竟所需的修改相对较少,不过即使这样,工作量仍然很大,我们需要打开A的封装,理解其内部实现,并修改方法调用细节
-------
其实我们还有更好的选择——定义一个Adapter,负责FormatText到DefaultText的转换(或者与此相反):
package AdapterPattern; /** * @author ayqy * 定义默认文本适配器 */ public class DefaultTextAdapter implements DefaultText{ FormatText formatText = null;//源对象 /** * @param text 需要转换的源文本对象 */ public DefaultTextAdapter(FormatText formatText){ this.formatText = formatText; } @Override public int getLineCount() { int lineNumber; lineNumber = formatText.getLineNumber(); /* * 在此添加额外的转换处理 * */ return lineNumber; } @Override public String getLineContent(int index) { String line; line = formatText.getLine(index); /* * 在此添加额外的转换处理 * */ return line; } }
我们的做法其实相当简单:
- 定义Adapter实现目标接口
- 获取并保留源接口对象
- 实现目标接口中的各个方法(在方法体中调用源接口对象的方法并添加额外的处理以实现转换)
适配器做好了,要怎么用呢?不妨实现一个Test类来测试一下:
package AdapterPattern; /** * @author ayqy * 测试接口适配器 */ public class Test implements FormatText{ public static void main(String[] args) { //创建源接口对象 FormatText text = new Test(); //创建文本检查模块对象 TextCheckModule tcm = new TextCheckModule(text); /*调用tcm实现文本检查*/ //创建适配器对象,进行源接口对象到目标接口对象的转换 DefaultTextAdapter textAdapter = new DefaultTextAdapter(text); //用Adapter创建文本显示模块对象 TextPrintModule tpm = new TextPrintModule(textAdapter); /*调用tcm实现文本显示*/ } /*请忽略下面偷懒的部分。。*/ @Override public int getLineNumber() { // TODO Auto-generated method stub return 0; } @Override public String getLine(int index) { // TODO Auto-generated method stub return null; } }
(P.S.原谅我的偷懒行为,谁让FormatText偏偏是个接口呢。。)
当然,Test是不会有运行结果的,但能通过编译就足够说明我们的转换没有问题。。
-------
其实我们忽略了一个很重要的问题,例子中源接口与目标接口的方法都是对应的,换句话说就是:源接口中定义的方法在目标接口中都有类似的方法与之对应
当然,这样的情况是极少的,通常都存在方法不对应的问题(源接口中存在目标接口未定义的方法,或者相反的情况)
这时我们有2个选择:
- 抛出异常,但应该在注释或者文档作出详细说明,就像这样:
throw new UnsupportedOperationException();//源接口不支持此操作
- 完成一个空的实现,比如,return false,0,null等等
具体选择哪一种,取决于具体情景,各有各的好处,不能一概而论
三.另一种适配器实现方式
例子中我们采用了“持有源接口对象,实现目标接口”的方式来实现适配器,其实还存在另一种方式——多继承(或者实现多个接口)
如果一个Adapter类既实现了A接口又实现了B接口,那么,毫无疑问,Adapter对象既属于A类型又属于B类型(多继承的原理类似。。)
虽然Java不支持多继承,但在支持多继承的语言环境下我们应当想到这样的实现方式,再视具体情况决定是否采用多继承来实现Adapter
四.总结
当我们手里同时握着一个两孔插头和一个三孔插口时,总是习惯把插头芯拧成八字形的。为什么不去买一个适配器呢?
- 既不需要破坏插头,也不需要破坏插口(有时代码修改确实是破坏性的,我们避免了修改也就避免了破坏)
- 更关键的是:我们可以把买来的适配器借给朋友用(可复用)