模式的本质就是不断重复出现的问题的可重用解决方案。一个完整的模式提供使用或者不使用该解决方案的理由,使用解决方案的结果,以及怎样实现该解决方案的建议。本文中模式总结仅描述问题的本质及其解决方案。这是最后一组与创建对象相关的模式。本文将重点讲解动态决定实例化哪个类,或者一个对象把职责委托给哪些对象。这些模式会告诉我们怎样组织和封装这些决定。该文将讨论原型模式(Prototype)。通过给类指定每种将要被创建的对象的原型实例,原型模式(Prototype)使得该类可以创建那些实现已知接口的对象。
假设你正在设计一个CAD(计算机辅助设计)程序,它允许用户根据图形符号画板来作图。该程序具有一套核心的内置图形符号。但是,使用这个程序的人们往往具有不同的特殊需求。那么这套核心图形符号将不能满足人们的这种特殊需求。这些人就希望程序具有额外的图形符号来适合他们的需求。由于这个程序的大多数用户都具有特殊的需求,所以程序必须尽可能的提供额外的图形符号集,使得用户可以根据他们的需要把它们添加到程序中去。
这种需求将会引起一个问题:怎样提供那些额外图形符号的画板。你可以很容易的组织如此之多的图形符号,包括核心的和额外的,它们都继承一个共同的祖先类。这给你的作图程序使用一致的方式来操作图形符号对象提供了支持。但是它并没有解决程序怎样创建对象的问题。创建诸如此类的对象通常比简单的实例化一个类要复杂得多。它可能还包括数据属性值的设定,或结合其他对象来组成一个复合对象。
一个解决方案就是给作图程序提供预先创建好的对象,然后把它们作为创建类似对象的原型。把对象用作原型,这就要求它们具有一个方法,一般称之为clone。clone方法返回一个对象,它是原始对象的一个新的拷贝。下面的类图描述了该方案是怎样组织的:
图1 Symbol原型
作图程序维护一组用作原型的Symbol对象。通过克隆这些原型对象,作图程序可以使用Symbol对象。SymbolBuilder创建Symbol对象并且向作图程序进行注册。
所有的Java类都从Object类那里继承一个叫做clone的方法。一个对象的clone方法返回该对象的一个拷贝。clone方法只有在对象类被授权的情况下才进行拷贝。如果一个类授权给它的实例可以克隆自己,那么该类必须实现Cloneable接口。因为Symbol类实现了Cloneable接口,所以作图程序可以克隆Symbol对象。并且作图程序管理这些Symbol对象并把它们组合成图。
一般来说,通过给类指定每种将要被创建的对象的原型实例,原型模式(Prototype)使得该类可以创建那些实现已知接口的对象。因此,克隆原型实例就可以创建新对象。
下图描述了一般情况下的原型模式(Prototype)的组织结构:
图2 原型模式(Prototype)
下面描述原型模式(Prototype)中的这些类和接口所扮演的角色:
l Client
对原型模式(Prototype)的目的来说,client类表示程序的其他部分。client类需要创建它不了解的对象。Client类具有一个方法,可以调用它来给client对象的原型对象集合增加新的原型对象。在上图中,该方法的名称为registerPrototype。但是,在实际的实现中,反映被原型化的对象集合的名字被叫做为registerSymbol可能会更恰当一些。
l Prototype
扮演Prototype角色的类实现PrototypeIF接口,并且可以被client克隆而达到被实例化的目的。扮演这种角色的类通常是抽象类,且它具有许多具体的子类。
l PrototypeIF
所有的原型对象都要实现PrototypeIF接口。client类通过这个接口与原型对象进行交互。这种角色的接口应该扩展Cloneable接口,这样所有实现该接口的对象才能够被克隆。
l PrototypeBuilder
PrototypeBuilder类对应于那些被实例化以给client对象提供原型对象的任何类。像这样的类应该具有这样一个名字,它必须能够表示它所能创建的原型对象的类型,例如SymbolBuilder。PrototypeBuilder对象创建Prototype对象。它把每次新创建的Prototype对象传递给Client对象的registerPrototype方法。
该模式的示例代码是基于一个不同的情形。假设你正在编写一个交互角色的游戏比赛。也就是说,该游戏允许用户与模拟的人物角色进行交互。其中,游戏的一个需求就是玩家越来越厌烦与同一个角色进行交互,而希望与一个新角色进行交互。由于这个原因,你还需要给游戏开发一个附加部分,它由一些预先生成的角色所组成,并且需要开发一个程序来生成这些附加的角色。
游戏中的角色都是一些相关的类的实例,例如Hero,Fool,Villain和Monster。是什么使得同一个类的实例之间相互不同呢?答案就是给这些实例设置不同属性值,例如表示它们的图像,高度,宽度,智力和敏捷。
下面的类图描述了游戏中所包含的部分类:
图3 原型模式示例
清单1列出了CharacterIF接口代码,该接口扮演PrototypeIF角色。
清单1:CharacterIF接口
public interface CharacterIF extends Cloneable {
public String getName() ;
public void setName(String name) ;
public Image getImage() ;
public void setImage(Image image) ;
public int getStrength() ;
public void setStrength(int strength) ;
...
} // interface CharacterIF
下面是Character类代码,该抽象类扮演PrototypeIF角色。
清单2:Character类
public abstract class Character implements CharacterIFloneable {
...
/**
* Override clone to make it public.
*/
public Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException e) {
// This should never happen because this class implements
// Cloneable.
throw new InternalError();
} // try
} // clone()
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Image getImage() { return image; }
public void setImage(Image image) { this.image = image; }
...
} // class Character
这个类的大部分代码都是简单的存取方法。比较不清楚地方法就是clone方法。所有的对象都从Object类那里继承一个clone方法。因为这个clone方法不是公共的,所以character类必须用一个public声明来覆盖它,这只是使得它可以被其他的类访问。
清单3是Hero类的源代码,这个类所扮演的其中一个角色就是Prototype:
清单3:Hero类
public class Hero extends Character {
private int bravery;
...
public int getBravery() { return bravery; }
public void setBravery(int bravery) { this.bravery = bravery; }
} // class Hero
Monster类与Hero类相似。
下面就是CharacterManager类代码,它扮演Client角色:
清单4:CharacterManager类
public class CharacterManager {
private Vector characters = new Vector();
...
/**
* Return a copy of random character from the collection.
*/
Character getRandomCharacter() {
int i = (int)(characters.size()*Math.random());
return (Character)((Character)characters.elementAt(i)).clone();
} // getRandomCharacter()
/**
* Add a prototypical object to the collection.
*/
void addCharacter(Character character) {
characters.addElement(character);
} // addCharacter(Character)
...
} // class CharacterManager
清单5列出了CharacterLoader类的代码,它来完成PrototypeBuilder角色:
清单5:CharacterLoader类
/**
* This class loads character objects and adds them to the
* the CharacterManager.
*/
class CharacterLoader {
private CharacterManager mgr;
/**
* Constructor
* @param cm The CharacterManager that this object will work with.
*/
CharacterLoader(CharacterManager cm) {
mgr = cm;
} // Constructor(CharacterManager)
/**
* Load character objects from the specified file.
* Since failure only affects the rest of the program to the extent
* that new character objects are not loaded, we need not throw any
* exceptions.
*/
int loadCharacters(String fname) {
int objectCount = 0; // The number of objects loaded
// If construction of the InputStream fails, just return
try {
InputStream in;
in = new FileInputStream(fname);
in = new BufferedInputStream(in);
ObjectInputStream oIn = new ObjectInputStream(in);
while(true) {
Object c = oIn.readObject();
if (c instanceof Character) {
mgr.addCharacter((Character)c);
} // if
} // while
} catch (Exception e) {
} // try
return objectCount;
} // loadCharacters(String)
} // class CharacterLoader