6.1 Prototype 模式
通常,我们使用 new 方式来生成类的实例。
new Something();
像这样 new 的方式来生成实例时,是必须指定类名的。但是,有时也需要 “不指定类名的前提下生成实例” 的需求。
(1) 对象种类繁多,无法将他们整合到一个类中时
此时需要要编写多个类文件来处理。
(2) 难以根据类生成实例时
生成实例的过程太过复杂,很难根据类来生成实例。通常,在想生成一个和之前用户通过操作所创建出来的实例完全一样的实例时候,会事先将用户通过操作所创建出的实例保存起来,然后在需要时通过复制来生成新的实例。
(3) 想解耦框架与生成的实例时
想要让生成实例的框架不依赖于具体的类。这时,不能指定类名来生成实例,要事先 “注册” 一个 “原型” 实例,然后通过复制该实例来生成新的实例。
这里,我们将了解不根据类来生成实例,而是根据实例来生成新实例的 Prototype 模式。Prototype 有 “原型” “模型” 的意思。在设计模式中是指根据实例原型、模型来生成新的实例。
6.2 示例程序
作用:将字符串放入方框中显示出来或是加上下划线显示出来。
Product 接口和 Manager 类属于 framework 包,负责复制实例。虽然 Manager 类会调用 createClone 方法,但是对于具体要复制哪个类一无所知。不过,只要是实现了 Product 接口的类,调用它的 createClone 方法就可以复制出新的实例。
|| Product 接口
Product 接口是复制功能的接口。该接口继承了 java.lang.Cloneable 接口。实现了该接口的类的实例可以调用 clone 方法来自动复制实例。
use 方法是用于 “使用” 的方法,由子类实现。
createClone 方法用于复制实例的方法。
/**
* 声明了 use 和 createClone 接口
*/
public interface Product extends Cloneable{
void use(String s);
Product createClone();
}
|| Manager 类
Manager 类使用 Product 接口来实现复制实例。
showcase 字段保留了 “名字” 和 “实例” 之间的对应关系。
register 方法将接收到的 “名字” 和 “Product 接口” 注册到 showcase 中。
注意,在 Manager 中并没有写明具体的类名,仅仅使用了 Product 这个接口名。这是非常重要的,因为一旦在类中使用到了别的类名,就意味着该类与其他类紧密地耦合在了一起。
/**
* 调用 createClone 方法复制实例的类
*/
public class Manager {
private HashMap<String, Product> showCase = new HashMap<>();
public void register(String name, Product product) {
showCase.put(name, product);
}
public Product create(String name) {
Product product = showCase.get(name);
return product.createClone();
}
}
|| MessageBox 类
MessageBox 类实现了 Product 接口。decochar 字段中保存的是像装饰方框那样环绕着字符串的字符。use 方法会使用 decochar 保存的字符把要显示的字符串框起来。
createClone 方法用于复制自己,内部所调用的 clone 方法是 Java 语言中定义的方法。在进行复制时,原来实例中的字段也会被复制到新的实例中。注意因为我们实现了 Cloneable 接口,否则在运行时会抛出 CloneNotSuppotedException 异常。
public class MessageBox implements Product {
private char decochar;
public MessageBox(char decochar) {
this.decochar = decochar;
}
/**
* *********
* * Hello *
* *********
* @param s
*/
@Override
public void use(String s) {
int length = s.toCharArray().length;
for (int i = 0; i < length + 4; i++) {
System.out.print(decochar);
}
System.out.println();
System.out.println(decochar + " " + s + " " + decochar);
for (int i = 0; i < length + 4; i++) {
System.out.print(decochar);
}
System.out.println();
}
@Override
public Product createClone() {
try {
return (Product)clone();
} catch (CloneNotSupportedException e) {
System.out.println("不支持的 Clone 类型");
}
return null;
}
}
|| UnderlinePen 类
UnderlinePen 类的实现与 MessageBox 几乎完全相同。
public class UnderlinePen implements Product {
private char ulchar;
public UnderlinePen(char ulchar) {
this.ulchar = ulchar;
}
/**
* "Hello"
* ~~~~~
* @param s
*/
@Override
public void use(String s) {
int len = s.toCharArray().length;
System.out.println("\"" + s + "\"");
System.out.print(" ");
for (int i = 0; i < len; i++) {
System.out.print(ulchar);
}
System.out.println();
}
@Override
public Product createClone() {
try {
return (Product) clone();
} catch (CloneNotSupportedException e) {
System.out.println("不支持的 Clone 类型");
}
return null;
}
}
|| Main 类
Main 类首先生成了 Manager 的实例。然后,在实例中注册了 UnderlinePen 的实例以及 MessageBox 实例。
public class Main {
public static void main(String[] args) {
Manager manager = new Manager();
UnderlinePen upen = new UnderlinePen('~');
MessageBox mbox = new MessageBox('*');
MessageBox sbox = new MessageBox('/');
manager.register("strong message", upen);
manager.register("warning box", mbox);
manager.register("slash box", sbox);
// 生成
Product p1 = manager.create("strong message");
p1.use("Hello, world.");
Product p2 = manager.create("warning box");
p2.use("Hello, world.");
Product p3 = manager.create("slash box");
p3.use("Hello, world.");
}
}
输出:
"Hello, world."
~~~~~~~~~~~~~
*****************
* Hello, world. *
*****************
/
/ Hello, world. /
/
6.3 Prototype 模式中的登场角色
- Prototype (原型)
Product 角色负责定义用于复制现有实例来生成新实例的方法。在示例程序中,由 Product 接口扮演此角色。 - ConcretePrototype(具体的原型)
ConcretePrototype 角色负责实现复制现有实例并生成新实例的方法。在示例程序中,由 MessageBox 类和 UnderlinePen 类扮演此角色。 - Client(使用者)
Client 角色负责使用复制实例的方法生成新的实例。在示例程序中,由 Manager 类扮演此角色。
6.4 拓展思路的要点
|| 类名是束缚吗
面向对象编程的目标之一,即 “作为组件复用”。
在代码中出现要使用的类的名字并非总是坏事。不过,一旦在代码中出现要使用的类的名,就无法与该类分离开来,也就无法实现复用。虽然可以通过替换源代码或是改变类名来解决这个问题,但以 Java 来说,重要的是当手边只有 class 文件时,该类能否被复用。即使没有 Java 文件也能复用该类才是关键。
当多个类必须紧密结合时,代码中出现这些类的名字是没有问题的。但是如果那些需要被独立出来作为组件复用的类的名字出现在代码中,那就会出现问题。