一、什么是Adapter模式
我们先举个例子:如果想让额定工作电压是直流12V的笔记本电脑在交流220V的电源下工作,应该怎么做呢?通常,我们会使用适配器,将家庭用的交流220V电压转换成我们所需要的直流12V电压。这就是适配器的工作,它位于实际情况与需求之间,填补两者之间的差异。
在程序世界中,经常会存在现有的程序无法直接使用,需要做适当的变换之后才能使用的情况。这种用于填补“现有的程序”和“所需的程序”之间差异的设计模式就是Adapter模式。
Adapter模式也被称为Wrapper模式。Wrapper有“包装器”的意思,就像用精美的包装纸将普通商品包装成礼物那样,替我们把某样东西包起来,使其能够用于其他用途的东西就被称为“包装器”或是“适配器”。
用一句话来概括:Adapter模式就是为程序加一个“适配器”以便于复用。
Adapter模式有以下两种:
- 类适配器模式(使用继承的适配器)
- 对象适配器模式(使用委托的适配器)
本文将依次介绍这两种Adapter模式。
二、使用继承的Adapter模式示例代码
2.1 各个类之间的关系
先看一下类图:
这里的示例程序是一段会将输入的字符串显示为括号包围或者星号包围的简单程序。例如,输入字符串Hello,显示为(Hello)或是*Hello*。
目前在Banner类( Banner有广告横幅的意思)中,有将字符串用括号括起来的showWithParen方法,和将字符串用*号括起来的showWithAster方法。我们假设这个Banner类是类似前文中的“交流220伏特电压”的“实际情况”。
假设Print接口中声明了两种方法,即弱化字符串显示(加括号)的printweak方法,和强调字符串显示(加*号)的printstrong方法。我们假设这个接口是类似于前文中的“直流12伏特电压”的“需求”。
现在要做的事情是使用Banner类编写一个实现了Print接口的类,也就是说要做一个将“交流220伏特电压”转换成“直流12伏特电压”的适配器。
扮演适配器角色的是 PrintBanner类。该类继承了Banner类并实现了“需求”——Print接口。PrintBanner类使用showWithParen方法实现了printWeak,使用showwithAster方法实现了printstrong。这样,PrintBanner类就具有适配器的功能了。
2.2 Banner类
Banner类是我们现有的功能。
public class Banner { private String string; public Banner(String string) { this.string = string; } public void showWithParen() { System.out.println("(" + string + ")"); } public void showWithAster() { System.out.println("*" + string + "*"); } }
2.3 Print接口
Print接口就是我们新的“需求”。
public interface Print { public abstract void printWeak(); public abstract void printStrong(); }
2.4 PrintBanner类
PrintBanner类扮演适配器的角色。
public class PrintBanner extends Banner implements Print{ public PrintBanner(String string) { super(string); } @Override public void printWeak() { showWithParen(); } @Override public void printStrong() { showWithAster(); } }
2.5 用于测试的Main类
public class Main { public static void main(String[] args) { Print p = new PrintBanner("Hello"); p.printStrong(); p.printWeak(); } }
2.6 运行结果
需要注意的是,我们是使用Print接口(即调用printweak方法和printstrong方法)来进行编程的。对Main类的代码而言,Banner类中的showWithParen'方法和showWithAster方法被完全隐藏起来了。这就好像需要12V的笔记本电脑插在220V的插座上能正常工作,但它并不知道这12伏特的电压是由适配器将220伏特交流电压转换而成的。
Main类并不知道PrintBanner类是如何实现的,这样就可以在不用对Main类进行修改的情况下改变 PrintBanner类的具体实现。
三、使用委托的Adapter模式示例代码
3.1 各个类之间的关系
先看一下类图:
Main类和 Banner类与示例程序中的内容完全相同,不过这里我们假设Print不是接口而是类。
也就是说,我们打算利用Banner类实现一个类,该类的方法和Print类的方法相同。由于在Java中无法同时继承两个类(只能是单一继承),因此我们无法将PrintBanner类分别定义为Print类和 Banner类的子类。
3.2 Banner类
同上面的Banner
public class Banner { private String string; public Banner(String string) { this.string = string; } public void showWithParen() { System.out.println("(" + string + ")"); } public void showWithAster() { System.out.println("*" + string + "*"); } }
3.3 Print类
public abstract class Print { public abstract void printWeak(); public abstract void printStrong(); }
3.4 PrintBanner类
PrintBanner类的banner字段中保存了Banner类的实例。该实例是在PrintBanner类的构造函数中生成的。然后,printWeak方法和printStrong方法会通过banner字段调用Banner类的showWithParen和 showWithAster方法。
与之前的示例代码中调用了从父类中继承的showWwithParen方法和showwithAster方法不同,这次我们通过字段来调用这两个方法。
这样就形成了一种委托关系。当PrintBanner类的printWeak被调用的时候,并不是PrintBanner类自己进行处理,而是将处理交给了其他实例(Banner类的实例)的showWithParen方法。
public class PrintBanner extends Print{ private Banner banner; public PrintBanner(String string) { this.banner = new Banner(string); } @Override public void printWeak() { banner.showWithParen(); } @Override public void printStrong() { banner.showWithAster(); } }
3.5 用于测试的Main类
同上面的Main
public class Main { public static void main(String[] args) { Print p = new PrintBanner("Hello"); p.printStrong(); p.printWeak(); } }
3.6 运行结果
四、拓展思路的要点
4.1 什么时候使用Adapter模式
一定会有读者认为“如果某个方法就是我们所需要的方法,那么直接在程序中使用不就可以了吗?为什么还要考虑使用Adapter模式呢?”那么,究竟应当在什么时候使用Adapter模式呢?
很多时候,我们并非从零开始编程,经常会用到现有的类。特别是当现有的类已经被充分测试过了,Bug很少,而且已经被用于其他软件之中时,我们更愿意将这些类作为组件重复利用。
Adapter模式会对现有的类进行适配,生成新的类。通过该模式可以很方便地创建我们需要的方法群。当出现 Bug时,由于我们很明确地知道Bug不在现有的类( Adaptee角色)中,所以只需调查扮演Adapter角色的类即可。这样一来,代码问题的排查就会变得非常简单。
4.2 如果没有现成的代码
让现有的类适配新的接口(API)时,使用Adapter模式似乎是理所当然的。不过实际上,我们在让现有的类适配新的接口时,常常会有“只要将这里稍微修改下就可以了”的想法,一不留神就会修改现有的代码。但是需要注意的是,如果要对已经测试完毕的现有代码进行修改,就必须在修改后重新进行测试。
使用Adapter模式可以在完全不改变现有代码的前提下使现有代码适配于新的接口(API)。此外,在Adapter模式中,并非一定需要现成的代码。只要知道现有类的功能,就可以编写出新的类。
4.3 版本升级与兼容性
软件的生命周期总是伴随着版本的升级,而在版本升级的时候经常会出现“与旧版本的兼容性”问题。如果能够完全抛弃旧版本,那么软件的维护工作将会轻松得多,但是现实中往往无法这样做。这时,可以使用Adapter模式使新旧版本兼容,帮助我们轻松地同时维护新版本和旧版本。
例如,假设我们今后只想维护新版本。这时可以让新版本扮演Adaptee角色,旧版本扮演Target角色。接着编写一个扮演Adapter角色的类,让它使用新版本的类来实现旧版本的类中的方法。
4.4 功能完全不同的类
当然,当Adaptee角色和Target角色的功能完全不同时,Adapter模式是无法使用的。就如同我们无法用交流220伏特电压让自来水管出水一样。
五、相关的设计模式
5.1 Bridge桥接模式
Adapter模式用于连接接口(API )不同的类,而 Bridge模式则用于连接类的功能层次结构与实现层次结构。
5.2 Decorator装饰器模式
Adapter模式用于填补不同接口(API)之间的缝隙,而Decorator模式则是在不改变接口(API)的前提下增加功能。
六、思考题
6.1
题目:
在示例程序中生成PrintBanner类的实例时,我们采用了如下方法,即使用Print类型的变量来保存PrintBanner实例。
Print p = new PrintBanner ( "Hello");
请问我们为什么不像下面这样使用PrintBanner类型的变量来保存PrintBanner的实例呢?
PrintBanner p = new PrintBanner ( "Hello");
答案:
明确地表明程序的意图,即“并不是使用PrintBanner类中的方法,而是使用Print接口中的方法”。