结构性模式描述类和对象怎样结合在一起成为较大的结构。 结构性模式描述两种不同的东西:类与类的实例。根据它们所描述的东西的不同, 结构性模式可以分为类结构模式和实例结构模式两种。
类结构模式使用继承(inheritance)来把类,接口等组合在一起,形成更大的结构。 当一个类从父类继承,并实现某接口时,这个新的类就把父类的结构和接口的结构结合起来。 类结构模式是静态的。一个类结构模式的典型的例子,就是类形式的变压器模式。
实例结构模式描述各种不同类型的把对象组合在一起,实现新的功能的方法。实例结构模式是动态的。 一个典型的实例结构模式,就是代理人模式,代理人模式将在以后介绍。其它的例子包括后面将要介绍的复合模式, 飞行重量模式,装饰模式,以及实例形式的变压器模式等。
有一些模式会有类结构模式的形式和实例结构模式的形式两种,成为以上两种形式的结构模式的极好注解。 本节要介绍的变压器模式就是这样,它有类形式和实例形式两种。
变压器模式的介绍
变压器模式把一个类的接口变换成客户端所期待的另一种接口。变压器模式使原本无法在一起工作的两个类能够在一起工作。 如前所述,变压器模式是关于类结构的结构性模式,因而是静态的模式。
这很象变压器(Adapter)---变压器把一种电压变换成另一种电压。当我把美国的电器拿回中国大陆去用的时候, 我就面临电压不同的问题。美国的生活用电压是110伏,而中国的电压是220伏。我如果要在中国大陆使用我在美国使用的电器, 我就必须有一个能把220伏电压转换成110伏电压的变压器。而这正象是本模式所做的事,因此此模式被称为变压器模式。
读者可能也会想到,Adapter在中文也可翻译为转换器(适配器)。实际上,转换器(适配器)也是一个合适的名字。仍用电器作例子, 美国的电器的插头一般是三相的,即除了阳极,阴极外,还有一个地极。中国大陆的建筑物内的电源插座一般只有两极,没有地极。 这时候,即便电器的确可以接受220伏电压,电源插座和插头不匹配,也使电器无法使用。 一个三相到两相的转换器(适配器)就能解决这个问题。因此此模式也可被称为转换器(适配器)模式。
同时,这种做法也很象包装过程,被包装的物体的真实样子被包装所掩盖和改变,因此有人把这种模式叫做包装(Wrapper)模式。事实上, 我们经常写很多这样的wrapper类,把已有的一些类包裹起来,使之能有满足需要的接口。
变压器模式有类形式和实例形式两种不同的形式。
类形式的变压器模式的定义
类形式的变压器模式的类图定义如下。
图1. 类形式的类变压器模式的类图定义
在图1可以看出,模式所涉及的成员有:
- 目标(Target)。这就是我们所期待得到的接口。注意,由于这里讨论的是类变压器模式,因此目标不可以是类。
- 源(Adaptee)。现有需要适配的接口。
- 变压器(Adapter)。变压器类是本模式的核心。变压器把源接口转换成目标接口。显然,这一角色不可以是接口, 而必须是实类。
本模式的示范代码如下:
package com.javapatterns.adapter.classAdapter; public interface Target { /** * Class Adaptee contains operation sampleOperation1. */ void sampleOperation1(); /** * Class Adaptee doesn't contain operation sampleOperation2. */ void sampleOperation2(); }
代码清单1. Target的源代码。
package com.javapatterns.adapter.classAdapter; public class Adaptee { public void sampleOperation1(){} }
代码清单2. Adaptee的源代码。
package com.javapatterns.adapter.classAdapter; public class Adapter extends Adaptee implements Target { /** * Class Adaptee doesn't contain operation sampleOperation2. */ public void sampleOperation2() { // Write your code here } }
代码清单3. Adapter的源代码。
类形式的变压器模式的效果
第一、 使用一个实类把源(Adaptee)适配到目标(Target)。这样一来,如果你想把源以及源的子类都使用此类适配, 就行不通了。
第二、 由于变压器类是源的子类,因此可以在变压器类中置换(override)掉源的一些方法。
第三、 由于只引进了一个变压器类,因此只有一个路线到达目标类。问题得到简化。
实例形式的变压器模式的定义
实例形式的变压器模式的类图定义如下。
图2. 实例变压器模式的类图定义
在图1可以看出,模式所涉及的成员有:
- 目标(Target)。这就是我们所期待得到的接口。目标可以是实的或抽象的类。
- 源(Adaptee)。现有需要适配的接口。
- 变压器(Adapter)。变压器类是本模式的核心。变压器把源接口转换成目标接口。 显然,这一角色必须是实类。
本模式的示范代码如下:
package com.javapatterns.adapter; public interface Target { /** * Class Adaptee contains operation sampleOperation1. */ void sampleOperation1(); /** * Class Adaptee doesn't contain operation sampleOperation2. */ void sampleOperation2(); }
代码清单4. Target的源代码。
package com.javapatterns.adapter; public class Adapter implements Target { public Adapter(Adaptee adaptee){ super(); this.adaptee = adaptee; } public void sampleOperation1(){ adaptee.sampleOperation1(); } public void sampleOperation2(){ // Write your code here } private Adaptee adaptee; }
代码清单5. Adapter的源代码。
package com.javapatterns.adapter; public class Adaptee { public void sampleOperation1(){} }
代码清单6. Adaptee的源代码。
实例形式的变压器模式的效果
第一、 一个变压器可以把多种不同的源适配到同一个目标。换言之,同一个变压器可以把源类和它的子类都适配到目标接口。
第二、 与类形式的变压器模式相比,要想置换源类的方法就不容易。如果一定要置换掉源类的一个或多个方法,就只好先做一个源类的子类, 将源类的方法置换掉,然后再把源类的子类当作真正的源进行适配。
第三、 虽然要想置换源类的方法不容易,但是要想增加一些新的方法则方便得很。 而且新增加的方法同时适用于所有的源。
在什么情况下使用变压器模式
在以下各种情况下使用变压器模式:
第一、 你需要使用现有的类,而此类的接口不符合你的需要。
第二、 你想要建立一个可以重复使用的类,用以与一些彼此之间没有太大关联的一些类, 包括一些可能在将来引进的类一起工作。这些源类不一定有很复杂的接口。
第三、 (对实例形式的变压器模式而言)你需要改变多个已有的子类的接口, 如果使用类形式的变压器模式,就要针对每一个子类做一个变压器类,而这不太实际。
J2SE中的变压器模式的使用
在爪哇语言2.0的标准SDK中,有很多的变压器类。如:
- 库程序包java/awt/event中有
- ComponentAdapter
- ContainerAdapter
- FocusAdapter
- HierarchyBoundsAdapter
- KeyAdapter
- MouseAdapter
- MouseMotionAdapter
- WindowAdapter
- 库程序包Javax/swing/event中有
- InternalFrameAdapter
- MouseInputAdapter
这些都是变压器模式使用的实际例子。值得指出的是,WindowAdapter的建立者们不可能预见到你所要使用的目标接口, 因此WindowAdapter不可能实现你的目标接口。但是,在考察了这些变压器类的使用范围之后,我们会发现, WindowAdapter只需实现WindowListener的接口即可,也就是说,目标接口被省略了。请见下面的解释。
抽象类WindowAdapter是变压器模式的一个例子
抽象类WindowAdapter是为接受视窗的事件而准备的。此抽象类内所有的方法都是空的。 使用此类可以很方便地创立listener对象。置换(Override)你所感兴趣的那个事件所对应的方法。 如果你不使用此抽象类,那么你必然规律要实现WindowsListener接口,而那样你就不得不实现所有接口中的方法, 即便是你不需要的事件所对应的方法,你也要给出一个空的方法,而这显然不方便。
显然,抽象类WindowAdapter的目标接口可以选得与源接口一样,而不影响效果。 这就解释了为什么目标接口不出现在WindowAdapter类图(见下面)里。
图3. 本例子SwingUI类与WindowAdapter实例变压器模式的类图定义
SwingUI类的代码如下。
import java.awt.Color; import java.awt.BorderLayout; import java.awt.event.*; import javax.swing.*; class SwingUI extends JFrame implements ActionListener { JLabel text, clicked; JButton button, clickButton; JPanel panel; private boolean m_clickMeMode = true; Public SwingUI() { text = new JLabel("我很高兴!"); button = new JButton("理我"); button.addActionListener(this); panel = new JPanel(); panel.setLayout(new BorderLayout()); panel.setBackground(Color.white); getContentPane().add(panel); panel.add(BorderLayout.CENTER, text); panel.add(BorderLayout.SOUTH, button); } public void actionPerformed(ActionEvent event) { Object source = event.getSource(); if (m_clickMeMode) { text.setText("我很烦!"); button.setText("别理我"); m_clickMeMode = false; } else { text.setText("我很高兴!"); button.setText("理我"); m_clickMeMode = true; } } public static void main(String[] args) { SwingUI frame = new SwingUI(); frame.setTitle("我"); WindowListener listener = new WindowAdapter() { public void windowClosing(WindowEvent e) { System.exit(0); } }; frame.addWindowListener(listener); frame.pack(); frame.setVisible(true); } }
代码清单7. SwingUI类的源代码。红色的代码就是使用WindowAdapter的无名内部类。
显然,由于无名内部类是继承自WindowAdapter抽象类,因此只需置换(override)掉我们需要的方法, 即windowClosing()而不必操心WindowListener的其它方法。
本例子在运行时的样子:
图4. SwingUI类在运行时的样子。单击命令键“理我”就变成下图的样子。
图5. 再单击命令键“别理我”就会回到前图的样子。
利用变压器模式指方为圆
中国古代有赵高指鹿为马的故事。鹿与马有很多相似之处,没见过的人本就分辨不清,指一指可能没什么大不了的。 指方为圆是否太过?非也。本例就是要指方为圆,需要的只是变压器模式这个魔术手指(Magic Finger)。
变压器模式在本例子的类图如下。
图6. 指方为圆的变压器模式类图
package com.javapatterns.adapter.cube2ball; public class Cube { public Cube(double width) { this.width = width; } public double calculateVolume() { return width * width * width; } public double calculateFaceArea() { return width * width; } public double getWidth() { return this.width; } public void setWidth(double width) { this.width = width; } private double width; }
代码清单8. Cube类的源代码。。
package com.javapatterns.adapter.cube2ball; public interface BallIF { double calculateArea(); double calculateVolume(); double getRadius(); void setRadius(double radius); }
代码清单9. BallIF接口的源代码。
package com.javapatterns.adapter.cube2ball; public class MagicFinger implements BallIF { public MagicFinger(Cube adaptee) { super(); this.adaptee = adaptee; radius = adaptee.getWidth(); } public double calculateArea() { return PI * 4.0D * ( radius * radius ); } public double calculateVolume() { return PI * 4.0D/3.0D * ( radius * radius * radius ); } public double getRadius() { return radius; } public void setRadius(double radius) { this.radius = radius; } private double radius = 0; private static final double PI = 3.14D; private Cube adaptee; }
代码清单10. MagicFinger类的源代码。
如果读者还记得中学的数学的话,应该可以看出,我们的指方为圆系统其实还是有道理的。它接受一个正方体, 返还此正方体的内切球,也就是能放进此正方体的最大的球。
显然,本例子里,我们使用的是实例形式的变压器模式。这样做的好处是,如果一旦我们决定不仅要支持正方体, 而且要支持四面体等多面体,我们可以使用同一个MagicFinger类,而不必针对每一个多面体都建立一个MagicFinger类。 这样也比较符合“魔术手指”这个名字。
关于模式实现的讨论
本模式在实现时有以下这些值得注意的地方:
第一、目标接口可以省略。此时,目标接口和源接口实际上是相同的。 由于源是一个接口,而变压器类是一个类(或抽象类),因此这种做法看似平庸而并不平庸, 它可以使客户端不必实现不需要的方法。这一点已经在WindowAdapter的例子里做了详尽的分析。
第二、变压器类可以是抽象类。这已经在WindowAdapter的例子里看到了。实际上,WindowAdapter的例子过于简单。 实际的情形里,你可能想给出一些实方法。
第三、带参数的变压器模式。使用这种办法,变压器类就不必,有时可能不能是源类的子类。 变压器类根据参数返还一个合适的实例给客户端。
问答题
第1题、请做一个小猫(kittie)的实类,并实现miao(),catchRat(),run(),sleep()等方法。 再做一个小狗(doggie)的接口,要求有wao(),fetchBall(),run(),sleep()等方法。
现在你的女朋友想要一只小狗,可是你只找到的一只小猫。请用变压器模式把小猫“适配成”小狗, 让你的女朋友满意。(提示:量力而为。)
第2题、请指出第一题的解答所使用的是那一种形式的变压器模式。
第3题、笔者在许多场合给各种不同水准的专业人士作过各种编程模式的介绍,发现参加OOP开发工作的不同时间长短的人, 对不同的模式理解接受的速度有所不同。唯独在讲过这个男朋友与小狗小猫的例子后,大家对变压器模式的理解都很准确。 让笔者百思不得其解。你知道这是怎样回事吗?
第4题、请讲一讲使用实例形式的变压器模式和使用类形式的变压器模式在第一题的解决上有何影响。
问答题答案
第1题、根据提示,我们可以量力而为。因此,我们将把miao()“适配成”wao(),catchRat()“适配成”fetchBall(), run(),sleep()不变。源代码如下:
图7. 男朋友小狗适配器的类图。
package com.javapatterns.adapter.kittie2doggie; public interface Doggie { void wao(); void fetchBall(); void run(); void sleep(); void setName(String name); String getName(); }
代码清单11. SwingUI类的源代码。红色的代码就是使用WindowAdapter的无名内部类。
package com.javapatterns.adapter.kittie2doggie; public class Kittie { public void miao(){} public void catchRat() { } public void run() { } public void sleep() { } public String getName(){ return name; } public void setName(String name){ this.name = name; } }
代码清单12. SwingUI类的源代码。红色的代码就是使用WindowAdapter的无名内部类。
package com.javapatterns.adapter.kittie2doggie; public class Boyfriend extends Kittie implements Doggie { public void wao() { this.miao(); } public void fetchBall() { this.catchRat(); } public void run() { super.run(); } public void sleep() { super.sleep(); } public String getName() { return super.getName(); } public void setName(String name) { super.setName(name); } }
代码清单13. SwingUI类的源代码。红色的代码就是使用WindowAdapter的无名内部类。
怎么,她不满意呀?那也有办法:把wao(),fatchBall()当作新的方法,在变压器类中实现。由于你扮演变压器角色, 当她调用wao(),fatchBall()方法是,你就叫一声,或把球捡回来就可以了。
你不满意呀?那就再去找一只真正的小狗吧。变压器模式的威力就到此为止了。
第2题、这里使用的是类形式的变压器模式。
第3题、我的一个学生告诉我,理解这个问题的关键,即男朋友必须装小狗。
第4题、使用类形式的结果是,她一旦想要另一个宠物,她就得换一个男朋友。 使用实例形式的变压器模式的结果是,她如果想要另一个宠物,原来的男朋友就得身兼几种身份。