设计模式 - 原型模式
原型模式(Prototype),在制造业种通常是指大批量生产开始之前研发出的概念模型,并基于各种参数指标对其进行检验,效果达到了质量要求,即可参照这个原型进行批量生产。即,原型模式可以用对象创建对象,而不是用类创建对象,以此达到效率的提升。
举个栗子,类似于打印机和复印机的区别:
- 第一份打印出来的原文稿,我们称之为“原型文件”
- 对于复印过程,我们称之为“原型拷贝”
原型模式对于 非常复杂初始化过程的对象,或者是 需要消耗大量资源 的情况下,原型模式是更好的选择。
目录
一、以空战游戏为例
1. 敌机类 EnemyPlane 代码:
2. 怎样创建500台敌机
1) for循环批量生产敌机
2) 懒加载依然有性能问题
3) 细胞分裂
3. 复杂对象的克隆 - 深拷贝、浅拷贝
4. 克隆的本质
一、以空战游戏为例
假设我们设计一个空战游戏的程序,
为了简单,我们设定游戏为单打,也就是说主角飞机只有一驾,而敌机有很多驾,而且可以在屏幕上垂直向下移动来撞击注解飞机,具体是怎样实现的呢?其实比较简单,就是程序不停改变坐标并且在画面上重绘而已。由浅入深,我们 先试着写一个敌机类。
Tips:空战游戏中的主角如果是单个实例的话,其实就用到单例模式了。可以参考: 设计模式 - 单例模式-CSDN博客,本文只关注可以有多个实例的敌机。
1. 敌机类 EnemyPlane 代码:
public class EnemyPlane {private int x; // 敌机横坐标private int y = 0; // 敌机纵坐标public EnemyPlane(int x) { // 构造器this.x = x;}public int getX() {return x;}public int getY() {return y;}public void fly() { // 让敌机飞y++; // 每调用一次,敌机飞行时纵坐标+1}
}
纵坐标固定为0,由于敌机一开始是从顶部飞出去的。
只有getter没有setter,也就是只能在初始化时确定好敌机的横坐标x,之后不允许改了
2. 怎样创建500台敌机
我们想让敌机向雨点一样不断下落,首先需要实例化500驾敌机。
1) for循环批量生产敌机
这样做法看似没有问题,实际上效率非常低。
游戏画面不可能同时出现500驾敌机,而且在游戏未开始的时候就加载了500驾,不仅使加载速度变慢、也是对有限内存资源的一种浪费。
public class Client {public static void main(String[] args) {List<EnemyPlane> enemyPlanes = new ArrayList<EnemyPlane>();for (int i = 0; i < 500; i++) {// 此处于随机纵坐标处出现敌机EnemyPlane ep = new EnemyPlane(new Random().nextInt(200));enemyPlanes.add(ep);}}
}
那么,到底什么时候才去构造敌机,-- 当然是懒加载了
按照地图坐标,屏幕滚动到某一点时才实时构造敌机,就解决问题了。
2) 懒加载依然有性能问题
主要原因在于,“new” 关键字进行的基于类的实例化过程,每驾敌机都进行全新构造的做法是不合适的,其代价是耗费更多的CPU资源。
尤其大型游戏中,很多个线程不停运转着,CPU资源本身就非常宝贵,此时如果进行大量的类构造与复杂的初始化工作,必然会造成游戏卡顿、甚至会造成系统无响应
3) 细胞分裂
硬件永远离不开优秀的软件,我们绝不允许以糟糕的软件设计对硬件发起挑战。
既然循环第一次之后已经实例化好了一个敌机原型,那么之后又何必去重复这个构造过程呢?敌机对象是否能像细胞分裂一样自我复制呢?要解决这个问题,原型模式是最好的解决方案了。
1)重构敌机类,支持原型拷贝
让敌机类EnemyPlane实现了java.lang包中的克隆接口Cloneable,并在实现方法中调用了父类Object的克隆方法,省去了由类而生的再造过程。
public class EnemyPlane implements Cloneable {private int x; // 敌机横坐标private int y = 0; // 敌机纵坐标public EnemyPlane(int x) { // 构造器this.x = x;}public int getX() {return x;}public int getY() {return y;}public void fly() { // 让敌机飞y++; // 每调用一次,敌机飞行时纵坐标+1}// 此处开放setX,是为了让克隆后的实例重新修改横坐标public void setX(int x) {this.x = x;}// 重写克隆方法@Overridepublic EnemyPlane clone() throws CloneNotSupportedException {return (EnemyPlane)super.clone();}
}
至此,克隆模式其实已经实现了,只需简单的调用克隆方法即可更高效地得到一个全新的实例副本。
为了更方便的生产飞机,我们决定定义一个敌机克隆工厂类
public class EnemyPlaneFactory {// 此处用单例模式创建一个敌机原型private static EnemyPlane protoType = new EnemyPlane(200);// 获取敌机克隆实例public static EnemyPlane getInstance(int x) {EnemyPlane clone = protoType.clone(); // 复制原型机clone.setX(x); // 重新设置克隆机的x坐标return clone;}
}
我们在敌机克隆工厂类EnemyPlaneFactory中第4行使用了一个静态的敌机对象作为原型,其中获取敌机实例的方法getInstance(),其简单的调用克隆方法得到了一个新的克隆对象(此处省略了一场捕获代码),并将其横坐标重设为传入的参数,最后返回此克隆对象,这样我们便可以轻松获取一驾敌机的克隆实例了
敌机克隆工厂类定义完毕,客户端代码就留给读者自己实践了。
但需要注意,一定要使用懒加载方式,如此既可以节省内存空间,又可以确保敌机的实例化速度,实现敌机的即时性按需克隆,这样游戏便再也不会出现卡顿现象了。
3. 复杂对象的克隆 - 深拷贝、浅拷贝
最后,在使用原型模式之前,必须搞清楚深拷贝与浅拷贝这两个概念,否则会对复杂对象的克隆感到无比困惑
假设,敌机类里有一颗子弹可以发射并击杀玩家的飞机,那么敌机中则包含一颗实例化好的子弹对象,请参考代码清单:
public class EnemyPlane implements Cloneable {private Bullet bullet = new Bullet();private int x; // 敌机横坐标private int y = 0; // 敌机纵坐标// 之后代码省略……
}
如上代码,此时如果进行克隆操作,能否将子弹对象一起成功克隆呢?
答案是否定的:
- Java中的变量分为原始类型和引用类型,浅拷贝指只复制原始类型的值。而引用类型也会被拷贝,但是这个操作知识拷贝了引用类型的地址引用(指针),也就是说副本敌机与原型敌机中的子弹是同一颗,因为两个同样的地址实际指向的内存对象是同一个bullet对象。
- 需要注意的是,克隆方法中调用父类Objecrt的clone方法进行的是浅拷贝,所以此处的bullet并没有真正克隆。
public class EnemyPlane implements Cloneable {private Bullet bullet;private int x; // 敌机横坐标private int y = 0; // 敌机纵坐标public EnemyPlane(int x, Bullet bullet) {this.x = x;this.bullet = bullet;}@Overrideprotected EnemyPlane clone() throws CloneNotSupportedException {EnemyPlane clonePlane = (EnemyPlane) super.clone(); // 克隆出敌机clonePlane.setBullet(this.bullet.clone()); // 对子弹进行深拷贝return clonePlane;}// 之后代码省略……
}
如上代码显示,首先clone方法中依旧对敌机对象进行克隆操作,紧接着对敌机子弹bullet也进行了克隆,这个就是深拷贝操作。当然,此处要注意对于子弹类Bullet同样也得实现克隆接口,请读者自行实现,此处就不再赘述了。
简而言之:深拷贝会复制对象及其所有嵌套子对象,而浅拷贝只复制对象本身,嵌套子对象仍然引用原对象。
4. 克隆的本质
在使用克隆模式对游戏代码反复重构后,游戏性能得到了极大的提升,流畅的游戏画面确保了优秀的用户体验。最后,我们来看原型模式的类结构。
- Prototype(原型接口):声明克隆方法,对应本例程代码中的Cloneable接口。
- ConcretePrototype(原型实现):原型接口的实现类,实现方法中调用super. clone()即可得到新克隆的对象。
- Client(客户端):客户端只需调用实现此接口的原型对象方法clone(),便可轻松地得到一个全新的实例对象。
从类到对象叫作“创建”,而由本体对象至副本对象则叫作“克隆”,当需要创建多个类似的复杂对象时,我们就可以考虑用原型模式。
究其本质,克隆操作时Java虚拟机会进行内存操作,直接拷贝原型对象数据流生成新的副本对象,绝不会拖泥带水触发一些多余的复杂操作(如类加载、实例化、初始化等),所以其效率远远高于“new”关键字所触发的实例化操作。
-- 秒懂设计模式学习笔记
-- 原型