原型模式( Prototype Pattern )
前面的几种模式中,我们使用了不同的构造方法(各种 Factory 或者 Builder )去代替或者说掩盖 Java 语言之中“ new ”这个操作来创建对象实例。 Java 中要创建一个新的对象并不一定只能靠“ new ”这个关键字的,我们还有“ clone() ”。
在接触原型模式之前,我们先来了解一下克隆一些知识:
1. clone() 方法在 Java 中从 Object 类开始就具备,并且作为原生( Native )方法出现。它默认是 protected 的,因此在被子类提升可见性之前,无法被外界使用。
2. 所有需要进行克隆操作的类都必须实现 Cloneable 接口,这个接口与 Serializable 接口一样, 没有任何需要实现的方法,仅仅是作为一个标示。
3. 所有数组都实现了 Cloneable 接口,并且已经提升了 clone() 方法的可见性至 public ,换句话说数组对象是可以直接调用 clone() 方法的。
4. 克隆分为浅拷贝和深拷贝两种,浅拷贝的操作基本上可以理解为只拷贝存储于栈中的内容,包括对象中简单类型的数据、指向其他复杂对象的指针等,但是不会将指向的复杂对象也拷贝一次。
目的:
用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
场景:
大话西游中唐僧说到:“人是人他妈生的,妖是妖他妈生的,人妖自然是人妖他妈生的咯”,那 Zerg 众多可爱……可怕的怪兽们都是谁生的?
早上 9 : 00 , Kerrigan 从床上起来,看见基地里面一片欣欣向荣的景象觉得心情特别舒畅:一群 Drone 在卖力的挖水晶搬气体, Zergling 、 Hydralisk 们在精神抖擞的踏着正步( BUG : Hydralisk 貌似很难走正步),就连基地刚刚分泌出来的 3 只 Larva (虫族的幼虫)都在屁颠屁颠的使劲蹦跶。
Kerrigan 也开始了她虫族女王的工作:看了看水晶和气体存量,指着一只 Zergling 对 Larva 说:你变成它吧,再指着另外一只 Hydralisk 对 Larva 说:你就变成它吧……
分析:
如果说 Zergling 的存在是为了锋利的抓牙, Hydralisk 的存在是为了强腐蚀性的毒液,那 Larva 生命的唯一意义就是变成别人,它可以说就是 Zerg 们“它妈”。用一句 OOP 的话来说:每个 Zerg 都是从 Larva 派生出来的, Larva 有一个 clone 方法,自然从 Larva 派生出来的对象就具备了一个 clone 方法,它的作用就是根据已有对象( prototype )去生产出一个新的对象。
我们可以用下面代码来模拟“ Kerrigan 指着一只 Zergling 对 Larva 说:你变成它”这个过程:
图 5.1 原型模式的 UML 图
刚刚说了, Larva 存在的唯一目的就是为了变成别人,所以它不具备任何的 attack() 、 patrol() 方法,只有一个 clone() 方法,并且是所有 Zerg 兵种的父类。在这里为了演示需要,给它赋予了 3 个额外的方法 getName() 、 setName() 和 toString() ,以便区分各个子类。
|
接着,我们建立 Zergling 和 Hydralisk 两个子类,在本次演示中,我们只希望知道 Zerg 是怎么繁殖的,而不需要去战斗,所以子类写的非常简单,仅仅赋予它们名字的差别:
|
到此为止,准备工作还差最后一步就要完成了。要使用原型模式去生产产品,自然就要先有一个原型对象,原型对象怎么来的 Prototype 不管,是 new 出来的、由别的工厂建造的、还是实现写在常量中的标本都可以。我个人觉得使用常量标本更加合理一些,请看下面代码:
|
在上述代码中我们建立了一个常量模版数组: HATCH ,里面有 Zergling 各种兵种的模版,要生产具体兵种的时候,只需要在模版中选择适合的原型( prototype )进行克隆。这样的具备以下特点:
1. 继承对象的属性值:其他的构造模式中,对象具备什么属性值(譬如演示中的 Name 属性),是由类构造时赋值所决定的,一般来说工厂生产出来的产品属性值都具备一致性。而原型模式生产出来的产品,属性值来源于原型对象而不是类,当前对象的属性值是什么样子,克隆对象的属性值就是什么样子,这点是 Prototype 模式与其他模式使用场景上最大的差异。
2. 运行时修改产品列表: Prototype 模式不依赖工厂,只需要拿到实例即可创建新的对象,这点比其他模式更为灵活。
3. 绕过对象构造:原型模式生产对象的过程中,本质上讲是一个内存复制的过程,因此它是不会调用对象的构造函数的。
展示一下程序的最终运行结果:
图 5.2 克隆的兵种
总结:
Prototype 模式中实现起来最困难的地方就是内存复制操作,所幸在 Java 中提供了 clone() 方法替我们做了绝大部分事情。在其他语言之中,一种比较简单的方法是可以考虑使用序列化技术( Serilization )来完成对象的复制。
在 Java 语言中 clone() 方法完成的是浅拷贝的克隆,如果需要深拷贝,也可以考虑使用序列化来完成,当然这样比起 Native 的 clone() 方法来说,效率差了很多,所以更加推荐的方法是针对类中包含的复杂对象情况,重写 clone() 方法,多次调用父类的 clone() 来完成,虽然要多写不少代码,但是保证了效率。