Factory模式与Prototype模式的异同

设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了重用代码、让代码更容易被他人理解、保证代码可靠性。 毫无疑问,设计模式于己于他人于系统都是多赢的,设计模式使代码编制真正工程化,设计模式是软件工程的基石,如同大厦的一块块砖石一样。项目中合理地运用设计模式可以完美地解决很多问题,每种模式在现实中都有相应的原理来与之对应,每种模式都描述了一个在我们周围不断重复发生的问题,以及该问题的核心解决方案,这也是设计模式能被广泛应用的原因。

原型模式与工厂模式的定义,本文不想在这讲太多,本文主要想在这讲一下对原型模式的一些误解--将原型模式等价于工厂模式。

为什么会产生这种误导呢?其实也不是我们的错,关键在于设计模式这本书以及网上的其它资料很喜欢将原型和工厂方法进行比较,从而导致我们误解了原型引入的本质意义。

按博主的理解,原型引入的根本原因就是在于它可以利用一个原型对象(指的是实例,而非类),快速地生成一批和原型对象一样的实例。举个例子来说,你有一个类A的实例a (A a=new A()),现在你想生成一个和a一样的实例b,那么,按照原型的定义,你应该可以这样做b=a.clone()。这样,你就可以得到一个和a一模一样的实例b(即a和部b的数据成员的值完全一样)。

上面是原型的一个简单说明,那么引入原型有什么好处呢?按博主的理解,就是在于:你如果要生成一大批很相像的类的实例时,省得每次去做重复的赋值工作。再举个例子,如果你有一个类A,它有十个成员变量,现在你打算生成100个A的实例,而这些实例的变量值大部分相同(比如说七个相同),只有一小部分不一样(比如说三个),那么如果没有Prototype,那么你就得每次New一个A的对像,然后赋值,这样,你要重复100次同样的七个变量的赋值工作,显然,这样很麻烦。现在你有了原型,那么问题就简单了,你只要生成一个A的实例,再通过clone来生成其它的实例,然后再一一修改其它实例不同的地方。

可能这么讲,大家不信,那下面,再让我们来看看Java中活生生的原型应用。

学过Java的人都知道,在Java中,有一个clone()函数,这个函数的功能,就是返回一个和当前调用它的对象一样的实例。那么Java中为什么要引入这个函数呢?

在【Think in Java】一书中,作者如是解释:
“如果,你要将一个对象的引用作为参数传进去,但又不希望函数改变对象的值,那么,你该怎么办?由于在Java中对象没有像C++那样的Const修饰符,所以,为了实现这个功能,Java中引入了clone函数,使得你将对象的引用作为参数传进函数时,这个函数可以调用该对象的Clone方法生成该对象的一份拷贝,从而达到不修改原对象的目的。”

之所以用上面这么多篇幅来讲述原型本质,目的就在于希望各位不要把原型的功能与它的意义给混了,以致于当真正要使用原型来解决问题时,却不知可以使用它。

好了,上面说了原型的本质意义。那为什么很多资料喜欢将原型同工厂模式进行比较呢?不知是不是巧合,虽然原型引入的初衷是像上面所说,但它实现起来,却又完全可以达到工厂模式的效果。而且,用起来甚至比工厂模式更方便、灵活。对于工厂模式与原型模式在功能上的这点巧合,也许是因为本来工厂模式和原型模式都是创建型模式,这样,它们的基本功能都能生成对象,因而使得原型模式在功能上可以代替工厂模式。

对这两种模式在功能上的相同点,程序员2001年第11期杂志上有一篇”非鱼“写的文章,作者理解得非常巧妙,即:如果你将工厂模式的UML图对折,你得到的就是Prototype原型的UML图。有兴趣比较这两种模式的朋友,可以去参考这篇文章。

接下来,让我们在实现机制上来看看原型模式为什么可以实现工厂模式的功能(本文只限于Java语言)。在Java中,对于原型的实现,其实根本不用我们做,在object类中早就定义了一个clone函数,而这个函数,就使得我们可以动态地生成对象的当前拷贝。既然这样,那么让我们来看看,如果要实现工厂模式的功能,我们该如何使用原型模式为做到呢?

工厂模式实现生产产品的功能,关键是利用了继承的特性。也就是说,你生成的产品,一定是由同一个抽象产品类派生出来的。所以,在工厂模式下,你如果要生成一类产品,就要引入一个抽像产品类,然后再由它派生出具体产品。同样,在原型模式中,你完全可以同样定义一个这样的“抽象产品--具体产品”层次,再利用具体产品本身的clone功能来产生具体产品本身。从而达到实现工厂模式功能的目的。可能说到这,大家有点糊涂了。实际上,在原型模式中,每个具体产品就扮演了工厂模式里的具体工厂的角色(为什么会这样,其实很简单,因为,每个具体产品都具有生成自己拷贝的功能?从这种意义上讲,难道这不正是工厂的作用吗?)。

另外,要在Java中利用原型模式实现工厂模式的功能,则更为简单,因为object已经为我们实现了clone函数,且对于clone方法,Java中默认是:如果A是父类且A实现了clone函数,B是A的子类,则B不用实现clone函数,它只要调用父类的clone函数,Java就会在运行时动态地为我们生成正确的B的对象。理解这点的关键在于,所有类实现的clone操作都是调用object的clone方法。这也就是说,上面所说的父类A根本就不用自己实现clone方法,而仅仅是调用父类(object)的clone方法而已。

