【设计模式】-创造篇-原型

定义

在这里插入图片描述
原型模式(Prototype),在制造业中通常是指大批量生产开始之前研发出的概念模型,并基于各种参数指标对其进行检验,如果达到了质量要求,即可参照这个原型进行批量生产。原型模式达到以原型实例创建副本实例的目的即可,并不需要知道其原始类,也就是说,原型模式可以用对象创建对象,而不是用类创建对象,以此达到效率的提升。

原型与副本

在讲原型模式之前,我们先得搞清楚什么是类的实例化。相信大家一定见过活字印章,如图3-1所示,当我们调整好需要的日期(初始化参数),再轻轻一盖(调用构造方法),一个实例化后的日期便跃然纸上了,这个过程正类似于类的实例化。
在这里插入图片描述
其实构造一个对象的过程是耗时耗力的。想必大家一定有过打印和复印的经历,为了节省成本,我们通常会用打印机把电子文档打印到A4纸上(原型实例化过程),再用复印机把这份纸质文稿复制多份(原型拷贝过程),这样既实惠又高效。那么,对于第一份打印出来的原文稿,我们可以称之为“原型文件”,而对于复印过程,我们则可以称之为“原型拷贝”,如图3-2所示。
在这里插入图片描述

卡顿的游戏

想必大家已经明白了类的实例化与克隆之间的区别,二者都是在造对象,但方法绝对是不同的。原型模式的目的是从原型实例克隆出新的实例,对于那些有非常复杂的初始化过程的对象或者是需要耗费大量资源的情况,原型模式是更好的选择。理论还需与实践结合,下面开始实战部分,假设我们准备设计一个空战游戏的程序,如图3-3所示。
在这里插入图片描述
我们这里为了保持简单,设定游戏为单打,也就是说主角飞机只有一架,而敌机则有很多架,而且可以在屏幕上垂直向下移动来撞击主角飞机。具体是如何实现的呢?其实非常简单,就是程序不停改变其坐标并在画面上重绘而已。由浅入深,我们先试着写一个敌机类,请参看代码清单3-1。

代码清单3-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
     }
  
 }

如代码清单3-1所示,敌机类EnemyPlane在第6行的敌机构造器方法中对飞机的横坐标x进行了初始化,而纵坐标则固定为0,这是由于敌机一开始是从顶部飞出的。所以其纵坐标y必然为0(屏幕左上角坐标为[0, 0])。继续往下看,敌机类只提供了getter方法而没有提供setter方法,也就是说我们只能在初始化时确定好敌机的横坐标x,之后则不允许再更改坐标了。当游戏运行时,我们只要连续调用第18行的飞行方法fly(),便可以让飞机像雨点一样不断下落。在开始绘制敌机动画之前,我们首先得实例化500架敌机,请参看代码清单3-2。

代码清单3-2 客户端类Client

1.  public class Client {
    
    
2.  
3.      public static void main(String[] args) {
    
    
4.          List<EnemyPlane> enemyPlanes = new ArrayList<EnemyPlane>();
5.  
6.          for (int i = 0; i < 500; i++) {
    
    
7.              //此处于随机纵坐标处出现敌机
8.              EnemyPlane ep = new EnemyPlane(new Random().nextInt(200));
9.              enemyPlanes.add(ep);
10.         }
11.  
12.     }
13.  
14. }

如代码清单3-2所示,我们在第6行使用了循环的方式来批量生产敌机,并使用了“new”关键字来实例化敌机,循环结束后500架敌机便统统被加入第4行定义的飞机列表enemyPlanes中。这种做法看似没有任何问题,然而效率却是非常低的。我们知道在游戏画面上根本没必要同时出现这么多敌机,而在游戏还未开始之前,也就是游戏的加载阶段我们就实例化了这一关卡的所有500架敌机,这不但使加载速度变慢,而且是对有限内存资源的一种浪费。那么到底什么时候去构造敌机?答案当然是懒加载了,也就是按照地图坐标,屏幕滚动到某一点时才实时构造敌机,这样一来问题就解决了。

然而遗憾的是,懒加载依然会有性能问题,主要原因在于我们使用的“new”关键字进行的基于类的实例化过程,因为每架敌机都进行全新构造的做法是不合适的,其代价是耗费更多的CPU资源,尤其在一些大型游戏中,很多个线程在不停地运转着,CPU资源本身就非常宝贵,此时若进行大量的类构造与复杂的初始化工作,必然会造成游戏卡顿,甚至有可能会造成系统无响应,使游戏体验大打折扣,如图3-4所示。

在这里插入图片描述

细胞分裂

硬件永远离不开优秀的软件,我们绝不允许以糟糕的软件设计对硬件发起挑战,因而代码优化势在必行。我们思考一下之前的设计,既然循环第一次后已经实例化好了一个敌机原型,那么之后又何必去重复这个构造过程呢?敌机对象能否像细胞分裂一样自我复制呢?要解决这些问题,原型模式是最好的解决方案了,下面我们对敌机类进行重构并让其支持原型拷贝,请参看代码清单3-3。

代码清单3-3 可被克隆的敌机类EnemyPlane

1.  public class EnemyPlane implements Cloneable{
    
    
2.  
3.      private int x;//敌机横坐标
4.      private int y = 0;//敌机纵坐标
5.  
6.      public EnemyPlane(int x) {
    
    //构造器
7.          this.x = x;
8.      }
9.  
10.     public int getX() {
    
    
11.         return x;
12.     }
13.  
14.     public int getY() {
    
    
15.         return y;
16.     }
17.  
18.     public void fly(){
    
    //让敌机飞
19.         y++;//每调用一次,敌机飞行时纵坐标+1
20.     }
21.  
22.     //此处开放setX,是为了让克隆后的实例重新修改横坐标
23.     public void setX(int x) {
    
    
24.         this.x = x;
25.     }
26.  
27.     //重写克隆方法
28.     @Override
29.     public EnemyPlane clone() throws CloneNotSupportedException {
    
    
30.         return (EnemyPlane)super.clone();
31.     }
32.  
33. }

如代码清单3-3所示,我们让敌机类EnemyPlane实现了java.lang包中的克隆接口Cloneable,并在第29行的实现方法中调用了父类Object的克隆方法,如此一来外部就能够对本类的实例进行克隆操作了,省去了由类而生的再造过程。还需要注意的是,我们在第23行处加入了设置横坐标方法setX(),使被实例化后的敌机对象依然可以支持坐标位置的变更,这是为了保证克隆飞机的坐标位置个性化。

克隆工厂

至此,克隆模式其实已经实现了,我们只需简单调用克隆方法便能更高效地得到一个全新的实例副本。为了更方便地生产飞机,我们决定定义一个敌机克隆工厂类,请参看代码清单3-4。

代码清单3-4 敌机克隆工厂类EnemyPlaneFactory

1.  public class EnemyPlaneFactory {
    
    
2.  
3.      //此处用单例饿汉模式造一个敌机原型
4.      private static EnemyPlane protoType = new EnemyPlane(200);
5.  
6.      //获取敌机克隆实例
7.      public static EnemyPlane getInstance(int x){
    
    
8.          EnemyPlane clone = protoType.clone();//复制原型机
9.          clone.setX(x);//重新设置克隆机的x坐标
10.         return clone;
11.     }
12.  
13. }

如代码清单3-4所示,我们在敌机克隆工厂类EnemyPlaneFactory中第4行使用了一个静态的敌机对象作为原型,并于第7行提供了一个获取敌机实例的方法getInstance(),其中简单地调用克隆方法得到一个新的克隆对象(此处省略了异常捕获代码),并将其横坐标重设为传入的参数,最后返回此克隆对象,这样我们便可轻松获取一架敌机的克隆实例了。

敌机克隆工厂类定义完毕,客户端代码就留给读者自己去实践了。但需要注意,一定得使用“懒加载”的方式,如此既可以节省内存空间,又可以确保敌机的实例化速度,实现敌机的即时性按需克隆,这样游戏便再也不会出现卡顿现象了。

深拷贝与浅拷贝

最后,在使用原型模式之前,我们还必须得搞清楚浅拷贝和深拷贝这两个概念,否则会对某些复杂对象的克隆结果感到无比困惑。让我们再扩展一下场景,假设敌机类里有一颗子弹可以发射并击杀玩家的飞机,那么敌机中则包含一颗实例化好的子弹对象,请参看代码清单3-5。

代码清单3-5 加装子弹的敌机类EnemyPlane

1.  public class EnemyPlane implements Cloneable{
    
    
2.  
3.      private Bullet bullet = new Bullet();
4.      private int x;//敌机横坐标
5.      private int y = 0;//敌机纵坐标
6.  
7.      //之后代码省略……
8.  
9.  }```

如代码清单3-5所示,对于这种复杂一些的敌机类,此时如果进行克隆操作,我们是否能将第3行中的子弹对象一同成功克隆呢?答案是否定的。我们都知道,Java中的变量分为原始类型和引用类型,所谓浅拷贝是指只复制原始类型的值,比如横坐标x与纵坐标y这种以原始类型int定义的值,它们会被复制到新克隆出的对象中。而引用类型bullet同样会被拷贝,但是请注意这个操作只是拷贝了地址引用(指针),也就是说副本敌机与原型敌机中的子弹是同一颗,因为两个同样的地址实际指向的内存对象是同一个bullet对象。

需要注意的是,克隆方法中调用父类Object的clone方法进行的是浅拷贝,所以此处的bullet并没有被真正克隆。然而,每架敌机携带的子弹必须要发射出不同的弹道,这就必然是不同的子弹对象了,所以此时原型模式的浅拷贝实现是无法满足需求的,那么该如何改动呢?请参看如代码清单3-6中对敌机类的深拷贝支持。

代码清单3-6 支持深拷贝的敌机类EnemyPlane

```java
1.  public class EnemyPlane implements Cloneable{
    
    
2.  
3.      private Bullet bullet;
4.      private int x;//敌机横坐标
5.      private int y = 0;//敌机纵坐标
6.  
7.      public EnemyPlane(int x, Bullet bullet) {
    
    
8.          this.x = x;
9.          this.bullet = bullet;
10.     }
11.  
12.     @Override
13.     protected EnemyPlane clone() throws CloneNotSupportedException {
    
    
14.         EnemyPlane clonePlane = (EnemyPlane) super.clone();//克隆出敌机
15.         clonePlane.setBullet(this.bullet.clone());//对子弹进行深拷贝
16.         return clonePlane;
17.     }
18.  
19.     //之后代码省略……
20.  
21. }

如代码清单3-6所示,首先我们在第13行的克隆方法clone()中依旧对敌机对象进行了克隆操作,紧接着对敌机子弹bullet也进行了克隆,这就是深拷贝操作。当然,此处要注意对于子弹类Bullet同样也得实现克隆接口,请读者自行实现,此处就不再赘述了。

克隆的本质

终于,在我们用克隆模式对游戏代码反复重构后,游戏性能得到了极大的提升,流畅的游戏画面确保了优秀的用户体验。最后,我们来看原型模式的类结构,如图3-5所示。原型模式的各角色定义如下。

Prototype(原型接口):声明克隆方法,对应本例程代码中的Cloneable接口。ConcretePrototype(原型实现):原型接口的实现类,实现方法中调用super.clone()即可得到新克隆的对象。

Client(客户端):客户端只需调用实现此接口的原型对象方法clone(),便可轻松地得到一个全新的实例对象。

在这里插入图片描述
从类到对象叫作“创建”,而由本体对象至副本对象则叫作“克隆”,当需要创建多个类似的复杂对象时,我们就可以考虑用原型模式。究其本质,克隆操作时Java虚拟机会进行内存操作,直接拷贝原型对象数据流生成新的副本对象,绝不会拖泥带水地触发一些多余的复杂操作(如类加载、实例化、初始化等),所以其效率远远高于“new”关键字所触发的实例化操作。看尽世间烦扰,拨开云雾见青天,有时候“简单粗暴”也是一种去繁从简、不绕弯路的解决方案。

猜你喜欢

转载自blog.csdn.net/qq_40500099/article/details/127442781