8.1 动机
在软件系统中,有些对象的创建过程较为复杂,而且有时候需要频繁创建,原型模式通过给出一个原型对象来指明所要创建的对象的类型,然后用复制这个原型对象的办法创建除更多同类型的对象。其使用于:当一个系统应该独立于它的产品创建、构成和表示时;当要实例化的类是在运行时指定时,例如通过动态装载;为了避免创建一个于产品层次平行的工厂类层次时;当一个类的实例只能有几个不同状态组合中的一种时,建立相应数目的原型并克隆它们可能比每次用合适的状态手工实例化该类更方便一些。
8.2 模式定义
用一个已经创建了的原型实例指定其创建对象的种类,并且通过拷贝该原型对象创建一个和目标原型相同或相似的新对象。
8.3 案例
完成史莱姆的分裂转换,即大型史莱姆转换成普通史莱姆,其中两只转换后普通史莱姆互不关联(深拷贝),普通史莱姆转换成小型史莱姆,其中四只转换后的小型史莱姆共享血量(浅拷贝)。
类图如下:
代码:
运行截图:
l模拟大型史莱姆分裂成两只普通史莱姆,并且尝试着攻击其中一只普通史莱姆
l模拟普通史莱姆分裂成四只小型史莱姆,并且尝试着攻击其中一只小型史莱姆
8.4 原型模式在主流框架中的应用
①原型模式在JDK中的应用
提起原型模型在JDK中的应用,很容易就让人联想到ArrayList中的clone方法,源码如下:
通过其源码注释“Returns a shallow copy of this ArrayList instance. (The elements themselves are not copied.”我们可以得知,其采用的是浅拷贝的方法,元素本身并不被复制,如果要实现深克隆,则需要用户自己设计具体算法,常用的方法为使用序列化来操作。对此做一个简单的小实验:
运行结果:
通过此小实验可以得知,JDK中关于ArrayList的clone方法的确是使用了浅拷贝。
② 原型模式在spring中的应用
通过查阅资料可以得知,spring bean默认是单例模式,当设置为prototype模式时,对于原型(prototype)bean来说当每次请求到来时直接实例化新的bean,没有缓存以及从缓存查的过程,因此其中涉及到了原型模型的使用,将调用过程整理如下:
对应的部分源码如下:
8.5 原型模式的扩展分析
①使用原型模式进行克隆的注意项:
在本案例中,如果对史莱姆的原型进行克隆,也就是分裂成不同的对象,而不同的史莱姆型号会对应着不同的技能,假设大型史莱姆是撞击,普通史莱姆是喷射,小型史莱姆是自爆,则简单地进行深克隆以及浅克隆可能会引起异常。因此,对此的解决办法便是原型模式结合状态模式使用,状态模式是指在软件构建过程中,某些对象的状态如果发生改变,其行为也会随之发生变化,使用状态模式将能允许一个对象在其内部状态发生改变时改变它的行为,从而使对象看起来似乎修改了其行为。修改后的类图如下:
② 带抽象接口的原型模式:
在面向对象语言Java中,有一个Cloneable接口,只要实现了该接口,便也完成了对原型模式的定义。对此做一个小小的实验,如下:
运行结果:
③ 带原型管理器的原型模式:
通过上述我们可以得知:需要创建不同类型的对象,将不可避免地需要调用不同的对象来进行扩展,如果对象的数量级很庞大,则这种方式将会存在很多不便之处。因此,解决办法便是添加一个原型管理器PrototypeManager类,用户可以通过该管理器获取不同类型的原型模型,其中对于prototypeManager对象的使用则需要结合单例模式,对此做一个小小的实验:
运行结果如下:
由运行结果我们可以得知,使用原型管理器则采用的是浅拷贝。
8.6 原型模式优缺点分析
优点:
- 在面向对象语言Java中,其自带的原型模式是基于对内存二进制流的复制,因此其在性能上就会比直接new一个对象更加优良;
- 使用深克隆可以保存对象的状态,通过原型模式创建新的与之一模一样的对象,简化了创建对象的过程,在使用时仅需调用其深克隆方式即可;
- 如果创建的新对象内部数据比较复杂且多变,原型模式创建对象的效率可能会高很多;
- 原型模式不存在额外的等级结构——原型模式不需要额外的工厂类。
缺点:
- 为实现原型模式,每一个类都需要配置一个 clone 方法,以实现浅克隆或者是深克隆;
- clone函数本身违背了设计模式中的开闭原则:当要对类进行改造时需要修改相应的代码;
- 深克隆函数实现复杂,需要对每一层对象对应的属性及方法进行克隆,不仅难度系数大,而且如果一个类结构过于复杂,将会出现“少克隆”情况(指由于粗心而少克隆了某些重要的属性和方法)。因此,在一个系统中如果要实现原型模式,深克隆和浅克隆要选择得当,否则不仅不利于创建过程,反而会加重系统负担。