好,到了这,读者也许又有疑问了,既然所有的clone操作都是由object实现的,而在Java中所有的自定义类默认都是由object派生而来,那这样的话,应该所有的类都自动就具有了clone自己的能力?
确实,如果object不将它的clone函数声明为protect的话,情况的确如此。但Java为了安全方面的原因,所以没有将clone方法公开,而是声明为保护类型,这样的话,子类是不可以直接调用object类的clone方法的,而必须做到如下两点:

  • 1.必须实现Cloneable接口;
  • 2.必须声明一个clone方法,来调用object的clone函数;

Java在调用父类的clone函数时,都会在运行时动态地进行检查,如果发现调用的类不符合上面的任何一点,则会抛出一个异常。
明白了上面的原因,那么如果我们希望某个类具备clone自身的能力,那么,我们可以这样做:

  • 1.直接按上面所说,自己实现clone操作;
  • 2.声明一个抽象父类,实现上面的clone操作并将它声明为公开方法,再由此类派生出子类,这样,所有的子类只要调用父类的clone方法,就能够正确地拷贝自己。

通常,我们都是使用第一种方式,但在我们现在讨论的如何用原型模式实现工厂模式的功能的问题中,我们最好是采用第二种方式。

最后,让我们通过具体的代码来看看如何用Prototype模式实现工厂模式的功能。

问题:

现有两类产品 1-Ram,2–Cpu,现在要生成具体的产品
MacRam,MacCpu和WinRam,WinCpu.

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
/*
*A:Abstract
*C:Concrete
*/

/* 定义抽象产品Ram的类 APrototypeRam
* 同时它也是抽象工厂
*/

abstract class APrototypeRam implements Cloneable {
public Object clone() {
Object o=null;
try {
o=super.clone();//调用父类,即Object的clone()
}
catch(CloneNotSupportedException e) {
System.err.println("APrototypeRam is not cloneable!");
}
return o;
}
}

/* 定义抽象产品Ram的类APrototypeProductCpu
* 同时它也是抽象工厂
*/

abstract class APrototypeCpu implements Cloneable {
public Object clone() {
Object o=null;
try {
o=super.clone();//调用父类,即Object的clone()
}
catch(CloneNotSupportedException e) {
System.err.println("APrototypeCpu is not cloneable!");
}
return o;
}
}

/* 定义具体产品MacRam的类CPrototypeMacRam
* 同时它也是具体工厂
*/

class CPrototypeMacRam extends APrototypeRam{
public String toString() {
return "MacRam";
}
}

/* 定义具体产品WinRam的类CPrototypeWinRam
* 同时它也是具体工厂
*/

class CPrototypeWinRam extends APrototypeRam {
public String toString() {
return "WinRam";
}
}

/* 定义具体产品MacCpu的类CPrototypeMacCpu
* 同时它也是具体工厂
*/

class CPrototypeMacCpu extends APrototypeCpu{
public String toString() {
return "MacCpu";
}
}

/* 定义具体产品WinCpu的类CPrototypeWinCpu
* 同时它也是具体工厂
*/

class CPrototypeWinCpu extends APrototypeCpu{
public String toString() {
return "WinCpu";
}
}

/* 客户端,使用CPrototypeRam和CPrototypeCpu生成如下产品
* MacRam,MacCpu,WinRam,WinCpu
*/

public class Prototype {
public static void main(String[] args) {
/*
* 在生成产品之前,先生成原型产品,以便后面利用它们成批生产相同产品
* 其作用等价于产品工厂
*/
CPrototypeMacRam prototypeMacRam=new CPrototypeMacRam();
CPrototypeWinRam prototypeWinRam=new CPrototypeWinRam();
CPrototypeMacCpu prototypeMacCpu=new CPrototypeMacCpu();
CPrototypeWinCpu prototypeWinCpu=new CPrototypeWinCpu();

CPrototypeMacRam MacRam=(CPrototypeMacRam)prototypeMacRam.clone();
CPrototypeWinRam WinRam=(CPrototypeWinRam)prototypeWinRam.clone();
CPrototypeMacCpu MacCpu=(CPrototypeMacCpu)prototypeMacCpu.clone();
CPrototypeWinCpu WinCpu=(CPrototypeWinCpu)prototypeWinCpu.clone();
System.out.println("打印原型产品与它的克隆产品与比较异同!");
System.out.println("prototypeMacRam:"+prototypeMacRam+" Cloned:"+MacRam);
System.out.println("prototypeWinRam:"+prototypeWinRam+" Cloned:"+WinRam);
System.out.println("prototypeMacCpu:"+prototypeMacCpu+" Cloned:"+MacCpu);
System.out.println("prototypeWinCpu:"+prototypeWinCpu+" Cloned:"+WinCpu);

}
}

通过上面代码,我们可以清楚地看到,用Prototype模式实现工厂模式更为简单,如果再配上原型管理器的话,那么Prototype模式则会变得更为灵活。但同时,我们也发现,使用原型模式时,有一个不足之处,即在客户端代码里,我们必须显式进行类型转换,这样可能导致错误。为了改正这一点,我们可以使用真正的工厂模式将Prototype模式再封装一遍。对工厂模式的这项功能,恐怕,Prototype原型模式就无能为力了。

总之,工厂模式和原型模式虽然在引入目的上不同,但在实现上,原型模式可以实现工厂模式同样的功能。但读者也不要因为这样,而将两者混为一体,因为,反过来,在将原型模式作为生成本身拷贝的这项功能使用时,工厂模式根本无法取代它。