2.1 Adapter 模式 如果想让额定工作电压是直流12 伏特的笔记本电脑在交流100 伏特A 的AC 电源下工作,应该怎么做呢?通常,我们会使用AC 适配器,将家庭用的交流100 伏特电压转换成我们所需要的直流12 伏特电压。这就是适配器的工作,它位于实际情况与需求之间,填补两者之间的差异。适配器的英文是Adapter,意思是“使……相互适合的东西”。前面说的AC 适配器的作用就是让工作于直流12 伏特环境的笔记本电脑适合于交流100 伏特的环境(图2-1)。 图2-1 适配器的角色 在程序世界中,经常会存在现有的程序无法直接使用,需要做适当的变换之后才能使用的情况。这种用于填补“现有的程序”和“所需的程序”之间差异的设计模式就是Adapter 模式。 Adapter 模式也被称为Wrapper 模式。Wrapper 有“包装器”的意思,就像用精美的包装纸将普通商品包装成礼物那样,替我们把某样东西包起来,使其能够用于其他用途的东西就被称为“包装器”或是“适配器”。 Adapter 模式有以下两种。 ● 类适配器模式(使用继承的适配器) ● 对象适配器模式(使用委托的适配器) 本章将依次学习这两种Adapter 模式。 2.2 示例程序(1)(使用继承的适配器) 首先,让我们来看一段使用继承的适配器的示例程序。这里的示例程序是一段会将输入的字符串显示为(Hello) 或是*Hello* 的简单程序。 目前在Banner 类(Banner 有广告横幅的意思)中,有将字符串用括号括起来的showWithParen方法,和将字符串用* 号括起来的showWithAster 方法。我们假设这个Banner 类是类似前文中的“交流100 伏特电压”的“实际情况”。 假设Print 接口中声明了两种方法,即弱化字符串显示(加括号)的printWeak(weak 有弱化的意思)方法,和强调字符串显示(加* 号)的printStrong(strong 有强化的意思)方法。我们假设这个接口是类似于前文中的“直流12 伏特电压”的“需求”。 A 日本的普通住宅区常用电压是100 伏特,而国内居民区常用电压是220 伏特,此处沿用了原文中的表述,故电压为100 伏特。——译者注 现在要做的事情是使用Banner 类编写一个实现了Print 接口的类,也就是说要做一个将“交流100 伏特电压”转换成“直流12 伏特电压”的适配器。 扮演适配器角色的是PrintBanner 类。该类继承了Banner 类并实现了“需求”——Print接口。PrintBanner 类使用showWithParen 方法实现了printWeak,使用showWithAster方法实现了printStrong。这样,PrintBanner 类就具有适配器的功能了。电源的比喻和示例程序的对应关系如表2-1 所示。 表2-1 电源的比喻和示例程序的对应关系 图2-2 使用了“类适配器模式”的示例程序的类图(使用继承) Banner 类 假设Banner 类(代码清单2-1)是现在的实际情况。 代码清单2-1 Banner 类(Banner.java) 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 + "*"); } } Print 接口 假设Print 接口(代码清单2-2)是“需求”的接口。 代码清单2-2 Print 接口(Print.java) public interface Print { public abstract void printWeak(); public abstract void printStrong(); } PrintBanner 类 PrintBanner 类(代码清单2-3)扮演适配器的角色。它继承(extends)了Banner 类,继承了showWithParen 方法和showWithAster 方法。同时,它又实现(implements)了Print 接口,实现了printWeak 方法和printStrong 方法。 代码清单2-3 PrintBanner 类(PrintBanner.java) public class PrintBanner extends Banner implements Print { public PrintBanner(String string) { super(string); } public void printWeak() { showWithParen(); } public void printStrong() { showWithAster(); } } Main 类 Main 类(代码清单2-4)的作用是通过扮演适配器角色的PrintBanner 类来弱化(带括号)或是强化Hello(带* 号)字符串的显示。 代码清单2-4 Main 类(Main.java) public class Main { public static void main(String[] args) { Print p = new PrintBanner("Hello"); p.printWeak(); p.printStrong(); } } 图2-3 运行结果 请注意,这里我们将PrintBanner 类的实例保存在了Print 类型的变量中。在Main 类中,我们是使用Print 接口(即 调用printWeak 方法和printStrong 方法)来进行编程的。对Main 类的代码而言,Banner 类、showWithParen 方法和showWithAster 方法被完全隐藏起来了。这就好像笔记本电脑只要在直流12 伏特电压下就能正常工作,但它并不知道这12 伏特的电压是由适配器将100 伏特交流电压转换而成的。 Main 类并不知道PrintBanner 类是如何实现的,这样就可以在不用对Main 类进行修改的情况下改变PrintBanner 类的具体实现。 2.3 示例程序(2)(使用委托的示例程序) 之前的示例程序展示了类适配器模式。下面我们再来看看对象适配器模式。在之前的示例程序中,我们使用“继承”实现适配,而这次我们要使用“委托”来实现适配。 小知识:关于委托 “委托”这个词太过于正式了,说得通俗点就是“交给其他人”。比如,当我们无法出席重要会议时,可以写一份委任书,说明一下“我无法出席会议,安排佐藤代替我出席”。委托跟委任的意思是一样的。在Java 语言中,委托就是指将某个方法中的实际处理交给其他实例的方法。 Main 类和Banner 类与示例程序(1)中的内容完全相同,不过这里我们假设Print 不是接口而是类(代码清单2-5)。 也就是说,我们打算利用Banner 类实现一个类,该类的方法和Print 类的方法相同。由于在Java 中无法同时继承两个类(只能是单一继承),因此我们无法将PrintBanner 类分别定义为Print 类和Banner 类的子类。 PrintBanner 类(代码清单2-6)的banner 字段中保存了Banner 类的实例。该实例是在PrintBanner 类的构造函数中生成的。然后,printWeak 方法和printStrong 方法会通过banner 字段调用Banner 类的showWithParen 和showWithAster 方法。 与之前的示例代码中调用了从父类中继承的showWithParen 方法和showWithAster 方法不同,这次我们通过字段来调用这两个方法。 这样就形成了一种委托关系(图2-4)。当PrintBanner 类的printWeak 被调用的时候,并不是PrintBanner 类自己进行处理,而是将处理交给了其他实例(Banner 类的实例)的showWithParen 方法。 图2-4 使用了“对象适配器模式”的示例程序的类图(使用委托) Print 类 代码清单2-5 Print 类(Print.java) public abstract class Print { public abstract void printWeak(); public abstract void printStrong(); } PrintBanner 类 代码清单2-6 PrintBanner 类(PrintBanner.java) public class PrintBanner extends Print { private Banner banner; public PrintBanner(String string) { this.banner = new Banner(string); } public void printWeak() { banner.showWithParen(); } public void printStrong() { banner.showWithAster(); } } 2.4 Adapter 模式中的登场角色 在Adapter 模式中有以下登场角色。 ◆ Target(对象) 该角色负责定义所需的方法。以本章开头的例子来说,即让笔记本电脑正常工作所需的直流12伏特电源。在示例程序中,由Print 接口(使用继承时)和Print 类(使用委托时)扮演此角色。 ◆ Client(请求者) 该角色负责使用Target 角色所定义的方法进行具体处理。以本章开头的例子来说,即直流12伏特电源所驱动的笔记本电脑。在示例程序中,由Main 类扮演此角色。 ◆ Adaptee(被适配) 注意不是Adapt-er(适配)角色,而是Adapt-ee(被适配)角色。Adaptee 是一个持有既定方法的角色。以本章开头的例子来说,即交流100 伏特电源。在示例程序中,由Banner 类扮演此角色。 如果Adaptee 角色中的方法与Target 角色的方法相同(也就是说家庭使用的电压就是12 伏特直流电压),就不需要接下来的Adapter 角色了。 ◆ Adapter(适配) Adapter 模式的主人公。使用Adaptee 角色的方法来满足Target 角色的需求,这是Adapter 模式的目的,也是Adapter 角色的作用。以本章开头的例子来说,Adapter 角色就是将交流100 伏特电压转换为直流12 伏特电 压的适配器。在示例程序中,由PrintBanner 类扮演这个角色。 在类适配器模式中,Adapter 角色通过继承来使用Adaptee 角色,而在对象适配器模式中,Adapter 角色通过委托来使用Adaptee 角色。 图2-5 和图2-6 展示了这两种Adapter 模式的类图。 图2-5 类适配器模式的类图(使用继承) 图2-6 对象适配器模式的类图(使用委托) 2.5 拓展思路的要点 什么时候使用Adapter 模式 一定会有读者认为“如果某个方法就是我们所需要的方法,那么直接在程序中使用不就可以了吗?为什么还要考虑使用Adapter 模式呢?”那么,究竟应当在什么时候使用Adapter 模式呢?很多时候,我们并非从零开始编程,经常会用到现有的类。特别是当现有的类已经被充分测试过了,Bug 很少,而且已经被用于其他软件之中时,我们更愿意将这些类作为组件重复利用。 Adapter 模式会对现有的类进行适配,生成新的类。通过该模式可以很方便地创建我们需要的方法群。当出现Bug 时,由于我们很明确地知道Bug 不在现有的类(Adaptee 角色)中,所以只需调查扮演Adapter 角色的类即可。这样一来,代码问题的排查就会变得非常简单。 如果没有现成的代码 让现有的类适配新的接口(API)时,使用Adapter 模式似乎是理所当然的。不过实际上,我们在让现有的类适配新的接口时,常常会有“只要将这里稍微修改下就可以了”的想法,一不留神就会修改现有的代码。但是需要注意的是,如果要对已经测试完毕的现有代码进行修改,就必须在修改后重新进行测试。 使用Adapter 模式可以在完全不改变现有代码的前提下使现有代码适配于新的接口(API)。此外,在Adapter 模式中,并非一定需要现成的代码。只要知道现有类的功能,就可以编写出新的类。 版本升级与兼容性 软件的生命周期总是伴随着版本的升级,而在版本升级的时候经常会出现“与旧版本的兼容性”问题。如果能够完全抛弃旧版本,那么软件的维护工作将会轻松得多,但是现实中往往无法这样做。这时,可以使用Adapter 模式使新旧版本兼容,帮助我们轻松地同时维护新版本和旧版本。 例如,假设我们今后只想维护新版本。这时可以让新版本扮演Adaptee 角色,旧版本扮演Target 角色。接着编写一个扮演Adapter 角色的类,让它使用新版本的类来实现旧版本的类中的方法。 图2-7 展示了这些关系的类图(请注意它并非UML 图)。 图2-7 提高与旧版本软件的兼容性的Adapter 模式 功能完全不同的类 当然,当Adaptee 角色和Target 角色的功能完全不同时,Adapter 模式是无法使用的。就如同我们无法用交流100 伏特电压让自来水管出水一样。 2.6 相关的设计模式 ◆ Bridge 模式(第9 章) Adapter 模式用于连接接口(API)不同的类,而Bridge 模式则用于连接类的功能层次结构与实现层次结构。 ◆ Decorator 模式(第12 章) Adapter 模式用于填补不同接口(API)之间的缝隙,而Decorator 模式则是在不改变接口(API)的前提下增加功能。 2.7 本章所学知识 在本章中,我们学习了Adapter 模式。Adapter 模式用于填补具有不同接口(API)的两个类之间的缝隙。此外,我们还学习了“使用继承”和“使用委托”这两种实现Adapter 模式的方式和它们各自的特征。 现在大家应该对设计模式有些了解了,那么接下来让我们做两道练习题。 2.8 练习题 答案请参见附录A(P.295) ●习题2-1 Java 在示例程序中生成PrintBanner 类的实例时,我们采用了如下方法,即使用Print类型的变量来保存PrintBanner 实例。 Print p = new PrintBanner("Hello"); 请问我们为什么不像下面这样使用PrintBanner 类型的变量来保存PrintBanner 的实例呢? PrintBannerp = new PrintBanner("Hello"); ●习题2-2 在java.util.Properties 类中,可以像下面这样管理键值对(属性)。 year=2004 month=4 day=21 java.util.Properties 类提供了以下方法,可以帮助我们方便地从流中取出属性或将属性写入流中。 void load(InputStream in) throws IOException 从InputStream 中取出属性集合 void store(OutputStream out, String header) throws IOException 向OutputStream 写入属性集合。header 是注释文字 请使用Adapter 模式编写一个将属性集合保存至文件中的FileProperties 类。 这里,我们假设在代码清单2-7 中的FileIO 接口(Target 角色)中声明了将属性集合保存至文件的方法,并假设FileProperties 类会实现这个FileIO 接口。 输入文件file.txt 以及输出文件newfile.txt 的内容请参见代码清单2-9 和代码清单2-10(以# 开始的内容是java.util.Properties 类自动附加的注释文字)。 当FileProperties 类编写完成后,即使FileProperties 类不了解java.util.Properties 类的方法,只要知道FileIO 接口的方法也可以对属性进行处理。还是以本章开头的电源的例子来说,java.util.Properties 类相当于现在家庭中使用的100 伏特交流电压,FileIO 接口相当于所需要的直流12 伏特电源, 而FileProperties 类则相当于适配器。 代码清单2-7 FileIO 接口(FileIO.java) import java.io.*; public interface FileIO { public void readFromFile(String filename) throws IOException; public void writeToFile(String filename) throws IOException; public void setValue(String key, String value); public String getValue(String key); } 代码清单2-8 Main 类(Main.java) import java.io.*; public class Main { public static void main(String[] args) { FileIO f = new FileProperties(); try { f.readFromFile("file.txt"); f.setValue("year", "2004"); f.setValue("month", "4"); f.setValue("day", "21"); f.writeToFile("newfile.txt"); } catch (IOException e) { e.printStackTrace(); } } } 代码清单2-9 输入文件(file.txt) year=1999 代码清单2-10 输出文件(newfile.txt) #written by FileProperties #Wed Apr 21 18:21:00 JST 2004 day=21 year=2004 month=4