Java 设计模式


Java 设计模式 1、爪哇语言结构性模式之变压器模式介绍 1.1、什么是结构性模式 结构性模式描述类和对象怎样结合在一起成为较大的结构。 结构性模式描述两种 不同的东西:类与类的实例。根据它们所描述的东西的不同, 结构性模式可以分为类 结构模式和实例结构模式两种。 类结构模式使用继承(inheritance)来把类,接口等组合在一起,形成更大的结构。 当一个类从父类继承,并实现某接口时,这个新的类就把父类的结构和接口的结构结合 起来。 类结构模式是静态的。一个类结构模式的典型的例子,就是类形式的变压器模 式。 实例结构模式描述各种不同类型的把对象组合在一起,实现新的功能的方法。实例 结构模式是动态的。 一个典型的实例结构模式,就是代理人模式,代理人模式将在以 后介绍。其它的例子包括后面将要介绍的复合模式, 飞行重量模式,装饰模式,以及 实例形式的变压器模式等。 有一些模式会有类结构模式的形式和实例结构模式的形式两种,成为以上两种形式 的结构模式的极好注解。 本节要介绍的变压器模式就是这样,它有类形式和实例形式 两种。 1.2、变压器模式的介绍 变压器模式把一个类的接口变换成客户端所期待的另一种接口。变压器模式使原本 无法在一起工作的两个类能够在一起工作。 如前所述,变压器模式是关于类结构的结 构性模式,因而是静态的模式。 这很象变压器(Adapter)---变压器把一种电压变换成另一种电压。当我把美国的电器 拿回中国大陆去用的时候, 我就面临电压不同的问题。美国的生活用电压是 110 伏, 而中国的电压是 220 伏。我如果要在中国大陆使用我在美国使用的电器, 我就必须有 一个能把 220 伏电压转换成 110 伏电压的变压器。而这正象是本模式所做的事,因此 此模式被称为变压器模式。 读者可能也会想到,Adapter 在中文也可翻译为转换器(适配器)。实际上,转换器 (适配器)也是一个合适的名字。仍用电器作例子, 美国的电器的插头一般是三相的, 即除了阳极,阴极外,还有一个地极。中国大陆的建筑物内的电源插座一般只有两极, 没有地极。 这时候,即便电器的确可以接受 220 伏电压,电源插座和插头不匹配,也 使电器无法使用。 一个三相到两相的转换器(适配器)就能解决这个问题。因此此模 式也可被称为转换器(适配器)模式。 同时,这种做法也很象包装过程,被包装的物体的真实样子被包装所掩盖和改变, 因此有人把这种模式叫做包装(Wrapper)模式。事实上, 我们经常写很多这样的 wrapper 类,把已有的一些类包裹起来,使之能有满足需要的接口。 变压器模式有类形式和实例形式两种不同的形式。 1.3、类形式的变压器模式的定义 类形式的变压器模式的类图定义如下。 图 1.1 类形式的类变压器模式的类图定义 在图 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. 1Target 的源代码。 package com.javapatterns.adapter.classAdapter; public class Adaptee { public void sampleOperation1(){} } 代码清单 1.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 } } 代码清单 1.3 Adapter 的源代码。 类形式的变压器模式的效果 第一、 使用一个实类把源(Adaptee)适配到目标(Target)。这样一来,如果你想把 源以及源的子类都使用此类适配, 就行不通了。 第二、 由于变压器类是源的子类,因此可以在变压器类中置换(override)掉源的 一些方法。 第三、 由于只引进了一个变压器类,因此只有一个路线到达目标类。问题得到简 化。 1.4、实例形式的变压器模式的定义 实例形式的变压器模式的类图定义如下。 图 1.2 实例变压器模式的类图定义 在图 1.2 可以看出,模式所涉及的成员有: • 目标(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(); } 代码清单 1.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; } 代码清单 1.5. Adapter 的源代码。 package com.javapatterns.adapter; public class Adaptee { public void sampleOperation1(){} } 代码清单 1.6. Adaptee 的源代码。 1.5、实例形式的变压器模式的效果 第一、 一个变压器可以把多种不同的源适配到同一个目标。换言之,同一个变压 器可以把源类和它的子类都适配到目标接口。 第二、 与类形式的变压器模式相比,要想置换源类的方法就不容易。如果一定要 置换掉源类的一个或多个方法,就只好先做一个源类的子类, 将源类的方法置换掉, 然后再把源类的子类当作真正的源进行适配。 第三、 虽然要想置换源类的方法不容易,但是要想增加一些新的方法则方便得很。 而且新增加的方法同时适用于所有的源。 在什么情况下使用变压器模式 在以下各种情况下使用变压器模式: 第一、 你需要使用现有的类,而此类的接口不符合你的需要。 第二、 你想要建立一个可以重复使用的类,用以与一些彼此之间没有太大关联的 一些类, 包括一些可能在将来引进的类一起工作。这些源类不一定有很复杂的接口。 第三、 (对实例形式的变压器模式而言)你需要改变多个已有的子类的接口, 如 果使用类形式的变压器模式,就要针对每一个子类做一个变压器类,而这不太实际。 1.6、J2SE 中的变压器模式的使用 在爪哇语言 2.0 的标准 SDK 中,有很多的变压器类。如: • 库程序包 java\awt\event 中有 o ComponentAdapter o ContainerAdapter o FocusAdapter o HierarchyBoundsAdapter o KeyAdapter o MouseAdapter o MouseMotionAdapter o WindowAdapter • 库程序包 Javax\swing\event 中有 o InternalFrameAdapter o MouseInputAdapter 这些都是变压器模式使用的实际例子。值得指出的是,WindowAdapter 的建立者 们不可能预见到你所要使用的目标接口, 因此 WindowAdapter 不可能实现你的目标接 口。但是,在考察了这些变压器类的使用范围之后,我们会发现, WindowAdapter 只 需实现 WindowListener 的接口即可,也就是说,目标接口被省略了。请见下面的解释。 抽象类 WindowAdapter 是变压器模式的一个例子 抽象类 WindowAdapter 是为接受视窗的事件而准备的。此抽象类内所有的方法都 是空的。 使用此类可以很方便地创立 listener 对象。置换(Override)你所感兴趣的那个 事件所对应的方法。 如果你不使用此抽象类,那么你必然规律要实现 WindowsListener 接口,而那样你就不得不实现所有接口中的方法, 即便是你不需要的事件所对应的方 法,你也要给出一个空的方法,而这显然不方便。 显然,抽象类 WindowAdapter 的目标接口可以选得与源接口一样,而不影响效果。 这就解释了为什么目标接口不出现在 WindowAdapter 类图(见下面)里。 图 1.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); } } 代码清单 1.7. SwingUI 类的源代码。红色的代码就是使用 WindowAdapter 的无名内部类。 显然,由于无名内部类是继承自 WindowAdapter 抽象类,因此只需置换(override) 掉我们需要的方法, 即 windowClosing()而不必操心 WindowListener 的其它方法。 本例子在运行时的样子: 图 1.4. SwingUI 类在运行时的样子。单击命令键“理我”就变成下图的样子。 图 1.5. 再单击命令键“别理我”就会回到前图的样子。 1.7、利用变压器模式指方为圆 中国古代有赵高指鹿为马的故事。鹿与马有很多相似之处,没见过的人本就分辨 不清,指一指可能没什么大不了的。 指方为圆是否太过?非也。本例就是要指方为圆, 需要的只是变压器模式这个魔术手指(Magic Finger)。 变压器模式在本例子的类图如下。 图 1.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; } 代码清单 1.8. Cube 类的源代码。 package com.javapatterns.adapter.cube2ball; public interface BallIF { double calculateArea(); double calculateVolume(); double getRadius(); void setRadius(double radius); } 代码清单 1.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; } 代码清单 1.10. MagicFinger 类的源代码。 如果读者还记得中学的数学的话,应该可以看出,我们的指方为圆系统其实还是 有道理的。它接受一个正方体, 返还此正方体的内切球,也就是能放进此正方体的最 大的球。 显然,本例子里,我们使用的是实例形式的变压器模式。这样做的好处是,如果 一旦我们决定不仅要支持正方体, 而且要支持四面体等多面体,我们可以使用同一个 MagicFinger 类,而不必针对每一个多面体都建立一个 MagicFinger 类。 这样也比较 符合“魔术手指”这个名字。 1.8、关于模式实现的讨论 本模式在实现时有以下这些值得注意的地方: 第一、目标接口可以省略。此时,目标接口和源接口实际上是相同的。 由于源是 一个接口,而变压器类是一个类(或抽象类),因此这种做法看似平庸而并不平庸, 它 可以使客户端不必实现不需要的方法。这一点已经在 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()不变。源代码如下: 图 1.7. 男朋友小狗适配器的类图。 package com.javapatterns.adapter.kittie2doggie; public interface Doggie { void wao(); void fetchBall(); void run(); void sleep(); void setName(String name); String getName(); } 代码清单 1.11. SwingUI 类的源代码。 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; } } 代码清单 1.12. SwingUI 类的源代码。 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); } } 代码清单 1.13. SwingUI 类的源代码。 怎么,她不满意呀?那也有办法:把 wao(),fatchBall()当作新的方法,在变压器类 中实现。由于你扮演变压器角色, 当她调用 wao(),fatchBall()方法是,你就叫一声,或 把球捡回来就可以了。 你不满意呀?那就再去找一只真正的小狗吧。变压器模式的威力就到此为止了。 第 2 题、这里使用的是类形式的变压器模式。 第 3 题、我的一个学生告诉我,理解这个问题的关键,即男朋友必须装小狗。 第 4 题、使用类形式的结果是,她一旦想要另一个宠物,她就得换一个男朋友。 使 用实例形式的变压器模式的结果是,她如果想要另一个宠物,原来的男朋友就得身兼几 种身份。 2、爪哇语言抽象工厂创立性模式介绍 工厂模式有简单工厂模式,工厂方法模式和抽象工厂模式几种形态。其中简单工厂模式 和工厂方法模式已经在前面作过介绍。在这里,我们来介绍抽象工厂模式。 抽象工厂模式是所有形态的工厂模式中最为抽象和最具广泛性的一种形态。 抽象工厂模式的定义 抽象工厂模式是工厂方法模式的进一步扩广化和抽象化。我们给出抽象工厂模式的 类图定义如下。 图 2.1. 抽象工厂模式的类图定义 从上图可以看出,简单工厂模式涉及到以下的角色 抽象工厂(AbstractFactory)类或接口 担任这个角色的是工厂方法模式的核心,它是与应用程序无关的。任何在模式中 创立对象的工厂类必须实现这个接口,或继承这个类。 实工厂类 (Conrete Factory) 担任这个角色的是与应用程序紧密相关的,直接在应用程序调用下,创立产品实 例的那样一些类。 抽象产品 (Abstract Product) 担任这个角色的类是工厂方法模式所创立的对象的父类,或它们共同拥有的接 口。 实产品 (Concrete Product) 担任这个角色的类是工厂方法模式所创立的任何对象所属的类。 怎么这个类图和工厂方法模式的类图看起来是一样的? 是的,图是一样的,但是含义有很大的不同。必须指出,在抽象工厂模式中,抽象 产品 (AbstractProduct) 可能是一个或多个,从而构成一个或多个产品族(Product Family)。 在只有一个产品族的情况下,抽象工厂模式实际上退化到工厂方法模式。在 上面的类图中,只给出了一个产品族,相当于位图中的一个点,而完整的位图应当是三 维的,如下图。 图 2.2. 抽象工厂模式的位图 从位图可以清楚地看到,与纸面垂直的数轴,即第三维轴,是代表产品族的数轴。 上面的位图中展示的是有两个产品族,族 A 和族 B 的情形。 在只有一个产品族时,第三维就坍缩掉,位图也就只剩下两维。这时抽象工厂模式 就退化得与工厂方法模式一模一样。 在什么情形下应当使用抽象工厂模式 在以下情况下,应当考虑使用抽象工厂模式。 首先,一个系统应当不依赖于产品类实例被创立,组成,和表示的细节。这对于所 有形态的工厂模式都是重要的。 其次,这个系统的产品有多于一个的产品族。 第三,同属于同一个产品族的产品是设计成在一起使用的。这一约束必须得在系统 的设计中体现出来。 最后,不同的产品以一系列的接口的面貌出现,从而使系统不依赖于接口实现的细 节。 其中第二丶第三个条件是我们选用抽象工厂模式而非其它形态的工厂模式的关键 性条件。 抽象工厂模式在小花果园系统中的实现 现在,我们在佛罗里达的渡假小屋修整好啦。接下来,一项重要而光荣的工作,就 是开发小屋后面的小花园。这意味着,我们有两处小花园需要照料,一处在北方地区, 另一处在亚热带地区。抽象工厂模式正好适用于我们的情况。 图 2.3. 抽象工厂模式应用于小花果园系统中。三种不同的背景颜色可以区分工厂类,蔬菜类(第 一产品族),和水果类的类图(第二产品族) 两处花园就相当于两个产品族。显然,给北方花园的植物是要种植在一起的,给南 方花园的植物是要另种植在一起的。这种分别应当体现在系统的设计上面。这就满足了 应当使用抽象工厂模式的第二和第三个条件。 package com.javapatterns.abstractfactory; public interface Gardener {} 代码清单 2.1. 接口 Gardener。 package com.javapatterns.abstractfactory; public class NorthenGardener implements Gardener { public VeggieIF createVeggie(String name) { return new NorthernVeggie(name); } public FruitIF createFruit(String name) { return new NorthernFruit(name); } } 代码清单 2.2. 实工厂类 NorthenGardener。 package com.javapatterns.abstractfactory; public class TropicalGardener implements Gardener { public VeggieIF createVeggie(String name) { return new TropicalVeggie(name); } public FruitIF createFruit(String name) { return new TopicalFruit(name); } } 代码清单 2.3. 实工厂类 TropicalGardener。 package com.javapatterns.abstractfactory; public interface VeggieIF {} 代码清单 2.4. 接口 VeggieIF。 package com.javapatterns.abstractfactory; public class NorthernVeggie implements VeggieIF { public NorthernVeggie(String name) { this.name = name; } public String getName(){ return name; } public void setName(String name){ this.name = name; } private String name; } 代码清单 2.5. 实产品类 NorthernVeggie。实产品类 NorthernFruit 与此极为类似,故 略去。 package com.javapatterns.abstractfactory; public class TropicalVeggie implements VeggieIF { public TropicalVeggie(String name) { this.name = name;} public String getName(){ return name; } public void setName(String name){ this.name = name; } private String name; } 代码清单 2.6. 实产品类 TropicalVeggie。实产品类 TropicalFruit 与此极为类似,故略 去。 笔者对植物的了解有限,为免遗笑大方,在上面的系统里采用了简化处理。没有给 出高纬度和低纬度的水果类或蔬菜类的具体名称。 抽象工厂模式的另一个例子 这个例子讲的是微型计算机的生产。产品族有两个,PC(IBM 系列)和 Mac(MacIntosh 系列)。显然,我们应该使用抽象工厂模式,而不是工厂方法模式,因 为后者适合于处理只有一个产品族的情形。 图 2.4. 抽象工厂模式应用于微型计算机生产系统中。两种不同的背景颜色可以区分两类产品 族,及其对应的实工厂类 关于模式的实现 在抽象实现工厂模式时,有下面一些值得注意的技巧。 第一丶实工厂类可以设计成单态类。很显然,在小花果园系统中,我们只需要 NorthenGardener 和 TropicalGardener 的一个实例就可以了。关于单态类的知识,请 见<爪哇语言单态类创立性模式>。 第二丶在实现抽象工厂模式时,产品类往往分属多于一个的产品族,而针对每一族, 都需要一个实工厂类。在很多情况下,几个实工厂类都彼此相象,只有些微的差别。 这时,笔者建议使用原始模型(Prototype)模式。这一模式会在以后介绍,届时作者 会进一步阐述这一点。 第三丶设计更加灵活的实工厂。以微型计算机生产系统为例,PCProducer 是一个 实工厂类,它的不灵活之处在于,每一种产品都有一个工厂方法。CPU 有 createCPU(), RAM 有 createRAM(),等等。如果一个已有的系统需要扩充,比如增加硬盘这一新产 品,我们就需要增加一系列的接口 (createHD())丶类(HD, PCHD, MacHD)和方法。这 似乎不很理想。 一个解决的办法是,把 createCPU(),createRAM(), createHD()这几个方法合并为一 个 createPart(String type)方法。这个合并后的方法返还一个 Part 接口。所有的产品都 要实现这一接口,而 CPU,RAM,和 HD 接口则不再需要了。每一个实产品都需要有 一个属性,表明它们的种类是 CPU,RAM,和 HD。 这样做的结果是,数据类型的丰富结构被扁平化了。客户端拿到的永远是一个 Part 接口。这对客户端而言不很安全。 第四丶抽象工厂类可以配备静态方法,以返还实工厂。设计的方法有两种。 一种是以一个静态方法,按照参量的值,返回所对应的实工厂。静态方法的数据类 型是抽象方法类。 另一种是以每一个实工厂类都配备一个静态方法,其数据类型是该实工厂类。 问答题 第 1 题。如上面的讨论,抽象工厂类可以配备一个静态方法,按照参量的值,返回 所对应的实工厂。请把微型计算机生产系统的抽象工厂类按照这一方案改造,给出 UML 类图和源代码。 第 2 题。如上面的讨论,抽象工厂类可以配备一系列静态方法对应一系列的实工厂。 请把微型计算机生产系统的抽象工厂类按照这一方案改造,给出 UML 类图和源代码。 第 3 题。如上面的讨论,实工厂类可以设计成单态类。请在第 1 题的基础上把微型 计算机生产系统的实工厂类按照这一方案改造,给出 UML 类图和源代码。 问答题答案 第 1 题。微型计算机生产系统的抽象工厂原本是接口,现在需要改造成抽象类。 图 2.5. 三种不同的背景颜色可以区分抽象工厂类,两类产品族,及其对应的实工厂类。 ComputerProducer 类图中类名为斜体表明该类是抽象的,而 getProducer()的下划线表明该方 法是静态的 package com.javapatterns.abstractfactory.exercise1; public class ComputerProducer { public static ComputerProducer getProducer(String which) { if (which.equalsIgnoreCase("PC")) { return new PCProducer(); } else (which.equalsIgnoreCase("Mac")) { return new MacProducer(); } } } 代码清单 2.7. 抽象类 ComputerProducer 的方法 getProducer(String which)。 第 2 题。略。 第 3 题。本题答案是在第 1 题基础之上的。 图 2.6. 三种不同的背景颜色可以区分抽象工厂类,两类产品族,及其对应的实工厂类。 ComputerProducer 类图中类名为斜体表明该类是抽象的,而 getProducer()的下划线表明该方 法是静态的。MacProducer 和 PCProducer 的构造子是私有的,因此这两个类必须自己将自 己实例化。 package com.javapatterns.abstractfactory.exercise3; abstract public class ComputerProducer { public static ComputerProducer getProducer(String which) { if (which.equalsIgnoreCase("PC")) { return PCProducer.getInstance(); } else (which.equalsIgnoreCase("Mac")) { return MacProducer.getInstance(); } } } 代码清单 2.8.抽象工厂类 ComputerProducer。 package com.javapatterns.abstractfactory.exercise3; public class MacProducer extends ComputerProducer { private MacProducer() { } public CPU createCPU() {} public RAM createRAM() {} private static final m_MacProducer = new MacProducer(); } 代码清单 2.9. 实工厂类 MacProducer 是单态类。 读过笔者<单态创立性模式>一节的读者应当知道,这里使用的单态类实现方法是饿汉式 方法。 package com.javapatterns.abstractfactory.exercise3; public class PCProducer extends ComputerProducer { private PCProducer() { } public CPU createCPU() {} public RAM createRAM() {} private static final m_PCProducer = new PCProducer(); } 代码清单 2.10. 实工厂类 PCProducer 是单态类, 使用的单态类实现方法是饿汉式方法。 各产品类没有变化,因此不在此重复。 3、爪哇语言工厂方法创立性模式介绍 正如同笔者在<简单工厂模式>一节里介绍的,工厂模式有简单工厂模式,工厂方法模式和 抽象工厂模式几种形态。简单工厂模式已经在前面作过介绍。在简单工厂模式中,一个 工厂类处于对产品类实例化调用的中心位置上,它决定那一个产品类应当被实例化, 如 同一个交通警察站在来往的车辆流中,决定放行那一个方向的车辆向那一个方向流动一 样。 而本节要讨论的工厂方法模式是简单工厂模式的进一步抽象化和推广。它比简单工 厂模式聪明的地方在于, 它不再作为一个具体的交通警察的面貌出现,而是以交通警察 局的面貌出现。它把具体的车辆交通交给下面去管理。换言之,工厂方法模式里不再只 由一个工厂类决定那一个产品类应当被实例化,这个决定被交给子类去作。处于工厂方 法模式的中心位置上的类甚至都不去接触那一个产品类应当被实例化这种细节。这种进 一步抽象化的结果,是这种新的模式可以用来处理更加复杂的情形。 为什么需要工厂方法模式 现在,让我们继续考察我们的小花果园。在<简单工厂模式>一节里,我们在后花园里 引进了水果类植物, 构造了简单工厂模式来处理, 使用一个 FruitGardener 类来负责创 立水果类的实例。见下图。 图 3.1. 简单工厂模式。FruitGardener 掌握所有水果类的生杀大权。 在这一节里,我们准备再次引进蔬菜类植物,比如 西红柿 (Tomato) 土豆 (Potato) 西芥兰花 (Broccoli) 蔬菜与花和水果当然有共同点,可又有不同之处。蔬菜需要喷洒(dust)杀虫剂 (pesticide)除虫, 不同的蔬菜需要喷洒不同的杀虫剂,等等。怎么办呢? 那么,再借用一下简单工厂模式不就行了? 再设计一个专管蔬菜类植物的工厂类,比 如 图 3.2. 简单工厂模式。VeggieGardener 掌握所有蔬菜类的生杀大权 这样做一个明显的不足点就是不够一般化和抽象化。在 FruitGardener 和 VeggieGardener类之间明显存在很多共同点, 这些共同点应当抽出来一般化和框架化。 这样一来,如果后花园的主人决定再在园子里引进些树木类植物时, 我们有框架化的处 理方法。本节所要引入的工厂方法模式就符合这样的要求。 简单工厂模式的回顾 有必要首先回顾一下简单工厂模式的定义,以便于比较。 图 3.3. 简单工厂模式的类图定义 从上图可以看出,简单工厂模式涉及到以下的角色 工厂类 (Creator) 担任这个角色的是工厂方法模式的核心,是与应用程序紧密相关的,直接在应用程 序调用下,创立产品实例的那个类。 工厂类只有一个,而且是实的。见下面的位图 产品 (Product) 担任这个角色的类是工厂方法模式所创立的对象的父类,或它们共同拥有的接 口。 实产品 (Concrete Product) 担任这个角色的类是工厂方法模式所创立的任何对象所属的类。 实产品类可以是分布在一维数轴上的分立点 1,2,3,...中的任何一个,见下面的位 图 工厂方法模式的定义 我们给出工厂方法模式的类图定义如下。 图 3.4. 工厂方法模式的类图定义 从上图可以看出,工厂方法模式涉及到以下的角色 抽象工厂接口(Creator) 担任这个角色的是工厂方法模式的核心,它是与应用程序无关的。任何在模式中 创立对象的工厂类必须实现这个接口。 实工厂类 (Conrete Creator) 担任这个角色的是与应用程序紧密相关的,直接在应用程序调用下,创立产品实 例的那样一些类。 实工厂类可以是分布在一维数轴上的分立点 1,2,3,...中的任何一个,见下面的位 图 产品 (Product) 担任这个角色的类是工厂方法模式所创立的对象的父类,或它们共同拥有的接 口。 实产品 (Concrete Product) 担任这个角色的类是工厂方法模式所创立的任何对象所属的类。 实产品类可以是分布在二维平面上的分立点 (1,1), (1,2), (2,3),...中的任何一个, 见下面的位图 由实工厂 1(横数轴上第一点)创立的对象来自实产品类(1,1), (1,2), (1,3),...。由实工厂 2(横数轴 上第二点)创立的对象来自实产品类(2,1), (2,2), (3,3),...。依此类推 工厂方法模式和简单工厂模式在定义上的不同是很明显的。工厂方法模式的核心是 一个抽象工厂类,而不像简单工厂模式, 把核心放在一个实类上。工厂方法模式可以允许 很多实的工厂类从抽象工厂类继承下来, 从而可以在实际上成为多个简单工厂模式的 综合,从而推广了简单工厂模式。 反过来讲,简单工厂模式是由工厂方法模式退化而来。设想如果我们非常确定一个 系统只需要一个实的工厂类, 那么就不妨把抽象工厂类合并到实的工厂类中去。而这样 一来,我们就退化到简单工厂模式了。 与简单工厂模式中的情形一样的是,ConcreteCreator 的 factory() 方法返还的数 据类型是一个接口 PlantIF,而不是哪一个具体的产品类。这种设计使得工厂类创立哪 一个产品类的实例细节完全封装在工厂类内部。 工厂方法模式又叫多形性工厂模式,显然是因为实工厂类都有共同的接口,或者都 有共同的抽象父类。 工厂方法模式在小花果园系统中的实现 好了,现在让我们回到小花果园的系统里,看一看怎样发挥工厂方法模式的威力, 解决需要接连不断向小花果园引进不同类别的植物所带来的问题。 首先,我们需要一个抽象工厂类,比如叫做 Gardener,作为两个实工厂类 FruitGardener 及 VeggieGardener 的父类。 Gardener 的 factory() 方法可以是抽象 的,留给子类去实现,也可以是实的,在父类实现一部分功能,再在子类实现剩余的功 能。我们选择将 factory() 做成抽象的,主要是因为我们的系统是一个示范系统,内容 十分简单,没有要在父类实现的任何功能。 图 3.5. 工厂方法模式在小花果园系统中的实现 抽象工厂类 Gardener 是工厂方法模式的核心,但是它并不掌握水果类或蔬菜类的 生杀大权。相反地,这项权力被交给子类,即 VeggieGardener 及 FruitGardener。 package com.javapatterns.factorymethod; abstract public class Gardener { public abstract PlantIF factory(String which) throws BadFruitException; } 代码清单 3.1. 父类 Gardener。 package com.javapatterns.factorymethod; public class VeggieGardener extends Gardener { public PlantIF factory(String which) throws BadPlantException { if (which.equalsIgnoreCase("tomato")) { return new Tomato(); } else if (which.equalsIgnoreCase("potato")) { return new Potato(); } else if (which.equalsIgnoreCase("broccoli")) { return new Broccoli(); } else { throw new BadPlantException("Bad veggie request"); } } } 代码清单 3.2. 子类 VeggieGardener。 package com.javapatterns.factorymethod; public class FruitGardener extends Gardener { public PlantIF factory(String which) { if (which.equalsIgnoreCase("apple")) { return new Apple(); } else if (which.equalsIgnoreCase("strawberry")) { return new Strawberry(); } else if (which.equalsIgnoreCase("grape")) { return new Grape(); } else { throw new BadPlantException("Bad fruit request"); } } } 代码清单 3.3. 子类 FruitGardener。 package com.javapatterns.factorymethod; public class Broccoli implements VeggieIF, PlantIF { public void grow() { log("Broccoli is growing..."); } public void harvest() { log("Broccoli has been harvested."); } public void plant() { log("Broccoli has been planted."); } private static void log(String msg) { System.out.println(msg); } public void pesticideDust(){ } } 代码清单 3.4. 蔬菜类 Broccoli。其它的蔬菜类与 Broccoli 相似,因此不再赘述。 package com.javapatterns.factorymethod; public class Apple implements FruitIF, PlantIF { public void grow() { log("Apple is growing..."); } public void harvest() { log("Apple has been harvested."); } public void plant() { log("Apple has been planted."); } private static void log(String msg) { System.out.println(msg); } public int getTreeAge(){ return treeAge; } public void setTreeAge(int treeAge){ this.treeAge = treeAge; } private int treeAge; } 代码清单 3.5. 水果类 Apple。与<简单工厂模式>一节里的 Apple 类相比,唯一的区别 就是多实现了一个接口 PlantIF。其它的水果类与 Apple 相似,因此不再赘述。 package com.javapatterns.factorymethod; public class BadPlantException extends Exception { public BadPlantException(String msg) { super(msg); } } 代码清单 3.6. 例外类 BadPlantException。 工厂方法模式应该在什么情况下使用 既然工厂方法模式与简单工厂模式的区别很是微妙,那么应该在什么情况下使用工 厂方法模式,又应该在什么情况下使用简单工厂模式呢? 一般来说,如果你的系统不能事先确定那一个产品类在哪一个时刻被实例化,从而 需要将实例化的细节局域化,并封装起来以分割实例化及使用实例的责任时,你就需要 考虑使用某一种形式的工厂模式。 在我们的小花果园系统里,我们必须假设水果的种类随时都有可能变化。我们必须 能够在引入新的水果品种时,能够很少改动程序,就可以适应变化以后的情况。因此, 我们显然需要某一种形式的工厂模式。 如果在发现系统只用一个产品类等级(hierarchy)就可以描述所有已有的产品类,以 及可预见的未来可能引进的产品类时,简单工厂模式是很好的解决方案。因为一个单一 产品类等级只需要一个单一的实的工厂类。 然而,当发现系统只用一个产品类等级不足以描述所有的产品类,包括以后可能要 添加的新的产品类时,就应当考虑采用工厂方法模式。由于工厂方法模式可以容许多个 实的工厂类,以每一个工厂类负责每一个产品类等级,因此这种模式可以容纳所有的产 品等级。 在我们的小花果园系统里,不只有水果种类的植物,而且有蔬菜种类的植物。换言 之,存在不止一个产品类等级。而且产品类等级的数目也随时都有可能变化。因此,简 单工厂模式不能满足需要,为解决向题,我们显然需要工厂方法模式。 关于模式的实现 在实现工厂方法模式时,有下面一些值得讨论的地方。 第一丶在图四的类图定义中,可以对抽象工厂(Creator) 做一些变通。变通的种类 有 抽象工厂(Creator) 不是接口而是抽象类。一般而言,抽象类不提供一个缺省的工 厂方法。 这样可以有效地解决怎样实例化事先不能预知的类的问题。 抽象工厂(Creator) 本身是一个实类,并提供一个缺省的工厂方法。 这样当最初的 设计者所预见的实例化不能满足需要时,后来的设计人员就可以用实工厂类的 factory() 方法来置换(Override))父类中 factory()方法。 第二丶在经典的工厂方法模式中,factory()方法是没有参量的。在本文举例时加入 了参量,这实际上也是一种变通。 第三丶在给相关的类和方法取名字时,应当注意让别人一看即知你是在使用工厂模 式。 COM 技术架构中的工厂方法模式 在微软(Microsoft)所提倡的 COM(Component Object Model)技术架构中, 工厂方 法模式起着关键的作用。 在 COM 架框里,Creator 接口的角色是由一个叫作 IClassFactory 的 COM 接口来担 任的。而实类 ConcreteCreator 的角色是由实现 IClassFactory 接口的类 CFactory(见下 图)来担任的。一般而言,对象的创立可能要求分配系统资源,要求在不同的对象之间进行 协调等等。因为 IClassFactory 的引进,所有这些在对象的创立过程中出现的细节问题, 都可以封装在一个实现 IClassFactory 接口的实的工厂类里面。这样一来, 一个 COM 架 构的支持系统只需要创立这个工厂类 CFactory 的实例就可以了。 图 3.6. 微软(Microsoft)的 COM(Component Object Model)技术架构是怎样工作的。 在上面的序列活动(Sequence Activity) 图中, 用户端调用 COM 的库函数 CoCreateInstance。 CoCreateInstance 在 COM 架框中以 CoGetClassObject 实现。 CoCreateInstance 会在视窗系统的 Registry 里搜寻所要的部件(在我们的例子中即 CEmployee)。如果找到了这个部件,就会加载支持此部件的 DLL。当此 DLL 加载成功后, CoGetClassObject 就会调用 DllGetClassObject。后者使用 new 操作符将工厂类 CFactory 实例化。 下面,DllGetClassObject 会向工厂类 CFactory 搜询 IClassFactory 接口,返还给 CoCreateInstance 。 CoCreateInstance 接下来利用 IClassFactory 接口调用 CreateInstance 函数。此时,IClassFactory::CreateInstance 调用 new 操作符来创立所 要的部件(CEmployee)。此外,它搜询 IEmployee 接口。在拿到接口的指针后, CoCreateInstance 释放掉工厂类并把接口的指针返还给客户端。 客户端现在就可以利用这个接口调用此部件中的方法了。 EJB 技术架构中的工厂方法模式 升阳(Sun Microsystem)倡导的 EJB(Enterprise Java Beans)技术架构是一套为爪 哇语言设计的, 用来开发企业规模应用程序的组件模型。我们来举例看一看 EJB 架构是 怎样利用工厂方法模式的。请考察下面的序列活动图。 图 3.7. 在升阳所提倡的 EJB 技术架构中, 工厂方法模式也起着关键的作用 在上面的图中,用户端创立一个新的 Context 对象,以便利用 JNDI 伺服器寻找 EJBObject。在得到这个 Context 对象后,就可以使用 JNDI 名, 比如"Employee", 来 拿到 EJB 类 Employee 的 Home 接口。使用 Employee 的 Home 接口,客户端可 以创立 EJB 对象,比如 EJB 类 Employee 的实例 emp, 然后调用 Employee 的各 个方法。 // 取到 JNDI naming context Context ctx = new InitialContext (); // 利用 ctx 索取 EJB Home 接口 EmployeeHome home = (EmployeeHome)ctx.lookup("Employee"); // 利用 Home 接口创立一个 Session Bean 对象 // 这里使用的是标准的工厂方法模式 Employee emp = home.create (1001, "John", "Smith"); // 调用方法 emp.setTel ("212-657-7879"); 代码清单 3.7. EJB 架构中,Home 接口提供工厂方法以便用户端可以动态地创立 EJB 类 Employee 的实例。 JMS 技术架构中的工厂方法模式 JMS 定义了一套标准的 API,让爪哇语言程序能通过支持 JMS 标准的 MOM(Message Oriented Middleware 面向消息的中间伺服器)来创立和交换消息 (message)。我们来举例看一看 JMS(Java Messaging Service)技术架构是怎样使用工 厂方法模式的。 图 3.8. 在 JMS 技术架构中, 工厂方法模式无处不在 在上面的序列图中,用户端创立一个新的 Context 对象,以便利用 JNDI 伺服器寻 找 Topic 和 ConnectionFactory 对象。在得到这个 ConnectionFactory 对象后, 就可 以利用 Connection 创立 Session 的实例。有了 Session 的实例后,就可以利用 Session 创立 TopicPublisher 的实例,并利用 Session 创立消息实例。 Properties prop = new Properties(); prop.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.fscontext.RefFSContextFactory"); prop.put(Context.PROVIDER_URL, "file:C:\temp"); // 取到 JNDI context Context ctx = new InitialContext(prop); // 利用 ctx 索取工厂类的实例 Topic topic = (Topic) ctx.lookup("myTopic"); TopicConnectionFactory tcf = (TopicConnectionFactory) ctx.lookup("myTCF"); // 利用工厂类创立 Connection,这是典型的工厂模式 TopicConnection tCon = tcf.createTopicConnectoin(); // 利用 Connection 创立 Session 的实例,又是工厂模式 TopicSession tSess = tCon.createTopicSession(false, Session.AUTO_ACKNOWLEDGE); // 利用 Session 创立 Producer 的实例,又是工厂模式 TopicPublisher publisher = tSess.createPublisher(topic); // 利用 Session 创立消息实例,又是工厂模式 TextMesage msg = tSess.createTextMessage("Hello from Jeff"); //发送消息 publisher.publish(msg); 代码清单 3.8. JMS 架构中,工厂模式被用于创立 Connection, Session, Producer 的实 例。 问答题 第 1 题、在这一节和上一节的类图中,我注意到 Apple 类的类图与 Strawberry 类 的类图有一点点不同。在 Apple 类的类图左上角有一个夹子样的标识。请问这个标识代 表什么意思。 第 2 题、在这一节的类图 4 中,我注意到 ConcreteProduct 类只出现一次,但实 现 Product 接口的类实际上可以有很多。这是否可以用在联接 Product 和 ConcreteProduct 之间的线旁注上 1,2,... 表示呢? 记得我在 UML 图中曾见过这种记 号。 第 3 题、请问在本节的小花果园系统的源代码清单 4 里,Broccoli 类实现两个接口, VeggieIF 和 PlantIF。只有 PlantIF 才与工厂模式有关。为什么不把 VeggieIF 接口 合并到 PlantIF 接口中去? 第 4 题、请问在工厂方法模式中,产品(Product) 何时应是抽象类,何时应是接口? 第 5 题、请问在工厂方法 (factory())中,为什么要使用 if 语句作过程性判断来决 定创立哪一个产品类,而不使用多形性原则 (Polymorphsm) 来创立产品类? 问答题答案 第 1 题、Apple 类有性质(property),而 Strawberry 类没有性质。 一个类的成员变量叫做属性(attribute)。性质与属性的区别在于性质是带着一套取 值丶赋值方法的属性。一个类有了属性,其类图左上角就会有一只夹子。有些人认为, 一个爪哇类有了属性才能被称做爪哇豆(Java Bean)。这只夹子就表示这个类是一只豆。 一个企业爪哇豆,或 EJB (Enterprise JavaBean) 的类图左上角也会有一只夹子,夹子 上面有一个 E 字以示与普通的爪哇豆的不同(请见下图)。 第 2 题、不能。在图 4 中联接 Product 和 ConcreteProduct 之间的线有两条,一 条表示两者之间的推广关系 (即有向上箭头的),另一条表示两者之间的关联关系(即有 向下箭头的)。在推广关系线旁写数字没有意义。在关联关系线旁写数字是有意义的,类 旁的数字可以表明类的实例的数目。 原来的问题是关于类的数目而不是类的实例的数目,因此是错的。 没有任何必要用数字标明这一点,而且 UML 也不提供这种标记。 第 3 题、在面向对象的编程,特别是爪哇语言的编程中,接口常常用来标志一种身 份(identity)。 VeggieIF 和 PlantIF 接口代表两种不同的身份。VeggieIF 表明 Broccoli 类属于蔬菜类等级, PlantIF 接口表明 Broccoli 类属于工厂的产品类。 因此,虽然把两个接口合并起来可能在功能上是行得通的,在原则上是不应鼓励 这样做的。 第 4 题、在工厂方法模式中,产品(Product)可以永远是抽象类。但在一些情形下可 仪简化为接口。 如果所实产品类( Concrete Product) 之间有共同的逻辑,这部分公有的代码就应 当转移到产品 (Product) 中去,这样产品就必须是抽象类而不可能是接口。 反过来,如果所实产品类( Concrete Product) 之间没有任何共同的逻辑,那么产 品(Product)就没有任何逻辑代码,它就应当被作为接口,而不是抽象类。但这不是必须 的,仅是建议而已。 第 5 题、多形性原则 (Polymorphism) 是在对象被创立之后才存在的,因此不能使 用多形性来创立对象。factory() 方法必然是非常过程性 (procedural)的。 4、爪哇语言简单工厂创立性模式介绍 研究和使用创立性模式的必要性 面向对象的设计的目的之一,就是把责任进行划分,以分派给不同的对象。我们推荐 这种划分责任的作法, 是因为它和封装(Encapsulation)和分派(Delegation)的精神是相 符合的。创立性模式把对象的创立过程封装起来,使得创立实例的责任与使用实例的责 任分割开, 并由专门的模块分管实例的创立,而系统在宏观上不再依赖于对象创立过程 的细节。 所有面向对象的语言都有固定的创立对象的办法。爪哇语的办法就是使用 new 操 作符。比如 StringBuffer s = new StringBuffer(1000); 就创立了一个对象 s,其类型是 StringBuffer。使 用 new 操作符的短处是事先必须明 确知道要实例化的类是什么, 而且实例化的责任往往与使用实例的责任不加区分。使用 创立性模式将类实例化,首先不必事先知道每次是要实例化哪一个类, 其次把实例化的 责任与使用实例的责任分割开来,可以弥补直接使用 new 操作符的短处。 而工厂模式就是专门负责将大量有共同接口的类实例化,而且不必事先知道每次是 要实例化哪一个类的模式。 工厂模式有几种形态 工厂模式有以下几种形态: 简单工厂(Simple Factory)模式 工厂方法(Factory Method)模式,又称多形性工厂(Polymorphic Factory)模式 抽象工厂(Abstract Factory)模式,又称工具箱(Kit 或 Toolkit)模式 介绍简单工厂模式 比如说,你有一个描述你的后花园的系统,在你的后花园里有各种的花,但还没有水 果。你现在要往你的系统里引进一些新的类,用来描述下列的水果: 葡萄 Grapes 草莓 Strawberry 萍果 Apple 花和水果最大的不同,就是水果最终是可以采摘食用的。那么,很自然的作法就是建 立一个各种水果都适用的接口,这样一来这些水果类作为相似的数据类型就可以和你的 系统的其余部分,如各种的花有所不同,易于区分。 图 4.1. Grape, Strawberry 和 Apple 是拥有共同接口 FruitIF 的类。 package com.javapatterns.simplefactory; public interface FruitIF { void grow(); void harvest(); void plant(); String color = null; String name = null; } 代码清单 4.1. 接口 FruitIF 的源代码。这个接口确定了水果类必备的方法:种植 plant(), 生长 grow(), 以及收获 harvest()。 package com.javapatterns.simplefactory; public class Apple implements FruitIF { public void grow() { log("Apple is growing..."); } public void harvest() { log("Apple has been harvested."); } public void plant() { log("Apple has been planted."); } public static void log(String msg) { System.out.println(msg); } public int getTreeAge(){ return treeAge; } public void setTreeAge(int treeAge){ this.treeAge = treeAge; } private int treeAge; } 代码清单 4.2. 类 Apple 的源代码。萍果是多年生木本植物 ,因此具备树龄 treeAge 性质。 package com.javapatterns.simplefactory; public class Grape implements FruitIF { public void grow() { log("Grape is growing..."); } public void harvest() { log("Grape has been harvested."); } public void plant() { log("Grape has been planted."); } public static void log(String msg) { System.out.println(msg); } public boolean getSeedful() { return seedful; } public void setSeedful(boolean seedful) { this.seedful = seedful; } private boolean seedful; } 代码清单 4.3. 类 Grape 的源代码。葡萄分为有籽与无籽两种 ,因此具有 seedful 性质。 package com.javapatterns.simplefactory; public class Strawberry implements FruitIF { public void grow() { log("Strawberry is growing..."); } public void harvest() { log("Strawberry has been harvested."); } public void plant() { log("Strawberry has been planted."); } public static void log(String msg) { System.out.println(msg); } } 代码清单 4.4. 类 Strawberry 的源代码。 你作为小花果园的主人兼园丁,也是系统的一部分,自然要由一个合适的类来代表, 这个类就是 FruitGardener 类。这个类的结构请见下面的 UML 类图。 图 4.2 FruitGardener 类图。 FruitGardener 类会根据要求,创立出不同的水果类,比如萍果 Apple,葡萄 Grape 或 草莓 Strawberry 的实例。而如果接到不合法的要求,FruitGardener 类会给出例外 BadFruitException。 图 4.3. BadFruitException 类图。 package com.javapatterns.simplefactory; public class FruitGardener { public FruitIF factory(String which) throws BadFruitException { if (which.equalsIgnoreCase("apple")) { return new Apple(); } else if (which.equalsIgnoreCase("strawberry")) { return new Strawberry(); } else if (which.equalsIgnoreCase("grape")) { return new Grape(); } else { throw new BadFruitException("Bad fruit request"); } } } 代码清单 4.5. FruitGardener 类的源代码。 package com.javapatterns.simplefactory; public class BadFruitException extends Exception { public BadFruitException(String msg) { super(msg); } } 代码清单 4.6. BadFruitException 类的源代码。 在使用时,只须呼叫 FruitGardener 的 factory()方法即可 try { FruitGardener gardener = new FruitGardener(); gardener.factory("grape"); gardener.factory("apple"); gardener.factory("strawberry"); ... } catch(BadFruitException e) { ... } 就这样你的小果园一定会有百果丰收啦! 简单工厂模式的定义 总而言之,简单工厂模式就是由一个工厂类根据参数来决定创立出那一种产品类的 实例。下面的 UML 类图就精确定义了简单工厂模式的结构。 图 4.4. 简单工厂模式定义的类图。 public class Creator { public Product factory() { return new ConcreteProduct(); } } public interface Product { } public class ConcreteProduct implements Product { public ConcreteProduct(){} } 代码清单 4.7. 简单工厂模式框架的源代码。 简单工厂模式实际上就是我们要在后面介绍的,工厂方法模式的一个简化了的情 形。在读者熟悉了本节所介绍的简单工厂模式后,就不难掌握工厂方法模式了。 问答题 在本节开始时不是说,工厂模式就是在不使用 new 操作符的情况下,将......类实例化 的吗, 可为什么在具体实现时,仍然使用了 new 操作符呢? 在本节的小果园系统里有三种水果类,可为什么在图4.3.(简单工厂模式定义的类图) 中产品(Product)类只有一种呢? 请使用简单工厂模式设计一个创立不同几何形状,如圆形,方形和三角形实例的描图 员(Art Tracer)系统。每个几何图形都要有画出 draw()和擦去 erase()两个方法。当描图 员接到指令,要求创立不支持的几何图形时,要提出 BadShapeException 例外。 请简单举例说明描图员系统怎样使用。 在简单工厂模式的定义(见图 4.4)中和花果园例子中,factory()方法都是属于实例的, 而非静态的或是类的方法。factory()方法可不可以是静态的方法呢? 问答题答案 对整个系统而言,工厂模式把具体使用 new 操作符的细节包装和隐藏起来。当然只 要程序是用爪哇语言写的, 爪哇语言的特征在细节里一定会出现的。 图 4.3.(简单工厂模式定义的类图),是精减后的框架性类图,用于给出这一模式的准 确而精练的定义。产品(Product)类到底会有几种,则要对每个系统作具体分析。 这里给出问题的完整答案。描图员(Art Tracer)系统的 UML 如下 系统的源代码如下 package com.javapatterns.simplefactory.exercise; public class ArtTracer { public Shape factory(String which) throws BadShapeException { if (which.equalsIgnoreCase("circle")) { return new Circle(); } else if (which.equalsIgnoreCase("square")) { return new Square(); } else if (which.equalsIgnoreCase("triangle")) { return new Triangle(); } else { throw new BadShapeException(which); } } } 代码清单 4.8. ArtTracer 类的源代码。 package com.javapatterns.simplefactory.exercise; public interface Shape { void draw(); void erase(); } 代码清单 4.9. Shape 接口的源代码。 package com.javapatterns.simplefactory.exercise; public class Square implements Shape { public void draw() { System.out.println("Square.draw()"); } public void erase() { System.out.println("Square.erase()"); } } 代码清单 4.10. Square 类的源代码。 package com.javapatterns.simplefactory.exercise; public class Circle implements Shape { public void draw() { System.out.println("Circle.draw()"); } public void erase() { System.out.println("Circle.erase()"); } } 代码清单 4.11. Circle 类的源代码。 package com.javapatterns.simplefactory.exercise; public class Triangle implements Shape { public void draw() { System.out.println("Triangle.draw()"); } public void erase() { System.out.println("Triangle.erase()"); } } 代码清单 4.12. Triangle 类的源代码。 package com.javapatterns.simplefactory.exercise; public class BadShapeException extends Exception { public BadShapeException(String msg) { super(msg); } } 代码清单 4.13. BadShapeException 类的源代码。 描图员(Art Tracer)系统使用方法如下 try { ArtTracer art = new ArtTracer(); art.factory("circle"); art.factory("square"); art.factory("triangle"); art.factory("diamond"); } catch(BadShapeException e) { ... } 注意对 ArtTracer 类提出菱形(diamond)请求时,会收到 BadShapeException 例外。 显然 factory()可以是静态的或是类的方法。本文这样介绍简单工厂模式,是为了能方 便与后面介绍的工厂方法模式作一比较。 5、爪哇语言单态创立性模式介绍 什么是模式 一个围棋下得好的人知道,好的"形"对于围棋非常重要。形是棋子在棋盘上的几何形 状的抽象化。 形就是模式(Pattern),也是人脑把握和认识外界的关键。而人脑对处理模 式的能力也非常高超, 人可以在几百张面孔中一下子辨认出所熟悉的脸来,就是一个例 子。 简而言之,在我们处理大量问题时,在很多不同的问题中重复出现的一种性质,它使 得我们可以使用一种方法来描述问题实质并用本质上相同,但细节永不会重复的方法去 解决,这种性质就叫模式。模式化过程是把问 题抽象化,在忽略掉不重要的细节后,发现 问题的一般性本值,并找到普遍使用的方法去解决的过程。 发现模式是与研究模式同时发生的,发现一个新的模式很不容易。一个好的模式必 须满足以下几点: 1、它可以解决问题。模式不能仅仅反映问题,而必须对问题提出解决方案。 2、它所提出解决方案是正确的,而且不是很明显的。 3、它必须是涉及软件系统深层的结构的东西,不能仅是对已有的模块的描述。 4、它必须满足人的审美,简洁美观。 换言之,一个美妙的东西不一定就是模式,但是一个模式必须是一个美妙的东西。 软件工程学的各个方面,诸如开发组织,软件处理,项目配置管理,等等,都可以 看到模式的影子。但至今 得到了最好的研究的是设计模式和组织模式。在软件编程中 使用模式化方法, 是在编程对象化之后才开始得到重视的。软件编程中模式化方法的研 究,也是在九十年代才开始。 在面向对象的编程中使用模式化方法研究的开创性著作,是 Design Patterns - Elements of Reusable Object-Oriented Software, E.Gamma, R. Helm, R. Johnson, and J. Vlissides,1995, Addison-Wesley. 这四位作者通常被称为四人帮(Gang of Four, 或 GoF)。(在这个词出现以后,很 多西方商业炒作利用这个 路人皆知的词赚钱,有一个八十年代的美国四人乐队以此为 队名。在英国政界更曾有好几个小帮派被称为四人帮。 在这里大家使用这个词称呼这 四个著名作者,带有戏虐成分。) 由于爪哇语言的特点,使得模式在爪哇语言的实现有自己的特点。 爪哇语言是现今 最普及的纯粹 OOP 的编程语言,使用爪哇语言编程的程序师平均的素质也相对比较高。 这些程序师往往不满足于只是实现程序功能要求,他们常常想要在代码结构,编程风格, 乃至解决问题的 思考方式上不断进取和自我完善。模式,就是在大量的实践中总结和理 论化之后的优选的代码结构,编程风格, 及解决问题的思考方式。对模式的了解和掌握, 是爪哇程序师提高自身素质的一个很好的方向。 作者在学习和工作中把自己的体会 总结下来,藉以与读者交流提高。 作者在后面会使用简单的 UML(统一建模语言,Unified Modelling Languge)。由于市 场上有很多介绍 UML 的书,而作者在后面使用到的 UML 又极为简单,因此只在此作一极 为简单的介绍,目的是让没有接触过 UML 的 读者能看懂后面的讲述。 图 5.1. UML 的类图举例 在图5.1的类图中可以看出,表示类的框分成四层:类名,变量清单,函数清单和属性清 单。 变量名如是正体字,表明类是实的(Concrete,即可以实例化的类),变量名如是斜体 字,表明类是抽象的。 显然,我们在图中给出了一个实的类。 在图 5.1 的类 ClassUML 中,一个变量或函数(方法)左面如果有一个加(+)号,表示它 是公开的, 左面如果有一个减(-)号,表示它是私有的,左面如果有一个井(#)号,表示它是 保护的。 一个属性即由一个内部变量,一个赋值函数(mutator)和一个取值函数(accessor)组 成的结构。 在类的方框的右上角里,通常还分两行写出类的父类和所实现的接口。在后面读者 会看到例子。 在类与类之间,会有线条指明它们之间的关系。在类与类之间可以发生推广(与继承 相反),依赖,累积和关联 等关系。在后面读者看到例子时作者会加以解释。 package com.javapatterns.singleton.demos; public class ClassUML { public ClassUML() {} private void aPrivateFunction() {} public void aPublicMethod() {} public int getAProperty(){ return aPrivateVar; } public void setAProperty(int aPrivateVar) { this.aPrivateVar = aPrivateVar; } static public void aStaticMethod() {} protected void aProtectedMethod() {} private int aPrivateVar; public int aPublicVar; protected int aProtectedVar; } 代码清单 5.1. ClassUML 类的源代码。 什么是创立性模式 创立性模式(Creational Patterns)是类在实例化时使用的模式。当一些系统在创立对 象时,需要动态地决定 怎样创立对象,创立哪些对象。创立性模式告诉我们怎样构造和包 装这些动态的决定。创立性模式通常包括 以下的模式 1、工厂函数模式 2、抽象工厂类模式 3、建设者模式 4、原始模型模式 5、单态模式 单态模式 一个单态类只可有一个实例。这样的类常用来进行资源管理。 需要管理的资源包括软件外部资源,譬如,每台 计算机可以有若干个打印机,但 只能有一个打印处理器软件。每台计算机可以有若干传真卡,但是 只应该有一个传真 软件管理传真。每台计算机可以有若干通讯端口,你的软件应当集中管理这些 通讯端 口,以避免同时一个通讯端口被两个请求同时调用。 需要管理的资源包括软件内部资源,譬如,大多数的软件都有一个(甚至多个)属性 (properties)文件 存放系统配置。这样的系统应当有一个对象来管理一个属性文件。 很多软件都有数据库,一般而言, 整个软件应当使用一个联接通道,而不是任意在需 要时就新打开一个联接通道。 需要管理的软件内部资源也包括譬如负责纪录网站来访人数的部件,记录软件系统 内部事件、出错 信息的部件,或是进行系统表现监查的的部件,等等。这些部件都必须 集中管理,不可政出多头。 单态类的特性 综合而言, 1、单态类只可有一个实例。 2、它必须自己创立自己这唯一的一个实例。 3、它必须给所有其它的类提供自己这一实例。 最后,单态类在理论和实践上都并非限定只能有"一个"实例,而是很容易推广到任意 有限个实例的情况。 单态模式的几种实现 由于爪哇语言的特点,使得单态模式在爪哇语言的实现有自己的特点。这些特点主 要表现在怎样实例化上。 饿汉式单态类 饿汉式单态类是在爪哇语言里实现得最为简便的单态类。 图 5.2.饿汉式单态类的 UML 类图 图中的关系线表明,此类自已将自己实例化。 package com.javapatterns.singleton.demos; public class EagerSingleton { private EagerSingleton() { } public static EagerSingleton getInstance() { return m_instance; } private static final EagerSingleton m_instance = new EagerSingleton(); } 代码清单 5.2.饿汉式单态类。 值得指出的是,由于构造子是私有的,因此此类不能被继承。 懒汉式单态类 懒汉式单态类在第一次被引用时将自己实例化。如果加载器是静态的,那么在懒汉 式单态类被加载时不 会将自己实例化。 package com.javapatterns.singleton.demos; public class LazySingleton { private LazySingleton() { } public static LazySingleton getInstance() { if (m_instance == null) { file://More than one threads might be here!!! synchronized(LazySingleton.class) { if (m_instance == null) { m_instance = new LazySingleton(); } } } return m_instance; } private static LazySingleton m_instance = null; } 代码清单 5.3.懒汉式单态类。 图 5.3.懒汉式单态类 图中的关系线表明,此类自已将自己实例化。 读者可能会注意到,在上面给出 懒汉式单态类实现里,使用了在多线程编程中常要 使用的,著名的双重检查原则。对双重检查原则 和多线程编程要点不十分熟悉的读者, 可以看看后面给出的问答题。 同样,由于构造子是私有的,因此此类不能被继承。 饿汉式单态类在自己被加载时就将自己实例化。既便加载器是静态的,在饿汉式单 态类被加载时仍 会将自己实例化。单从资源利用效率角度来讲,这是比懒汉式单态类稍 差些。从速度和反应时间角度来 讲,则比懒汉式单态类稍好些。然而,懒汉式单态类在实 例化时必须处理好在多个线程同时首次引 用此类时,实例化函数内部关键段的访问限 制问题。特别是当单态类作为资源控器,在实例化时必然涉及 资源初始化,而资源初始化 很有可能耗费时间。这意味着出现多线程同时首次引 用此类的几率变得较大。 饿汉式单态类可以在爪哇语言内实现,但不易在 C++内实现,因为静态初始化在 C++ 里没有固定的顺序, 因而静态的 m_instance 变量的初始化与类的加载顺序没有保证,可 能会出问题。这就是为什么 GoF 在提出 单态类的概念时,举的例子是懒汉式的。他们的 书影响之大,以致爪哇语言中单态类的例子也大多是 懒汉式的。实际上,作者认为饿汉 式单态类更符合爪哇语言本身的特点。 登记式单态类 登记式单态类是 GoF 为了克服饿汉式单态类及懒汉式式单态类均不可继承的缺点 而设计的。 作者把他们的例子翻译为爪哇语言,并将它自己实例化的方式从懒汉式改为 饿汉式。只是它的 子类实例化的方式只能是懒汉式的,这是无法改变的。 图 5.4. 登记式单态类的一个例子 图中的关系线表明,此类自已将自己实例化。 package com.javapatterns.singleton.demos; import java.util.HashMap; public class RegSingleton { protected RegSingleton() {} static public RegSingleton getInstance(String name) { if (name == null) { name = "com.javapatterns.singleton.demos.RegSingleton"; } if (m_registry.get(name) == null) { try { m_registry.put( name, Class.forName(name).newInstance() ) ; } catch(Exception e) { System.out.println("Error happened."); } } return (RegSingleton) (m_registry.get(name) ); } static private HashMap m_registry = new HashMap(); static { RegSingleton x = new RegSingleton(); m_registry.put( x.getClass().getName() , x); } public String about() { return "Hello, I am RegSingleton."; } } 代码清单 5.4. 登记式单态类。(注意为简单起见,这里没有考虑多线程访问限制的问题, 读者可自行加入一个有双重 检查的访问限制) 它的子类 图 5.5. 登记式单态类子类的一个例子。 图中的关系线表明,此类是由父类将自己实例化的。 package com.javapatterns.singleton.demos; import java.util.HashMap; public class RegSingletonChild extends RegSingleton { public RegSingletonChild() {} static public RegSingletonChild getInstance() { return (RegSingletonChild) RegSingleton.getInstance( "com.javapatterns.singleton.demos.RegSingletonChild" ); } public String about() { return "Hello, I am RegSingletonChild."; } } 代码清单 5.5. 登记式单态类的子类。 在 GoF 原始的例子中,并没有 getInstance()方法,这样得到子类必须调用文类的 getInstance(String name) 方法,并传入子类的名字,很不方便。 作者在登记式单态类 子类的例子里,加入了 getInstance()方法,这样做的好处是 RegSingletonChild 可以通过 这个方法,返还自已的实例,而这样做的缺点是,由于数据类型不同,无法在 RegSingleton 提供 这样一个方法。 由于子类必须充许父类以构造子调用产生实例,因此它的构造子必须是公开的。这 样一来,就等于允许了 以这样方式产生实例而不在父类的登记中。这是登记式单态类 的一个缺点。 GoF 曾指出,由于父类的实例必须存在才可能有子类的实例,这在有些情况下是一 个浪费。 这是登记式单态类的另一个缺点。 爪哇语言里的垃圾回收 爪哇语言里垃圾回收使得单态类的使用变得有点复杂。原因就在于 JDK1.1 版里加 进去的类的自动清除。 这种类的垃圾回收会清除掉类本身,而不仅仅是对象!事实上 JDK1.1 甚至可以清除掉一些系统类! 在 JDK1.0.x 版本里,类的自动清除尚未加入。 在 JDK1.2 及以后的版本里,升阳公司又收紧了类的垃圾回收规则,它规定,所有通过 局部的和系统的 类加载器加载的类,永不被回收。并且,通过其它类加载器加载的类,只 有在加载器自己被回收后才可被回收。 在 1.1 版 JDK 里使用单态类的读者,如果不了解这一版爪哇语言的特点,很有可能会 遇到类消失掉的奇特问题。 为了使你的单态类能在所有版本的爪哇环境里使用,作者特 别提供一个"看守"类程序,它能保证你的单态类, 甚至其它任何对象,一旦交给"看守"对象, 即不会莫名其妙地被垃圾回收器回收,直到你把它从"看守" 那里把它释放出来。 图 5.6. "看守"类的一个例子 package com.javapatterns.singleton.demos; import java.util.Vector; /** * This class keeps your objects from garbage collected */ public class ObjectKeeper extends Thread { private ObjectKeeper() { new Thread(this).start(); } public void run() { try { join(); } catch (InterruptedException e) {} } /** * Any object passed here will be kept until you call discardObject() */ public static void keepObject(Object myObject) { System.out.println(" Total number of kept objects: " + m_keptObjects.size()); m_keptObjects.add(myObject); System.out.println(" Total number of kept objects: " + m_keptObjects.size()); } /** * This method will remove the protect of the object you pass in and make it * available for Garbage Collector to collect. */ public static void discardObject(Object myObject) { System.out.println(" Total number of kept objects: " + m_keptObjects.size()); m_keptObjects.remove(myObject); System.out.println(" Total number of kept objects: " + m_keptObjects.size()); } private static ObjectKeeper m_keeper = new ObjectKeeper(); private static Vector m_keptObjects = new Vector(); } 代码清单 5.6. 看守类的一个实现。 看守类应当自我实例化,而且在每个系统里只需一个实例。这就意味着看守类本身 就应当是单态类。当然,类 消失的事情绝不可以发生在它自己身上。作者提供的例子 刚好满足所有的要求。 一个实用的例子 这里作者给出一个读取属性(properties)文件的单态类,作为单态类的一个实用的例 子。 属性文件如同老式的视窗编程时的.ini 文件,属于系统的“资源“,而读取属性文件 即为资源管理, 显然应当由一个单态类负责。 图 5.7. 这个例子的 UML 显然,在大多数的系统中都会涉及属性文件的读取问题,因而这个例子非常有实用 价值。 在这个例子里,作者假定需要读取的属性文件就在当前目录中,且名为 singleton.properties。 在这个文件中有如下的一些属性项: node1.item1=How node1.item2=are node2.item1=you node2.item2=doing node3.item1=? 代码清单 5.7. 属性文件内容 本例子的源代码如下: package com.javapatterns.singleton.demos; import java.util.Properties; import java.io.FileInputStream; import java.io.File; public class ConfigManager { /** * 私有的构造子, 用以保证实例化的唯一性 */ private ConfigManager() { m_file = new File(PFILE); m_lastModifiedTime = m_file.lastModified(); if(m_lastModifiedTime == 0) { System.err.println(PFILE + " file does not exist!"); } m_props = new Properties(); try { m_props.load(new FileInputStream(PFILE)); } catch(Exception e) { e.printStackTrace(); } } /** * * @return 返还 ConfigManager 类的单一实例 */ synchronized public static ConfigManager getInstance() { return m_instance; } /** * 读取一特定的属性项 * * @param name 属性项的项名 * @param defaultVal 属性项的缺省值 * @return 属性项的值(如此项存在), 缺省值(如此项不存在) */ final public Object getConfigItem(String name, Object defaultVal) { long newTime = m_file.lastModified(); // 检查属性文件是否被其它程序(多数情况是程序员手动)修改过。 // 如果是,重新读取此文件。 if(newTime == 0) { // 属性文件不存在 if(m_lastModifiedTime == 0) { System.err.println(PFILE + " file does not exist!"); } else { System.err.println(PFILE + " file was deleted!!"); } return defaultVal; } else if(newTime > m_lastModifiedTime) { m_props.clear(); // Get rid of the old properties try { m_props.load(new FileInputStream(PFILE)); } catch(Exception e) { e.printStackTrace(); } } m_lastModifiedTime = newTime; Object val = m_props.getProperty(name); if( val == null ) { return defaultVal; } else { return val; } } /** * 属性文件全名 */ private static final String PFILE = System.getProperty("user.dir") + "/Singleton.properties"; /** * 对应于属性文件的文件对象变量 */ private File m_file = null; /** * 属性文件的最后修改日期 */ private long m_lastModifiedTime = 0; /** * 属性文件所对应的属性对象变量 */ private Properties m_props = null; /** * 本类可能存在的唯一的一个实例 */ private static ConfigManager m_instance = new ConfigManager(); } 代码清单 5.8. ConfigMan 的源代码。 显然,作者是用饿汉型实现方法,从而避免了处理多线程访问可能引起的麻烦。在 下面的源代码里,作者演示了怎样利用看守类来"看守"和"释放"ConfigMan 类,以及怎 样调用 ConfigMan 来读取属性文件。 ObjectKeeper.keepObject(ConfigManager.getInstance()); BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); System.out.println("Type quit to quit"); do { System.out.print("Property item to read: "); String line = reader.readLine(); if(line.equals("quit")) { break; } System.out.println(ConfigManager.getInstance().getConfigItem (line, "Not found.")); } while(true); ObjectKeeper.discardObject(ConfigManager.getInstance()); 代码清单 5.9. 怎样调用 ConfigMan 类以读取属性文件,及调用看守类来看守和释放 ConfigMan 类 下面的图显示出上面代码运行时的情况。 图 5.8. 代码运行时的情况 问答题 1、为什么不使用一个静态的"全程"变量,而要建一个类?一个静态的变量当然只 能有一个值, 从而自然而然不就是"单态"的吗? 2、在 LazySingleton 的例子中,如果把限制访问的关键词从 LazySingleton.class 移 到 getInstance() 方法的声明语句中,会怎样? package com.javapatterns.singleton.demos; public class LazySingleton { private LazySingleton() { } synchronized static public LazySingleton getInstance() { if (m_instance == null) { m_instance = new LazySingleton(); } return m_instance; } private static LazySingleton m_instance = null; } 代码清单 5.10.懒汉式单态类的变种。 3、在 LazySingleton 的例子中,出现了两层检查 if (m_instance == null)。这是否必 要?如果将内层的检查去掉,会出问题吗? 4、同上,如果将外层的检查去掉,会出问题吗? 5、举例说明如何调用 EagerSingleton 类。 6、举例说明如何调用 RegSingleton 类和 RegSingletonChild 类。 7、在看守类中,变量 m_keptObjects 还可选择什么数据类型,使得程序占用更小? 8、设法用实例生成的时间,实例的 identityHashCode,类加载器,实例的总数, 实例化的序数 来确定一个单态确实是单态。 问答题答案 1、一个变量不能自已初始化,不可能有继承的关系。在爪哇语言里并没有真正的" 全程"变量, 一个变量必须属于某一个类。而在复杂的程序当中,一个静态变量的 初 始化发生在哪里,常常是一个不易确定的问题。当然,使用变量并没有什么错误,就好 选择使用 Fortran 语言而非爪哇语言编程并不是一种对错的问题一样。 2、这样做不会出错,但是效率不好。在原来的源代码中,synchronized 行为只在 第一次调用 此方法起作用,以后的调用均不会遇到。而在这里,任何凋用都会遇到 synchronized 的限制,这无异于 人为制造一个不必要的独木桥,十分愚蠢。 3、这样做一定会出问题。在第一次调用 getInstance()时可能有多个线程几乎同时 到达, 只有一个线程能到达内层检 查之内,其它的线程会在 synchronized()语句处等 待。这样当第一线程完成实例化之后,等待在 synchronized()语句处的其它线程会逐一 获准进入 synchronized()之后的语句。如果那里没有第二 次检查,它们就会逐一试图 进行实例化,而这是错的。 4、这样不会出问题,但是效率不好,十分愚蠢。道理与第一题类似。 5、 package com.javapatterns.singleton.demos; public class RegSingletonTest { public static void main(String[] args) { file://(1) Test eager System.out.println( EagerSingleton.getInstance() ); file://(2) Test reg System.out.println( RegSingleton.getInstance( "com.javapatterns.singleton.demos.RegSingleton").about() ) ; System.out.println( RegSingleton.getInstance(null).about() ) ; System.out.println( RegSingleton.getInstance( "com.javapatterns.singleton.demos.RegSingletonChild").about() ) ; System.out.println( RegSingletonChild.getInstance().about()) ; } } 代码清单 5.11. 几种单态类的使用方法。 6、见上题答案。 7、变量 m_keptObjects 还可选择 HashMap,这样更省资源。 6、爪哇语言观察者模式介绍 简单地说,观察者模式定义了一个一对多的依赖关系,让一个或多个观察者对象监察一 个主题对象。这样一个主题对象在状态上的变化能够通知所有的依赖于此对象的那些观 察者对象,使这些观察者对象能够自动更新。 观察者模式的结构 观察者(Observer)模式是对象的行为型模式,又叫做发表-订阅 (Publish/Subscribe)模式、模型-视图(Model/View)模式、源-收听者(Source/Listener) 模式或从属者(Dependents)模式。 本模式的类图结构如下: 图 6.1、观察者模式的静态结构可从类图中看清楚。 在观察者模式里有如下的角色: . 抽象主题(Subject)角色:主题角色把所有的观察者对象的引用保存在一个列 表里;每个主题都可以有任何数量的观察者。主题提供一个接口可以加上或撤销观察者 对象;主题角色又叫做抽象被观察者(Observable)角色; 图 6.2、抽象主题角色,有时又叫做抽象被观察者角色,可以用一个抽象类或者一个接 口实现;在具体的情况下也不排除使用具体类实现。 . 抽象观察者(Observer)角色:为所有的具体观察者定义一个接口,在得到通知 时更新自己; 图 6.3、抽象观察者角色,可以用一个抽象类或者一个接口实现;在具体的情况下也不 排除使用具体类实现。 . 具体主题(ConcreteSubject)角色:保存对具体观察者对象有用的内部状态; 在这种内部状态改变时给其观察者发出一个通知;具体主题角色又叫作具体被观察者角 色; 图 6.4、具体主题角色,通常用一个具体子类实现。 .具体观察者(ConcreteObserver)角色:保存一个指向具体主题对象的引用; 和一个与主题的状态相符的状态。具体观察者角色实现抽象观察者角色所要求的更新自 己的接口,以便使本身的状态与主题的状态自恰。 图 6.5、具体观察者角色,通常用一个具体子类实现。 下面给出一个示意性实现的 Java 代码。首先在这个示意性的实现里,用一个 Java 接口实现抽象主题角色,这就是下面的 Subject 接口: public interface Subject { public void attach(Observer observer); public void detach(Observer observer); void notifyObservers(); } 代码清单 6.1、Subject 接口的源代码。 这个抽象主题接口规定出三个子类必须实现的操作,即 attach() 用来增加一个观 察者对象;detach() 用来删除一个观察者对象;和 notifyObservers() 用来通知各个观 察者刷新它们自己。抽象主题角色实际上要求子类保持一个以所有的观察者对象为元素 的列表。 具体主题则是实现了抽象主题 Subject 接口的一个具体类,它给出了以上的三个操 作的具体实现。从下面的源代码可以看出,这里给出的 Java 实现使用了一个 Java 向量 来保存所有的观察者对象,而 attach() 和 detach() 操作则是对此向量的元素增减操 作。 import java.util.Vector; import java.util.Enumeration; public class ConcreteSubject implements Subject { public void attach(Observer observer) { observersVector.addElement(observer); } public void detach(Observer observer) { observersVector.removeElement(observer); } public void notifyObservers() { Enumeration enumeration = observers(); while (enumeration.hasMoreElements()) { ((Observer)enumeration.nextElement()).update(); } } public Enumeration observers() { return ((Vector) observersVector.clone()).elements(); } private Vector observersVector = new java.util.Vector(); } 代码清单 6.2、ConcreteSubject 类的源代码。 抽象观察者角色的实现实际上是最为简单的一个,它是一个 Java 接口,只声明了 一个方法,即 update()。这个方法被子类实现后,一被调用便刷新自己。 public interface Observer { void update(); } 代码清单 6.3、Observer 接口的源代码。 具体观察者角色的实现其实只涉及 update()方法的实现。这个方法怎么实现与应用 密切相关,因此本类只给出一个框架。 public class ConcreteObserver implements Observer { public void update() { // Write your code here } } 代码清单 6.4、ConcreteObserver 类的源代码。 虽然观察者模式的实现方法可以有设计师自己确定,但是因为从 AWT1.1 开始视窗 系统的事件模型采用观察者模式,因此观察者模式在 Java 语言里的地位较为重要。正 因为这个原因,Java 语言给出了它自己对观察者模式的支持。因此,本文建议读者在 自己的系统中应用观察者模式时,不妨利用 Java 语言所提供的支持。 Java 语言提供的对观察者模式的支持 在 Java 语言的 java.util 库里面,提供了一个 Observable 类以及一个 Observer 接 口,构成 Java 语言对观察者模式的支持。 Observer 接口 这个接口只定义了一个方法,update()。当被观察者对象的状态发生变化时,这个 方法就会被调用。这个方法的实现应当调用每一个被观察者对象的 notifyObservers()方 法,从而通知所有的观察对象。 图 6.6、java.util 提供的 Observer 接口的类图。 package java.util; public interface Observer { /** * 当被观察的对象发生变化时,这个方法会被调用。 */ void update(Observable o, Object arg); } 代码清单 6.5、java.util.Observer 接口的源代码。 Observable 类 被观察者类都是 java.util.Observable 类的子类。java.util.Observable 提供公开的 方法支持观察者对象,这些方法中有两个对 Observable 的子类非常重要:一个是 setChanged(),另一个是 notifyObservers()。第一个方法 setChanged()被调用之后会 设置一个内部标记变量,代表被观察者对象的状态发生了变化。第二个是 notifyObservers(),这个方法被调用时,会调用所有登记过的观察者对象的 update()方 法,使这些观察者对象可以更新自己。 java.util.Observable 类还有其它的一些重要的方法。比如,观察者对象可以调用 java.util.Observable 类的 addObserver()方法,将对象一个一个加入到一个列表上。当 有变化时,这个列表可以告诉 notifyObservers()方法那些观察者对象需要通知。由于这 个列表是私有的,因此 java.util.Observable 的子对象并不知道观察者对象一直在观察 着它们。 图 6.7、Java 语言提供的被观察者的类图。 被观察者类 Observable 的源代码: package java.util; public class Observable { private boolean changed = false; private Vector obs; /** 用 0 个观察者构造一个被观察者。**/ public Observable() { obs = new Vector(); } /** * 将一个观察者加到观察者列表上面。 */ public synchronized void addObserver(Observer o) { if (!obs.contains(o)) { obs.addElement(o); } } /** * 将一个观察者对象从观察者列表上删除。 */ public synchronized void deleteObserver(Observer o) { obs.removeElement(o); } /** * 相当于 notifyObservers(null) */ public void notifyObservers() { notifyObservers(null); } /** * 如果本对象有变化(那时 hasChanged 方法会返回 true) * 调用本方法通知所有登记在案的观察者,即调用它们的 update()方法, * 传入 this 和 arg 作为参量。 */ public void notifyObservers(Object arg) { /** * 临时存放当前的观察者的状态。参见备忘录模式。 */ Object[] arrLocal; synchronized (this) { if (!changed) return; arrLocal = obs.toArray(); clearChanged(); } for (int i = arrLocal.length-1; i>=0; i--) ((Observer)arrLocal[i]).update(this, arg); } /** * 将观察者列表清空 */ public synchronized void deleteObservers() { obs.removeAllElements(); } /** * 将“已变化”设为 true */ protected synchronized void setChanged() { changed = true; } /** * 将“已变化”重置为 false */ protected synchronized void clearChanged() { changed = false; } /** * 探测本对象是否已变化 */ public synchronized boolean hasChanged() { return changed; } /** * 返还被观察对象(即此对象)的观察者总数。 */ public synchronized int countObservers() { return obs.size(); } } 代码清单 6.6、java.util.Observer 接口的源代码。 这个 Observable 类代表一个被观察者对象。一个被观察者对象可以有数个观察者 对象,一个观察者可以是一个实现 Observer 接口的对象。在被观察者对象发生变化时, 它会调用 Observable 的 notifyObservers方法,此方法调用所有的具体观察者的 update() 方法,从而使所有的观察者都被通知更新自己。见下面的类图: 图 6.8、使用 Java 语言提供的对观察者模式的支持。 发通知的次序在这里没有指明。Observerable 类所提供的缺省实现会按照 Observers 对象被登记的次序通知它们,但是 Observerable 类的子类可以改掉这一次 序。子类并可以在单独的线程里通知观察者对象;或者在一个公用的线程里按照次序执 行。 当一个可观察者对象刚刚创立时,它的观察者集合是空的。两个观察者对象在它们 的 equals()方法返回 true 时,被认为是两个相等的对象。 怎样使用 Java 对观察者模式的支持 为了说明怎样使用 Java 所提供的对观察者模式的支持,本节给出一个非常简单的 例子。在这个例子里,被观察对象叫做 Watched,也就是被监视者;而观察者对象叫做 Watcher。Watched 对象继承自 java.util.Obsevable 类;而 Watcher 对象实现了 java.util.Observer 接口。另外有一个对象 Tester,扮演客户端的角色。 这个简单的系统的结构如下图所示。 图 6.9、一个使用 Observer 接口和 Observable 类的例子。 在客户端改变 Watched 对象的内部状态时,Watched 就会通知 Watcher 采取必要 的行动。 package com.javapatterns.observer.watching; import java.util.Observer; public class Tester { static private Watched watched; static private Observer watcher; public static void main(String[] args) { watched = new Watched(); watcher = new Watcher(watched); watched.changeData("In C, we create bugs."); watched.changeData("In Java, we inherit bugs."); watched.changeData("In Java, we inherit bugs."); watched.changeData("In Visual Basic, we visualize bugs."); } } 代码清单 6.7、Tester 类的源代码。 package com.javapatterns.observer.watching; import java.util.Observable; public class Watched extends Observable { private String data = ""; public String retrieveData() { return data; } public void changeData(String data) { if ( !this.data.equals( data) ) { this.data = data; setChanged(); } notifyObservers(); } } 代码清单 6.8、Watched 类的源代码。 package com.javapatterns.observer.watching; import java.util.Observable; import java.util.Observer; public class Watcher implements Observer { public Watcher(Watched w) { w.addObserver(this); } public void update( Observable ob, Object arg) { System.out.println("Data has been changed to: '" + ((Watched)ob).retrieveData() + "'"); } } 代码清单 6.9、Watcher 类的源代码。 可以看出,虽然客户端将 Watched 对象的内部状态赋值了四次,但是值的改变只 有三次: watched.changeData("In C, we create bugs."); watched.changeData("In Java, we inherit bugs."); watched.changeData("In Java, we inherit bugs."); watched.changeData("In Visual Basic, we visualize bugs."); 代码清单 6.10、被观察者的内部状态发生了改变。 对应地,Watcher 对象汇报了三次改变,下面就是运行时间程序打印出的信息: Data has been changed to: 'In C, we create bugs.' Data has been changed to: 'In Java, we inherit bugs.' Data has been changed to: 'In Visual Basic, we visualize bugs.' 代码清单 6.11、运行的结果。 菩萨的守瓶龟 想当年齐天大圣为解救师傅唐僧,前往南海普陀山请菩萨降伏妖怪红孩儿:“菩萨 听说...恨了一声,将手中宝珠净瓶往海心里扑的一掼...只见那海当中,翻波跳浪,钻出 个瓶来,原来是一个怪物驮着出来...要知此怪名和姓,兴风作浪恶乌龟。” 使用面向对象的语言描述,乌龟便是一个观察者对象,它观察的主题是菩萨。一旦 菩萨将净瓶掼到海里,就象征着菩萨作为主题调用了 notifyObservers()方法。在西游记 中,观察者对象有两个,一个是乌龟,另一个是悟空。悟空的反应在这里暂时不考虑, 而乌龟的反应便是将瓶子驮回海岸。 图 6.10、菩萨和菩萨的守瓶乌龟。 菩萨作为被观察者对象,继承自 Observable 类;而守瓶乌龟作为观察者,继承自 Observer 接口;这个模拟系统的实现可以采用 Java 对观察者模式的支持达成。 Java 中的 DEM 事件机制 AWT 中的 DEM 机制 责任链模式一章中曾谈到,AWT1.0 的事件处理的模型是基于责任链的。这种模型 不适用于复杂的系统,因此在 AWT1.1 版本及以后的各个版本中,事件处理模型均为基 于观察者模式的委派事件模型(Delegation Event Model 或 DEM)。 在 DEM 模型里面,主题(Subject)角色负责发布(publish)事件,而观察者角色 向特定的主题订阅(subscribe)它所感兴趣的事件。当一个具体主题产生一个事件时, 它就会通知所有感兴趣的订阅者。 使用这种发布-订阅机制的基本设计目标,是提供一种将发布者与订阅者松散地耦 合在一起的联系形式,以及一种能够动态地登记、取消向一个发布者的订阅请求的办法。 显然,实现这一构思的技巧,是设计抽象接口,并把抽象层和具体层分开。这在观察者 模式里可以清楚地看到。 使用 DEM 的用词,发布者叫做事件源(event source),而订阅者叫做事件聆听 者(event listener)。在 Java 里面,事件由类代表,事件的发布是通过同步地调用成 员方法做到的。 Servlet 技术中的的 DEM 机制 AWT 中所使用的 DEM 事件模型实际上被应用到了所有的 Java 事件机制上。 Servlet 技术中的事件处理机制同样也是使用的 DEM 模型。 SAX2 技术中的 DEM 机制 DEM 事件模型也被应用到了 SAX2 的事件处理机制上。 观察者模式的效果 观察者模式的效果有以下的优点: 第一、观察者模式在被观察者和观察者之间建立一个抽象的耦合。被观察者角色所 知道的只是一个具体观察者列表,每一个具体观察者都符合一个抽象观察者的接口。被 观察者并不认识任何一个具体观察者,它只知道它们都有一个共同的接口。 由于被观察者和观察者没有紧密地耦合在一起,因此它们可以属于不同的抽象化层 次。如果被观察者和观察者都被扔到一起,那么这个对象必然跨越抽象化和具体化层次。 第二、观察者模式支持广播通讯。被观察者会向所有的登记过的观察者发出通知, 观察者模式有下面的缺点: 第一、如果一个被观察者对象有很多的直接和间接的观察者的话,将所有的观察者 都通知到会花费很多时间。 第二、如果在被观察者之间有循环依赖的话,被观察者会触发它们之间进行循环调 用,导致系统崩溃。在使用观察者模式是要特别注意这一点。 第三、如果对观察者的通知是通过另外的线程进行异步投递的话,系统必须保证投 递是以自恰的方式进行的。 第四、虽然观察者模式可以随时使观察者知道所观察的对象发生了变化,但是观察 者模式没有相应的机制使观察者知道所观察的对象是怎么发生变化的。 观察者模式与其它模式的关系 观察者模式使用了备忘录模式(Memento Pattern)暂时将观察者对象存储在被观察 者对象里面。 问答题 第一题、我和妹妹跟妈妈说:“妈妈,我和妹妹在院子里玩;饭做好了叫我们一声。” 请问这是什么模式?能否给出类图说明? 问答题答案 第一题答案、这是观察者模式。我和妹妹让妈妈告诉我们饭做好了,这样我们就可 以来吃饭了。换用较为技术化的语言来说,当系统的主题(饭)发生变化时,就告诉系 统的其它部份(观察者们,也就是妈妈、我和妹妹),使其可以调整内部状态(有开始 吃饭的准备),并采取相应的行动(吃饭)。 系统的类图说明如下。 图 6.11、系统的类图。 7、Java 模式开发之责任链模式 从击鼓传花谈起 击鼓传花是一种热闹而又紧张的饮酒游戏。在酒宴上宾客依次坐定位置,由一人击 鼓,击鼓的地方与传花的地方是分开的,以示公正。开始击鼓时,花束就开始依次传递, 鼓声一落,如果花束在某人手中,则该人就得饮酒。 假比说,贾母、贾赦、贾政、贾宝玉和贾环是五个参加击鼓传花游戏的传花者,他 们组成一个环链。击鼓者将花传给贾母,开始传花游戏。花由贾母传给贾赦,由贾赦传 给贾政,由贾政传给贾宝玉,又由贾宝玉传给贾环,由贾环传回给贾母,如此往复(见 下图)。当鼓声停止时,手中有花的人就得执行酒令。 图 7.1、击鼓传花。 击鼓传花便是责任链模式的应用。在责任链模式里,很多的对象由每一个对象对其 下家的引用而联接起来形成一条链。请求在这个链上传递,直到链上的某一个对象决定 处理此请求。发出这个请求的客户端并不知道链上的哪一个对象最终处理这个请求,这 使得系统可以在不影响客户端的情况下动态地重新组织链和分配责任。 责任链可能是一条直线、一个环链甚至一个树结构的一部分。 责任链模式的结构 责任链模式是一种对象的行为模式,它所涉及到的角色如下: 第一、抽象处理者(Handler)角色、定义出一个处理请求的接口;如果需要,接 口可以定义出一个方法,以返回对下家的引用。下图给出了一个示意性的类图: 图 7.2、抽象处理者角色。 在图中的积累关系给出了具体子类对下家的引用,抽象方法 handleRequest()规范 了子类处理请求的操作。 第二、具体处理者(ConcreteHandler)角色、处理接到请求后,可以选择将请求 处理掉,或者将请 求传给下家。下图给出了一个示意性的类图。 图 7.3、具体处理者角色。 上图中的示意性的具体处理者 ConcreteHandler 类只有 handleRequest()一个方 法。 责任链模式的静态类结构可见下图: 图 7.4、责任链模式的类图定义。 在图中还给出了一个客户端,以便读者可以更清楚地看到责任链模式是怎样应用 的。抽象处理者的示意性源代码: public class Handler { public void handleRequest() { if (successor != null) { successor.handleRequest(); } // Write your code here } public void setSuccessor(Handler successor) { this.successor = successor; } public Handler getSuccessor() { return successor; } private Handler successor; } 代码清单 7.1、抽象处理者的源代码。 具体处理者的示意性源代码: public class ConcreteHandler extends Handler { public void handleRequest() { if (getSuccessor() != null) { getSuccessor().handleRequest(); } if (successor != null) { successor.handleRequest(); } // Write your code here } } 代码清单 7.2、具体处理者的源代码。 客户端的源代码如下: public class Client { private Handler handler; public static void main(String[] args) { handler = new ConcreteHandler(); //write your code here } } 代码清单 7.3、客户端的源代码。 纯的与不纯的责任链模式 一个纯的责任链模式要求一个具体的处理者对象只能在两个行为中选择一个:一是 承担责任,二是把责任推给下家。不允许出现某一个具体处理者对象在承担了一部分责 任后又把责任向下传的情况。 在一个纯的责任链模式里面,一个请求必须被某一个处理者对象所接受;在一个不 纯的责任链模式里面,一个请求可以最终不被任何接受端对象所接受。 纯的责任链模式的实际例子很难找到,一般看到的例子均是不纯的责任链模式的实 现。有些人认为不纯的责任链根本不是责任链模式,这也许是有道理的;但是在实际的 系统里,纯的责任链很难找到;如果坚持责任链不纯便不是责任链模式,那么责任链模 式便不会有太大的意义了。 Java1.0 版的 AWT 事件处理机制 Java 的 1.0 版中 AWT 库使用了责任链模式和命令模式来处理 GUI 的事件。由于视 窗部件往往处在容器部件里面,因此当事件发生在一个部件上时,此部件的事件处理器 可以处理此事件,然后决定是否将事件向上级容器部件传播;上级容器部件接到事件后 可以在此处理此事件然后决定是否将事件再次向上级容器部件传播,如此往复,直到事 件到达顶层部件。 事件浮升机制 比如,当一个视窗部件接到一个 MOUSE_CLICKED 事件时,事件首先传播到它所 发生的部件上,然后向其容器部件传播。容器可以选择处理这个事件,或者再将此事件 向更高一级的容器部件传播。事件如此一级级地向上传播,就像水底的气泡一点一点地 冒到水面上一样,因此又叫做事件浮升(Event Bubbling)机制。下面就是一段典型的 Java1.0 版的 AWT 库里处理事件的代码: public boolean action(Event event, Object obj) { if (event.target == btnOK) { doOKBtnAction(); } else if (event.target == btnExit) { doExitBtnAction(); } else { return super.action(event, obj); } return true; } 代码清单 7.4、Java1.0 版本中 AWT 处理事件的典型代码。 在这段代码里面,action()判断目标部件是不是 btnOK 或 btnExit;如果是,便运行 相应的方法;如果不是,便返还 true。一个方法返还 true 便使得事件停止浮升。 AWT1.0 的事件处理的模型的缺点之一 AWT1.0 的事件处理的模型是基于继承的。为了使一个程序能够捕捉 GUI 的事件并 处理此事件,必须 subclass 此部件并且给其子类配备事件处理器,也就是置换掉 action() 方法或者 handleEvent()方法。这不是应当提倡的做法:在一个面向对象的系统里,经 常使用的应当是委派,继承不应当是常态。 在一个复杂的 GUI 系统里,这样为所有有事件的部件提供子类,会导致很多的子 类,这是不是很麻烦的吗? 当然,由于事件浮升机制,可以在部件的树结构的根部部件里面处理所有的事件。 但是这样一来,就需要使用复杂的条件转移语句在这个根部部件里辨别事件的起源和处 理方法。这种非常过程化的处理方法很难维护,并且与面向对象的设计思想相违背。 AWT1.0 的事件处理的模型的缺点之二 由于每一个事件都会沿着部件树结构向上传播,因此事件浮升机制会使得事件的处 理变得较慢。这也是缺点之一。 比如在有些操作系统中,鼠标每移动一个色素,都会激发一个 MOUSE_MOVE 事 件。每一个这样的事件都会沿着部件的容器树结构向上传播,这会使得鼠标事件成灾。 AWT1.0 的事件处理的模型的缺点之三 AWT1.0 的事件处理的模型只适用于 AWT 部件类。这是此模型的另一个缺点。 责任链模式要求链上所有的对象都继承自一个共同的父类,这个类便是 java.awt.Component 类。 AWT1.0 的事件处理的模型是不纯的责任链模式 显然,由于每一级的部件在接到事件时,都可以处理此事件;而不论此事件是否在 这一级得到处理,事件都可以停止向上传播或者继续向上传播。这是典型的不纯的责任 链模式。 AWT1.1 以后的事件处理的模型 自从 AWT1.1 以后,AWT 的事件处理模型于 1.0 相比有了很大的变化。新的事件 处理模型是建立在观察者模式的基础之上的,而不再是责任链模式的基础之上的。 关于新的事件处理模型和观察者设计模式,请见“观察者模式”一节。 红楼梦中击鼓传花的故事 显然,击鼓传花符合责任链模式的定义。参加游戏的人是一个个的具体处理者对象, 击鼓的人便是客户端对象。花代表酒令,是传向处理者的请求,每一个参加游戏的人在 接到传来的花时,可选择的行为只有两个:一是将花向下传;一是执行酒令---喝酒。一 个人不能既执行酒令,又向下家传花;当某一个人执行了酒令之后,游戏重新开始。击 鼓的人并不知道最终是由哪一个做游戏的人执行酒令,当然执行酒令的人必然是做游戏 的人们中的一个。 击鼓传花的类图结构如下: 图 7.5、击鼓传花系统的类图定义。 单独考虑击鼓传花系统,那么像贾母、贾赦、贾政、贾宝玉和贾环等传花者均应当 是“具体传花者”的对象,而不应当是单独的类;但是责任链模式往往是建立在现有系统 的基础之上的,因此链的结构和组成不由责任链模式本身决定。 系统的分析 在《红楼梦》第七十五回里生动地描述了贾府里的一场击鼓传花游戏:“贾母坐下, 左垂首贾赦,贾珍,贾琏,贾蓉,右垂首贾政,宝玉,贾环,贾兰,团团围坐。...贾母 便命折一枝桂花来,命一媳妇在屏后击鼓传花。若花到谁手中,饮酒一杯...于是先从贾 母起,次贾赦,一一接过。鼓声两转,恰恰在贾政手中住了,只得饮了酒。”这场游戏 接着又把花传到了宝玉和贾赦手里,接着又传到了在贾环手里... 如果用一个对象系统描述贾府,那么贾母、贾赦、贾政、贾宝玉和贾环等等就应当 分别由一个个具体类代表,而这场击鼓传花游戏的类图,按照责任链模式,应当如下图 所示: 图 7.6、红楼梦中的击鼓传花游戏的示意性类图。 换言之,在击鼓传花游戏里面,有下面的几种角色: • 抽象传花者,或 Handler 角色、定义出参加游戏的传花人要遵守的规则,也 就是一个处理请求的接口 和对下家的引用; • 具体传花者,或 ConcreteHandler 角色、每一个传花者都知道下家是谁,要 么执行酒令,要么把花 向下传。这个角色由贾母、贾赦、贾珍、贾琏、贾蓉、 贾政、宝玉、贾环、贾兰等扮演。 • 击鼓人,或 Client 角色、即行酒令的击鼓之人。《红楼梦》没有给出此人的具 体姓名,只是说由“一 媳妇”扮演。 图 7.7、贾府这次击鼓传花的示意性对象图。 可以看出,击鼓传花游戏满足责任链模式的定义,是纯的责任链模式的例子。 Java 系统的解 下面的类图给出了这些类的具体接口设计。读者不难看出,DrumBeater(击鼓者)、 Player(传花者)、JiaMu(贾母)、JiaShe(贾赦)、JiaZheng(贾政)、JiaBaoYu(宝 玉)、JiaHuan(贾环)等组成这个系统。 图 7.8、击鼓传花的类图完全符合责任链模式的定义。 下面是客户端类 DrumBeater 的源代码: public class DrumBeater { private static Player player; static public void main(String[] args) { player = new JiaMu( new JiaShe( new JiaZheng( new JiaBaoYu(new JiaHuan(null))))); player.handle(4); } } 代码清单 7.5、DrumBeater 的源代码。 abstract class Player { abstract public void handle(int i); private Player successor; public Player() { successor = null; } protected void setSuccessor(Player aSuccessor) { successor = aSuccessor; } public void next(int index) { if( successor != null ) { successor.handle(index); } else { System.out.println("Program terminated."); } } } 代码清单 7.6、抽象传花者 Play 类的源代码。 抽象类 Player 给出了两个方法的实现,以格式 setSuccessor(),另一个是 next()。 前者用来设置一个传花者对象的下家,后者用来将酒令传给下家。Player 类给出了一个 抽象方法 handle(),代表执行酒令。 下面的这些具体传花者类将给出 handle()方法的实现。 class JiaMu extends Player { public JiaMu(Player aSuccessor) { this.setSuccessor(aSuccessor); } public void handle(int i) { if( i == 1 ) { System.out.println("Jia Mu gotta drink!"); } else { System.out.println("Jia Mu passed!"); next(i); } } } 代码清单 7.7、代表贾母的 JiaMu 类的源代码。 class JiaShe extends Player { public JiaShe(Player aSuccessor) { this.setSuccessor(aSuccessor); } public void handle(int i) { if( i == 2 ) { System.out.println("Jia She gotta drink!"); } else { System.out.println("Jia She passed!"); next(i); } } } 代码清单 7.8、代表贾赦的 JiaShe 类的源代码。 class JiaZheng extends Player { public JiaZheng(Player aSuccessor) { this.setSuccessor(aSuccessor); } public void handle(int i) { if( i == 3 ) { System.out.println("Jia Zheng gotta drink!"); } else { System.out.println("Jia Zheng passed!"); next(i); } } } 代码清单 7.9、代表贾政的 JiaZheng 类的源代码。 class JiaBaoYu extends Player { public JiaBaoYu(Player aSuccessor) { this.setSuccessor(aSuccessor); } public void handle(int i) { if( i == 4 ) { System.out.println("Jia Bao Yu gotta drink!"); } else { System.out.println("Jia Bao Yu passed!"); next(i); } } } 代码清单 7.10、代表贾宝玉的 JiaBaoYu 类的源代码。 class JiaHuan extends Player { public JiaHuan(Player aSuccessor) { this.setSuccessor(aSuccessor); } public void handle(int i) { if( i == 5 ) { System.out.println("Jia Huan gotta drink!"); } else { System.out.println("Jia Huan passed!"); next(i); } } } 代码清单 7.11、代表贾环的 JiaHuan 类的源代码。 可以看出,DrumBeater 设定了责任链的成员和他们的顺序:责任链由贾母开始到 贾环,周而复始。JiaMu 类、JiaShe 类、JiaZheng 类、JiaBaoYu 类与 JiaHuan 类均是 抽象传花者 Player 类的子类。 本节所实现的 DrumBeater 类在把请求传给贾母时,实际上指定了由 4 号传花者处 理酒令。虽然 DrumBeater 并不知道哪一个传花者类持有号码 4,但是这个号码在本系 统一开始就写死的。这当然并不符合击鼓传花游戏的精神,因为这个游戏实际上要求有 两个同时进行的过程:击鼓过程和传花过程。击鼓应当是定时停止的,当击鼓停止时, 执行酒令者就确定了。但是本节这样做可以使问题得到简化并将读者的精力放在责任链 模式上,而不是两个过程的处理上。 下一章会给出一个多线程的系统,更加逼真地模拟击鼓传花系统。 在什么情况下使用责任链模式 在下面的情况下使用责任链模式: 第一、系统已经有一个由处理者对象组成的链。这个链可能由复合模式给出, 第一、当有多于一个的处理者对象会处理一个请求,而且在事先并不知道到底由哪 一个处理者对象处理一个请求。这个处理者对象是动态确定的。 第二、当系统想发出一个请求给多个处理者对象中的某一个,但是不明显指定是哪 一个处理者对象会处理此请求。 第三、当处理一个请求的处理者对象集合需要动态地指定时。 使用责任链模式的长处和短处 责任链模式减低了发出命令的对象和处理命令的对象之间的耦合,它允许多与一个 的处理者对象根据自己的逻辑来决定哪一个处理者最终处理这个命令。换言之,发出命 令的对象只是把命令传给链结构的起始者,而不需要知道到底是链上的哪一个节点处理 了这个命令。 显然,这意味着在处理命令上,允许系统有更多的灵活性。哪一个对象最终处理一 个命令可以因为由那些对象参加责任链、以及这些对象在责任链上的位置不同而有所不 同。 责任链模式的实现 链结构的由来 值得指出的是,责任链模式并不创建出责任链。责任链的创建必须有系统的其它部 分完成。 责任链模式减低了请求的发送端和接收端之间的耦合,使多个对象都有机会处理这 个请求。一个链可以是一条线,一个树,也可以是一个环。链的拓扑结构可以是单连通 的或多连通的,责任链模式并不指定责任链的拓扑结构。但是责任链模式要求在同一个 时间里,命令只可以被传给一个下家(或被处理掉);而不可以传给多于一个下家。在 下面的图中,责任链是一个树结构的一部分。 图 7.9、责任链是系统已有的树结构的一部分。图中有阴影的对象给出了一个可能的命 令传播路径。 责任链的成员往往是一个更大的结构的一部分。比如在前面所讨论的《红楼梦》中 击鼓传花的游戏中,所有的成员都是贾府的成员。如果责任链的成员不存在,那么为了 使用责任链模式,就必须创建它们;责任链的具体处理者对象可以是同一个具体处理者 类的实例。 在 Java 的 1.0 版的 AWT 事件处理模型里,责任链便是视窗上的部件的容器等级结 构。 在下面会谈到的 Internet Explorer 的 DHTML 的 DOM 事件处理模型里,责任链则 是 DOM 等级结构本身。 命令的传递 在一个责任链上传递的可能不只有一个命令,而是数个命令。这些命令可以采取抽 象化层、具体化层的多态性实现方式,见下图,从而可以将命令对象与责任链上的对象 之间的责任分隔开,并将命令对象与传播命令的对象分隔开。 图 7.10、多个命令在责任链上的传播。 当然如果责任链上的传播的命令只有一个、且是固定的命令,那么这个命令不一定 要对象化。这就是本节处理击鼓传花游戏里面传来传去的花束的办法。花束代表酒令, 可以由一个对象代表;但是本章的处理是过程式的,用对下家对象的 next()方法的调用 达成。 对象的树结构 在面向对象的技术里,对象的树结构是一个强有力的工具,更是模式理论的一个重 要的组成部分,需要应用到符合模式、装饰模式和迭代子模式。 《墨子.天志》说:“庶人竭力从事,未得次己而为政,有士政之,士竭力从事,未 得次己而为政,有将军、大夫政之;将军、大夫竭力从事,未得次己而为政,有三公、 诸侯政之;三公、诸侯竭力听治,未得次己而为政,有天子政之;天子未得次己而为政, 有天政之。” “次”意为恣意。上面的话就是说,百姓有官吏管治,官吏由将军和士大夫管治,将 军和士大夫由三公和诸侯管治,三公和诸侯由天子管治,天子由天管治。 图 7.11、墨子论责任和责任链的传播。图中有阴影的对象给出了一个可能的责任链选择。 当一个百姓提出要求时,此要求会传达到“士”一级,再到“大夫”一级,进而传到“诸 侯”一级,“天子”一级,最后到“天”一级。 DHTML 中的事件处理 浏览器的 DOM(Document Object Model)模型中的事件处理均采用责任链模式。本节 首先考察 Netscape 浏览器的 DHTML 的事件处理,然后再研究 Internet Explorer 的事 件模型。 Netscape 的事件模型 Netscape 的事件处理机制叫做“事件捕捉”(Event Capturing)。在事件捕捉机制 里面,一个事件是从 DOM 的最高一层向下传播,也就是说,window 对象是第一个接 到事件的,然后是 document 对象,如此往下---事件的产生对象反而是最后一个接到事 件的。 如果要是一个对象捕获某一个事件,只需要调用 captureEvent()方法;如果要使一 个对象把某一个事件向下传而不处理此事件,只需要对此对象使用 releaseEvents 方法 即可。下面考察一个简单的事件捕获和传递的例子。 图 7.12、一个 Netscape 的例子。 在这个例子里,有一个 textbox 和两个 button,一个叫做“Capture Event”,单击后 会使网页的 click 事件被捕捉,文字框中的计数会加一;另一个叫做“Release Event”, 单击后会使网页的 click 事件不被捕捉。 使 click 事件被捕捉需要调用 captureEvent()方法,而使 click 事件不被捕捉需要调 用 releaseEvent()方法。下面是具体的 html 和 JavaScript 代码。 代码清单 7.6、JavaScript 和 HTML 源代码。 显然,一个事件可以在几个不同的等级上得到处理,这是一个不纯的责任链模式。 Internet Explorer 的事件模型 Internet Explorer 处理事件的方式与 Netscape 既相似又不同。当一个事件发生在 Internet Explorer 所浏览的网页中时,Internet Explorer 会使用 DHTML 的“Event Bubbling”即事件浮升机制处理此事件。Internet Explorer 的 DOM 模型是 html 对象等级 结构和事件处理机制。在 DOM 里面,每一个 html 标示都是一个 DOM 对象,而每一个 DOM 对象都可以产生事先定义好的几个事件中的一个(或几个)。这样的一个事件会 首先发生在事件所属的对象上,然后向上传播,传到此对象所属的容器对象上,如此等 等。因此,事件浮升机制恰恰是事件捕捉机制的相反面。 在 Event Bubbling 机制里面,产生事件的对象首先会收到事件。然后,事件会依 照对象的等级结构向上传播。比如一个 DIV 里有一个 Form,Form 里面又有一个 Button, 那么当 Button 的 onclick 事件产生时,Form 的 onclick 事件代码就会被执行。然后,事 件就会传到 DIV 对象。如果 DIV 对象的 onclick 事件有任何代码的话,这代码就会被执 行,然后事件继续沿着 DOM 结构上行。 如果要阻止事件继续向上传播,可以在事件链的任何一个节点上把 cancelBubble 性质设置成 True 即可。 Internet Explorer 浏览器几乎为所有的 HTML 标识符都提供了事件句柄,因此 Internet Explorer 不需要 captureEvents()方法和 releaseEvents()方法来捕获和释放事 件。下面的 JavaScript 语句指定了 document 对象的 onclick 事件的处理方法: document.onclick = functionName; 而下面的语句则停止了 document 对象对 onclick 事件的处理。 document.onclick = null; 因为事件处理性质被赋值 null,document 便没有任何的方法处理此事件。换言之, null 值禁止了此对象的事件处理。这种方法可以用到任何的对象和任何的事件上面。当 然这一做法不适用于 Netscape。 与 Netscape 中一样,一个事件处理方法可以返还 Boolean 值。比如,单击一个超 链接标记符是否造成浏览器跟进,取决于此超链接标记符的 onclick 事件是否返还 true。 为了显示 Internet Explorer 中的事件浮升机制,本节特准备了下面的例子。一个 Form 里面有一个 Button,请见下图: 图 7.13、一个 Internet Explorer 的例子。 其 HTML 代码请见下面: 代码清单 7.7、JavaScript 和 HTML 源代码。 当 myButton 的 onclick 事件发生时,myButton 的事件处理首先被激发,从而显示 出如下的对话窗: 图 7.14、myButton 对象的事件处理被激发。 然后事件会象气泡一样浮升到上一级的对象,即 myForm 对象上。myForm 对象的 事件处理给出下面的对话窗: 图 7.15、myFormn 对象的事件处理被激发。 这以后事件继续浮升到更上一级的对象,即 body 上。这时,document 对象的事 件处理被激发,并给出下面的对象窗: 图 7.16、document 对象的事件处理被激发。 这就是事件浮升(Event Bubbling)机制。 显然,这三级对象组成一个责任链,而事件便是命令或请求。当事件沿着责任链传 播时,责任链上的对象可以选择处理或不处理此事件;不论事件在某一个等级上是否得 到处理,事件都可以停止上浮或继续上浮。这是不纯的责任链模式。 责任链模式与其它模式的关系 责任链模式与以下的设计模式相关: 复合模式(Composite Pattern) 当责任链模式中的对象链属于一个较大的结构 时,这个较大的结构可能符合复合模式。 命令模式(Command Pattern) 责任链模式使一个特定的请求接收对象对请求或 命令的执行变得不确定。而命令模式使得一个特定的对象对一个命令的执行变得明显和 确定。 模版方法模式(Template Method) 当组成责任链的处理者对象是按照复合模式 组成一个较大的结构的责成部分的话,模版方法模式经常用来组织单个的对象的行为。 问答题 第一题、在称为“拱猪”的纸牌游戏中,四个参加者中由“猪”牌的,可以选择一个时 机放出这张“猪”牌。“猪”牌放出后,四个人中的一个会不可避免地拿到这张“猪”牌。 请使用责任链模式说明这一游戏,并给出 UML 结构图。 第二题、《墨子.迎敌祠》里描守城军队的结构:“城上步一甲、一戟,其赞三人。五 步有伍长,十步有什长,百步有佰长,旁有大帅,中有大将,皆有司吏卒长。” 一个兵勇需要上级批准以便执行一项任务,他要向伍长请求批准。伍长如果有足够 的权限,便会批准或驳回请求;如果他没有足够的权限,便会向上级,即什长转达这个 请求。什长便会重复同样的过程,直到大将那里。一个请求最终会被批准或驳回,然后 就会象下传,直到传回到发出请求的士兵手里。 有些请求会很快返回,有些则要经过较长的过程。请求到底由谁批准,事前并不知 道。请求的处理者并不是固定的,有些军官会晋升,转业,或从别的单位转过来,等等。 请使用责任链模式解释这个核准请求的结构。 (本例子受到文献[ALPERT98]里“Chain of Responsibility”一节所给出的一个例子 的启发。) 第三题、王羲之在《兰亭序》中写道:“有清流激湍,映带左右,引以为流觞曲水, 列坐其次。”讲的是大伙列坐水畔,随水流放下带羽毛的酒杯饮酒。远道而来的酒杯流 到谁的面前,谁就取而饮之。 在这个活动中,参加者做成一排,面对着一条弯曲的小溪。侍者把酒杯盛满酒,让 酒杯沿着小溪向下漂流。酒杯漂到一个参加者面前的时候,他可以选择取酒饮之,也可 以选择让酒杯漂向下家。 假设每一杯酒最终都会被参加者中之一喝掉,那么这个游戏是不是纯的责任链模 式? 问答题答案 第一题答案、这是一个纯的责任链模式。 首先,在“猪”牌放出之后,每个人都只能要么躲过“猪”牌,要么吃住“猪”牌。“猪”牌 便是责任链模式中的请求,四个人便是四个处理者对象,组成责任链。 每一个参加者的行为不仅仅取决于他手中的牌,而且取决于他是否想得“猪”牌。一 个想收全红的人,可能会权力揽“猪”牌,一个不想收全红的人,一般不想收“猪”牌,除 非他想阻止别人收“猪”牌。因为一旦有人收全红,另外三个人就会复出较大的代价,因 此阻止别人收全红的动机,会促使一个参与者主动收“猪”牌。有的时候,放出“猪”牌的 人也会想要得“猪”牌而得不到,有的时候放出“猪”牌的人想要害人但却害了自己。 这就是说,到底是四个人中的哪一个人得到“猪”牌是完全动态决定的。 系统的 UML 结构图如下: 图 7.18、纸牌游戏“拱猪”的 UML 类图。 由于玩牌的时候,可能有四人位置的任意调换,或者有候补者在旁等待,一旦在任 的玩家被淘汰,便可上任。这样四个人组成的牌局是动态变化的。同时因为谁会拿到“猪” 牌在每一局均会不同,因此谁会放出“猪”牌也是动态的。 因此,责任链的组成和顺序变不是一成不变的,而是动态的和变化的。 第二题答案、墨子的守城部队的等级结构可以用下面的对象图表示。 图 7.17、对象图,显示墨子的守城部队。 显然,这是一个纯的责任链模式。任何提出申请的兵勇便是客户端,伍长、什长、 佰长、大帅和大将是责任链的具体处理者对象。一个申请会在链上传播,直到某一级的 有合适的权限的军官处理申请为止。每一个申请必会得到处理,批准或驳回。一个被处 理过的申请会按照相反的方向传播,直到传回到发出申请的兵勇手中。 发出申请的士兵在发出申请时根本不知道他的申请会向上传播多少等级。 第三题答案、这是纯的责任链模式。 首先,酒便是请求的代表。每一个酒会的参与者都是一个请求的处理者对象,所有 的参加者组成责任链。一个酒杯会漂过每一个参加者,代表一个请求经过每一个请求处 理者对象。 每一个酒会的参加者都有可能选择喝掉某一杯酒,或者让酒继续漂向下一个参加 者,而且假定所有的酒最后都会被某一个参加者喝掉,因此这是纯的责任链模式。 8、设计模式之 Observer Java 深入到一定程度,就不可避免的碰到设计模式(design pattern)这一概念,了解 设计模式,将使自己对 java 中的接口或抽象类应用有更深的理解.设计模式在 java 的中 型系统中应用广泛,遵循一定的编程模式,才能使自己的代码便于理解,易于交流, Observer(观察者)模式是比较常用的一个模式,尤其在界面设计中应用广泛,而本站 所关注的是 Java 在电子商务系统中应用,因此想从电子商务实例中分析 Observer 的应 用. 虽然网上商店形式多样,每个站点有自己的特色,但也有其一般的共性,单就"商 品的变化,以便及时通知订户"这一点,是很多网上商店共有的模式,这一模式类似 Observer patern. 具体的说,如果网上商店中商品在名称 价格等方面有变化,如果系统能自动通知 会员,将是网上商店区别传统商店的一大特色.这就需要在商品 product 中加入 Observer 这样角色,以便 product 细节发生变化时,Observer 能自动观察到这种变化,并能进行 及时的 update 或 notify 动作. Java 的 API 还为为我们提供现成的 Observer 接口 Java.util.Observer.我们只要直 接使用它就可以. 我们必须 extends Java.util.Observer 才能真正使用它: 1.提供 Add/Delete observer 的方法; 2.提供通知(notisfy) 所有 observer 的方法; //产品类 可供 Jsp 直接使用 UseBean 调用 该类主要执行产品数据库 插入 更新 public class product extends Observable{ private String name; private float price; public String getName(){ return name;} public void setName(){ this.name=name; //设置变化点 setChanged(); notifyObservers(name); } public float getPrice(){ return price;} public void setPrice(){ this.price=price; //设置变化点 setChanged(); notifyObservers(new Float(price)); } //以下可以是数据库更新 插入命令. public void saveToDb(){ ..................... } 我们注意到,在 product 类中 的 setXXX 方法中,我们设置了 notify(通知)方法, 当 Jsp 表单调用 setXXX,实际上就触发了 notisfyObservers 方法,这将通知相应观察 者应该采取行动了. 下面看看这些观察者的代码,他们究竟采取了什么行动: //观察者 NameObserver 主要用来对产品名称(name)进行观察的 public class NameObserver implements Observer{ private String name=null; public void update(Observable obj,Object arg){ if (arg instanceof String){ name=(String)arg; //产品名称改变值在 name 中 System.out.println("NameObserver :name changet to "+name); } } } //观察者 PriceObserver 主要用来对产品价格(price)进行观察的 public class PriceObserver implements Observer{ private float price=0; public void update(Observable obj,Object arg){ if (arg instanceof Float){ price=((Float)arg).floatValue(); System.out.println("PriceObserver :price changet to "+price); } } } Jsp 中我们可以来正式执行这段观察者程序: <jsp:useBean id="product" scope="session" class="Product" /> <jsp:setProperty name="product" property="*" /> <jsp:useBean id="nameobs" scope="session" class="NameObserver" /> <jsp:setProperty name="product" property="*" /> <jsp:useBean id="priceobs" scope="session" class="PriceObserver" /> <jsp:setProperty name="product" property="*" /> <% if (request.getParameter("save")!=null) { product.saveToDb(); out.println("产品数据变动 保存! 并已经自动通知客户"); }else{ //加入观察者 product.addObserver(nameobs); product.addObserver(priceobs); %> //request.getRequestURI()是产生本 jsp 的程序名,就是自己 调用自己 <form action="<%=request.getRequestURI()%>" method=post> <input type=hidden name="save" value="1"> 产品名称:<input type=text name="name" > 产品价格:<input type=text name="price"> <input type=submit> </form> <% } %> 执行改 Jsp 程序,会出现一个表单录入界面, 需要输入产品名称 产品价格,点按 Submit 后,还是执行该 jsp 的 if (request.getParameter("save")!=null)之间的代 码. 由于这里使用了数据 javabeans 的自动赋值概念,实际程序自动执行了 setName setPrice 语句.你会在服务器控制台中发现下面信息:: NameObserver :name changet to ?????(Jsp 表单中输入的产品名称) PriceObserver :price changet to ???(Jsp 表单中输入的产品价格); 这说明观察者已经在行动了.!! 同时你会在执行 jsp 的浏览器端得到信息: 产品数据变动 保存! 并已经自动通知客户 上文由于使用 jsp 概念,隐含很多自动动作,现将调用观察者的 Java 代码写如下: public class Test { public static void main(String args[]){ Product product=new Product(); NameObserver nameobs=new NameObserver(); PriceObserver priceobs=new PriceObserver(); //加入观察者 product.addObserver(nameobs); product.addObserver(priceobs); product.setName("橘子红了"); product.setPrice(9.22f); } } 你会在发现下面信息:: NameObserver :name changet to 橘子红了 PriceObserver :price changet to 9.22 这说明观察者在行动了.!! 9、设计模式之 Strategy(策略) Strategy 是属于设计模式中 对象行为型模式,主要是定义一系列的算法,把这些算法 一个个封装成单独的类。 Stratrgy 应用比较广泛,比如,公司经营业务变化图,可能有两种实现方式,一个 是线条曲线,一个是框图(bar),这是两种算法,可以使用 Strategy 实现。 这里以字符串替代为例, 有一个文件,我们需要读取后,希望替代其中相应的变 量,然后输出.关于替代其中变量的方法可能有多种方法,这取决于用户的要求,所以 我们要准备几套变量字符替代方案。 首先,我们建立一个抽象类 RepTempRule 定义一些公用变量和方法: public abstract class RepTempRule{ protected String oldString=""; public void setOldString(String oldString){ this.oldString=oldString; } protected String newString=""; public String getNewString(){ return newString; } public abstract void replace() throws Exception; } 在 RepTempRule 中有一个抽象方法 abstract 需要继承明确,这个 replace 里其实 是替代的具体方法。我们现在有两个字符替代方案, 1.将文本中 aaa 替代成 bbb; 2.将文本中 aaa 替代成 ccc; 对应的类分别是 RepTempRuleOne RepTempRuleTwo public class RepTempRuleOne extends RepTempRule{ public void replace() throws Exception{ //replaceFirst 是 jdk1.4 新特性 newString=oldString.replaceFirst("aaa", "bbbb") System.out.println("this is replace one"); } } public class RepTempRuleTwo extends RepTempRule{ public void replace() throws Exception{ newString=oldString.replaceFirst("aaa", "ccc") System.out.println("this is replace Two"); } } 至此我们完成了类图的设计和程序编制.调用如下: public class test{ ...... public void testReplace(){ //使用第一套方案进行替换. RepTempRule rule=new RepTempRuleOne(); rule.setOldString(record); rule.replace(); } ..... } 实际整个 Strategy 的核心部分就是抽象类的使用,使用 Strategy 模式可以在用户 需要变化时,修改量很少,而且快速。 Strategy 和 Factory 有一定的类似,Strategy 相对简单容易理解: Strategy 适合下列场合: 1.以不同的格式保存文件; 2.以不同的算法压缩文件; 3.以不同的算法截获图象; 4.以不同的格式输出同样数据的图形,比如曲线 或框图 bar 等。 10、设计模式之 State State 的定义: 不同的状态,不同的行为;或者说,每个状态有着相应的行为。 何时使用? State 模式在实际使用中比较多,适合"状态的切换".因为我们经常会使用 If elseif else 进行状态切换,如果针对状态的这样判断切换反复出现,我们就要联想到是否可 以采取 State 模式了。 不只是根据状态,也有根据属性.如果某个对象的属性不同,对象的行为就不一样, 这点在数据库系统中出现频率比较高,我们经常会在一个数据表的尾部,加上 property 属性含义的字段,用以标识记录中一些特殊性质的记录,这种属性的改变(切换)又是 随时可能发生的,就有可能要使用 State。 是否使用? 在实际使用,类似开关一样的状态切换是很多的,但有时并不是那么明显,取决于 你的经验和对系统的理解深度。 这里要阐述的是"开关切换状态" 和" 一般的状态判断"是有一些区别的, " 一般的 状态判断"也是有 if..elseif 结构,例如: if (which==1) state="hello"; else if (which==2) state="hi"; else if (which==3) state="bye"; 这是一个 " 一般的状态判断",state 值的不同是根据 which 变量来决定的,which 和 state 没有关系.如果改成: if (state.euqals("bye")) state="hello"; else if (state.euqals("hello")) state="hi"; else if (state.euqals("hi")) state="bye"; 这就是 "开关切换状态",是将 state 的状态从"hello"切换到"hi",再切换到""bye"; 在切换到"hello",好象一个旋转开关,这种状态改变就可以使用 State 模式了。 如果单纯有上面一种将"hello"-->"hi"-->"bye"-->"hello"这一个方向切换,也不一定需 要使用 State 模式,因为 State 模式会建立很多子类,复杂化,但是如果又发生另外一 个行为:将上面的切换方向反过来切换,或者需要任意切换,就需要 State 了。 请看下例: public class Context{ private Color state=null; public void push(){ //如果当前 red 状态 就切换到 blue if (state==Color.red) state=Color.blue; //如果当前 blue 状态 就切换到 green else if (state==Color.blue) state=Color.green; //如果当前 black 状态 就切换到 red else if (state==Color.black) state=Color.red; //如果当前 green 状态 就切换到 black else if (state==Color.green) state=Color.black; Sample sample=new Sample(state); sample.operate(); } public void pull(){ //与 push 状态切换正好相反 if (state==Color.green) state=Color.blue; else if (state==Color.black) state=Color.green; else if (state==Color.blue) state=Color.red; else if (state==Color.red) state=Color.black; Sample2 sample2=new Sample2(state); sample2.operate(); } } 在上例中,我们有两个动作 push 推和 pull 拉,这两个开关动作,改变了 Context 颜色,至此,我们就需要使用 State 模式优化它。 另外注意:但就上例,state 的变化,只是简单的颜色赋值,这个具体行为是很简单 的,State 适合巨大的具体行为,因此在,就本例,实际使用中也不一定非要使用 State 模式,这会增加子类的数目,简单的变复杂。 例如: 银行帐户, 经常会在 Open 状态和 Close 状态间转换。 例如: 经典的 TcpConnection, Tcp 的状态有创建 侦听 关闭三个,并且反复转换, 其创建 侦听 关闭的具体行为不是简单一两句就能完成的,适合使用 State 例如:信箱 POP 帐号, 会有四种状态, start HaveUsername Authorized quit,每 个状态对应的行为应该是比较大的.适合使用 State 例如:在工具箱挑选不同工具,可以看成在不同工具中切换,适合使用 State.如 具 体绘图程序,用户可以选择不同工具绘制方框 直线 曲线,这种状态切换可以使用 State。 如何使用 State 需要两种类型实体参与: 1.state manager 状态管理器,就是开关,如上面例子的Context实际就是一个state manager, 在 state manager 中有对状态的切换动作。 2.用抽象类或接口实现的父类,,不同状态就是继承这个父类的不同子类。 以上面的 Context 为例.我们要修改它,建立两个类型的实体。 第一步: 首先建立一个父类: public abstract class State{ public abstract void handlepush(Context c); public abstract void handlepull(Context c); public abstract void getcolor(); } 父类中的方法要对应 state manager 中的开关行为,在 state manager 中本例就是 Context 中,有两个开关动作 push 推和 pull 拉.那么在状态父类中就要有具体处理这两 个动作:handlepush() handlepull(); 同时还需要一个获取 push 或 pull 结果的方法 getcolor() 下面是具体子类的实现: public class BlueState extends State{ public void handlepush(Context c){ //根据 push 方法"如果是 blue 状态的切换到 green" ; c.setState(new GreenState()); } public void handlepull(Context c){ //根据 pull 方法"如果是 blue 状态的切换到 red" ; c.setState(new RedState()); } public abstract void getcolor(){ return (Color.blue)} } 同样其他状态的子类实现如 blue 一样。 第二步: 要重新改写 State manager 也就是本例的 Context: public class Context{ private Sate state=null; //我们将原来的 Color state 改成了新建 的 State state; //setState 是用来改变 state 的状态 使用 setState 实现状态的切 换 pulic void setState(State state){ this.state=state; } public void push(){ //状态的切换的细节部分,在本例中是颜色的变化,已经封 装在子类的 handlepush 中实现,这里无需关心 state.handlepush(this); //因为 sample 要使用 state 中的一个切换结果,使用 getColor () Sample sample=new Sample(state.getColor()); sample.operate(); } public void pull(){ state.handlepull(this); Sample2 sample2=new Sample2(state.getColor()); sample2.operate(); } } 至此,我们也就实现了 State 的 refactorying 过程。 以上只是相当简单的一个实例,在实际应用中,handlepush 或 handelpull 的处理 是复杂的。 11、设计模式之 Facade(外观) Facade 的定义: 为子系统中的一组接口提供一个一致的界面. Facade 一个典型应用就是数据库 JDBC 的应用,如下例对数据库的操作: public class DBCompare { Connection conn = null; PreparedStatement prep = null; ResultSet rset = null; try { Class.forName( "<;driver>;" ).newInstance(); conn = DriverManager.getConnection( "<;database>;" ); String sql = "SELECT * FROM <;table>; WHERE <;column name >; = ?"; prep = conn.prepareStatement( sql ); prep.setString( 1, "<;column value>;" ); rset = prep.executeQuery(); if( rset.next() ) { System.out.println( rset.getString( "<;column name" ) ); } } catch( SException e ) { e.printStackTrace(); } finally { rset.close(); prep.close(); conn.close(); } } 上例是 Jsp 中最通常的对数据库操作办法。 在应用中,经常需要对数据库操作,每次都写上述一段代码肯定比较麻烦,需要将 其中不变的部分提炼出来,做成一个接口,这就引入了 facade 外观对象。如果以后我 们更换 Class.forName 中的<;driver>;也非常方便,比如从 Mysql 数据库换到 Oracle 数据库,只要更换 facade 接口中的 driver 就可以。 我们做成了一个 Facade 接口,使用该接口,上例中的程序就可以更改如下: public class DBCompare { String sql = "SELECT * FROM <;table>; WHERE <;column name>; = ?"; try { Mysql msql=new mysql(sql); prep.setString( 1, "<;column value>;" ); rset = prep.executeQuery(); if( rset.next() ) { System.out.println( rset.getString( "<;column name" ) ); } } catch( SException e ) { e.printStackTrace(); } finally { mysql.close(); mysql=null; } } 可见非常简单,所有程序对数据库访问都是使用改接口,降低系统的复杂性,增加 了灵活性。 如果我们要使用连接池,也只要针对 facade 接口修改就可以。 由上图可以看出,facade 实际上是个理顺系统间关系,降低系统间耦合度的一个常 用的办法,也许你已经不知不觉在使用,尽管不知道它就是 facade。 12、设计模式之 Interpreter(解释器) Interpreter 定义: 定义语言的文法,并且建立一个解释器来解释该语言中的句子。 Interpreter 似乎使用面不是很广,它描述了一个语言解释器是如何构成的,在实际 应用中,我们可能很少去构造一个语言的文法。我们还是来简单的了解一下: 首先要建立一个接口,用来描述共同的操作。 public interface AbstractExpression { void interpret( Context context ); } 再看看包含解释器之外的一些全局信息 public interface Context { } AbstractExpression 的具体实现分两种:终结符表达式和非终结符表达式: public class TerminalExpression implements AbstractExpression { public void interpret( Context context ) { } } 对于文法中没一条规则,非终结符表达式都必须的: public class NonterminalExpression implements AbstractExpression { private AbstractExpression successor; public void setSuccessor( AbstractExpression successor ) { this.successor = successor; } public AbstractExpression getSuccessor() { return successor; } public void interpret( Context context ) { } } 13、设计模式之 Visitor Visitor 定义 作用于某个对象群中各个对象的操作。它可以使你在不改变这些对象本身的情况 下,定义作用于这些对象的新操作。 在 Java 中,Visitor 模式实际上是分离了 collection 结构中的元素和对这些元素进 行操作的行为。 为何使用 Visitor? Java 的 Collection(包括 Vector 和 Hashtable)是我们最经常使用的技术,可是 Collection 好象是个黑色大染缸,本来有各种鲜明类型特征的对象一旦放入后,再取出 时,这些类型就消失了。那么我们势必要用 If 来判断,如: Iterator iterator = collection.iterator() while (iterator.hasNext()) { Object o = iterator.next(); if (o instanceof Collection) messyPrintCollection((Collection)o); else if (o instanceof String) System.out.println("'"+o.toString()+"'"); else if (o instanceof Float) System.out.println(o.toString()+"f"); else System.out.println(o.toString()); } 在上例中,我们使用了 instanceof 来判断 o的类型。 很显然,这样做的缺点代码 If else if 很繁琐。我们就可以使用 Visitor 模式解决它。 如何使用 Visitor?/ 针对上例,我们设计一个接口 visitor 访问者: public interface Visitor { public void visitCollection(Collection collection); public void visitString(String string); public void visitFloat(Float float); } 在这个接口中,将我们认为 Collection 有可能的类的类型放入其中。 有了访问者,我们需要被访问者,被访问者就是我们 Collection 的每个元素 Element,我们要为这些 Element 定义一个可以接受访问的接口(访问和被访问是互动 的,只有访问者,被访问者如果表示不欢迎,访问者就不能访问)。 我们定义这个接口叫 Visitable,用来定义一个 Accept 操作,也就是说让 Collection 每个元素具备可访问性。 public interface Visitable { public void accept(Visitor visitor); } 好了,有了两个接口,我们就要定义他们的具体实现(Concrete class): public class ConcreteElement implements Visitable { private String value; public ConcreteElement(String string) { value = string; } //定义 accept 的具体内容 这里是很简单的一句调用 public void accept(Visitor visitor) { visitor.visitString(this); } } 再看看访问者的 Concrete 实现: public class ConcreteVisitor implements Visitor { //在本方法中,我们实现了对 Collection 的元素的成功访问 public void visitCollection(Collection collection) { Iterator iterator = collection.iterator() while (iterator.hasNext()) { Object o = iterator.next(); if (o instanceof Visitable) ((Visitable)o).accept(this); } public void visitString(String string) { System.out.println("'"+string+"'"); } public void visitFloat(Float float) { System.out.println(float.toString()+"f"); } } 在上面的 visitCollection 我们实现了对 Collection 每个元素访问,只使用了一个判 断语句,只要判断其是否可以访问。 至此,我们完成了 Visitor 模式基本架构。 使用 Visitor 模式的前提 对象群结构中(Collection) 中的对象类型很少改变,也就是说访问者的身份类型很 少改变,如上面中 Visitor 中的类型很少改变,如果需要增加新的操作,比如上例中我 们在 ConcreteElement 具体实现外,还需要新的 ConcreteElement2 ConcreteElement3。 可见使用 Visitor 模式是有前提的,在两个接口 Visitor 和 Visitable 中,确保 Visitor 很少变化,变化的是 Visitable,这样使用 Visitor 最方便。 如果 Visitor 也经常变化, 也就是说,对象群中的对象类型经常改变,一般建议是, 不如在这些对象类中逐个定义操作。但是 Java 的 Reflect 技术解决了这个问题。 Reflect 技术是在运行期间动态获取对象类型和方法的一种技术。
还剩106页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 10 金币 [ 分享pdf获得金币 ] 3 人已下载

下载pdf

pdf贡献者

liukov79

贡献于2012-11-19

下载需要 10 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf