设计模式:原型模式

原型模式

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

原型模式之前,我们先得搞清楚什么是类的实例化。相信大家一定见过活字印章,当我们调整好需要的日期(初始化参数),再轻轻一盖(调用构造方法),一个实例化后的日期便跃然纸上了,这个过程正类似于类的实例化。

其实构造一个对象的过程是耗时耗力的。想必大家一定有过打印和复印的经历,为了节省成本,我们通常会用打印机把电子文档打印到A4纸上(原型实例化过程),再用复印机把这份纸质文稿复制多份(原型拷贝过程),这样既实惠又高效。那么,对于第一份打印出来的原文稿,我们可以称之为“原型文件”,而对于复印过程,我们则可以称之为“原型拷贝”.
在这里插入图片描述

想必大家已经明白了类的实例化与克隆之间的区别,二者都是在造对象,但方法绝对是不同的。原型模式的目的是从原型实例克隆出新的实例,对于那些有非常复杂的初始化过程的对象或者是需要耗费大量资源的情况,原型模式是更好的选择。

深拷贝与浅拷贝

在使用原型模式之前,我们还必须得搞清楚浅拷贝和深拷贝这两个概念,否则会对某些复杂对象的克隆结果感到无比困惑

  • 浅拷贝是创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象
  • 深拷贝是将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象

案例讲解

我们要设计一个空战游戏

在这里插入图片描述

首先我们设计敌机的类EnemyPlane

Java

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++;
    }
}

Go

package prototype

type EnemyPlane struct {
    x int
    y int
}

func NewEnemyPlane(x int) *EnemyPlane {
    return &EnemyPlane{
        x: x,
    }
}

func (this EnemyPlane) GetX() int {
    return this.x
}

func (this EnemyPlane) GetY() int {
    return this.y
}
func (this *EnemyPlane) Fly() {
    this.y++
}

敌机类只提供了getter方法而没有提供setter方法,也就是说我们只能在初始化时确定好敌机的横坐标x,之后则不允许再更改坐标了。当游戏运行时,我们只要连续调用飞行方法fly(),便可以让飞机像雨点一样不断下落。在开始绘制敌机动画之前,我们首先得实例化500架敌机

Java

package prototype;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

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);
        }
    }
}

Go

package main

import (
    "desginPatterns/prototype"
    "math/rand"
)

func main() {
    enemyplanes := make([]*prototype.EnemyPlane, 500)
    for i := 0; i < 500; i++ {
        enemyplane := prototype.NewEnemyPlane(rand.Intn(200))
        enemyplanes = append(enemyplanes, enemyplane)
    }
}

这种做法看似没有任何问题,然而效率却是非常低的。我们知道在游戏画面上根本没必要同时出现这么多敌机,而在游戏还未开始之前,也就是游戏的加载阶段我们就实例化了这一关卡的所有500架敌机,这不但使加载速度变慢,而且是对有限内存资源的一种浪费。那么到底什么时候去构造敌机?答案当然是懒加载了,也就是按照地图坐标,屏幕滚动到某一点时才实时构造敌机,这样一来问题就解决了。

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

我们思考一下之前的设计,既然循环第一次后已经实例化好了一个敌机原型,那么之后又何必去重复这个构造过程呢?敌机对象能否像细胞分裂一样自我复制呢?要解决这些问题,原型模式是最好的解决方案了,下面我们对敌机类进行重构并让其支持原型拷贝

package prototype;

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++;
    }

    //为了让克隆后实例重新修改横坐标
    public void setX(int x){
        this.x = x;
    }
    
    //浅拷贝,如果我们定义其它引用类型,需要重写其类型的clone,并且修改我们这个类的clone
    @Override
    protected EnemyPlane clone() throws CloneNotSupportedException {
        return (EnemyPlane) super.clone();
    }
}

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

Go

package prototype

type EnemyPlane struct {
    x int
    y int
}

func NewEnemyPlane(x int) *EnemyPlane {
    return &EnemyPlane{
        x: x,
    }
}

func (this EnemyPlane) GetX() int {
    return this.x
}

func (this EnemyPlane) GetY() int {
    return this.y
}
func (this *EnemyPlane) Fly() {
    this.y++
}

func (this *EnemyPlane) Clone() *EnemyPlane {
    res := *this
    return &res
}

go 语言中的传递都是值传递,传递一个对象,就会把对象拷贝一份传入函数中,传递一个指针,就会把指针拷贝一份传入进去。

赋值的时候也是这样,res:=*e 就会把传递的 Example 对象拷贝一份,如果是 res:=e 的话,那么拷贝的就是对象的指针了.

注意:

Go语言中所有赋值操作都是值传递,如果结构中不含指针,则直接赋值就是深度拷贝;如果结构中含有指针(包括自定义指针,以及切片,map等使用了指针的内置类型),则数据源和拷贝之间对应指针会共同指向同一块内存,这时深度拷贝需要特别处理。目前,有三种方法,一是用gob序列化成字节序列再反序列化生成克隆对象;二是先转换成json字节序列,再解析字节序列生成克隆对象;三是针对具体情况,定制化拷贝。前两种方法虽然比较通用但是因为使用了reflex反射,性能比定制化拷贝要低出2个数量级,所以在性能要求较高的情况下应该尽量避免使用前两者。

为了更方便地生产飞机,我们决定定义一个敌机克隆工厂类

Java

public class EnemyPlaneFactory {

    //建造一个敌机原型
    private static EnemyPlane protoType = new EnemyPlane(200);

    public static EnemyPlane getInstance(int x) throws CloneNotSupportedException {
        EnemyPlane clone = protoType.clone();
        clone.setX(x);//重新设定克隆机的x坐标
        return clone;
    }
}

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

克隆的本质

Prototype(原型接口):声明克隆方法,对应本例程代码中的Cloneable接口。

ConcretePrototype(原型实现):原型接口的实现类,实现方法中调用super.clone()即可得到新克隆的对象。

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

在这里插入图片描述

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值