设计模式相关文章,用于整理网络中对应的设计模式的一些解读。

原型模式

原型模式(Prototype Pattern)是用于创建重复的对象,同时又能保证性能。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

这种模式是实现了一个原型接口,该接口用于创建当前对象的克隆。当直接创建对象的代价比较大时,则采用这种模式。例如,一个对象需要在一个高代价的数据库操作之后被创建。我们可以缓存该对象,在下一个请求时返回它的克隆,在需要的时候更新数据库,以此来减少数据库调用。

介绍

意图:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。

主要解决:在运行期建立和删除原型。

何时使用: 1、当一个系统应该独立于它的产品创建,构成和表示时。 2、当要实例化的类是在运行时刻指定时,例如,通过动态装载。 3、为了避免创建一个与产品类层次平行的工厂类层次时。 4、当一个类的实例只能有几个不同状态组合中的一种时。建立相应数目的原型并克隆它们可能比每次用合适的状态手工实例化该类更方便一些。

如何解决:利用已有的一个原型对象,快速地生成和原型对象一样的实例。

关键代码: 1、实现克隆操作,在 JAVA 继承 Cloneable,重写 clone(),在 .NET 中可以使用 Object 类的 MemberwiseClone() 方法来实现对象的浅拷贝或通过序列化的方式来实现深拷贝。 2、原型模式同样用于隔离类对象的使用者和具体类型(易变类)之间的耦合关系,它同样要求这些”易变类”拥有稳定的接口。

应用实例: 1、细胞分裂。 2、JAVA 中的 Object clone() 方法。

优点: 1、性能提高。 2、逃避构造函数的约束。

缺点: 1、配备克隆方法需要对类的功能进行通盘考虑,这对于全新的类不是很难,但对于已有的类不一定很容易,特别当一个类引用不支持串行化的间接对象,或者引用含有循环结构的时候。 2、必须实现 Cloneable 接口。

使用场景: 1、资源优化场景。 2、类初始化需要消化非常多的资源,这个资源包括数据、硬件资源等。 3、性能和安全要求的场景。 4、通过 new 产生一个对象需要非常繁琐的数据准备或访问权限,则可以使用原型模式。 5、一个对象多个修改者的场景。 6、一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值时,可以考虑使用原型模式拷贝多个对象供调用者使用。 7、在实际项目中,原型模式很少单独出现,一般是和工厂方法模式一起出现,通过 clone 的方法创建一个对象,然后由工厂方法提供给调用者。原型模式已经与 Java 融为浑然一体,大家可以随手拿来使用。

注意事项:与通过对一个类进行实例化来构造新对象不同的是,原型模式是通过拷贝一个现有对象生成新对象的。浅拷贝实现 Cloneable,重写,深拷贝是通过实现 Serializable 读取二进制流。

实例

以下实例来源于微信公众号(Java知音)文章,设计模式是什么鬼(原型)

假设我们要做一个打飞机游戏,游戏设定位纵版移动,单打。

打飞机
打飞机

既然是单打,那我们的主角飞机当然只有一架,于是我们写一个单例模式,此处我们省略主角代码。那么敌机呢?当然有很多架了,好,为了说明问题我们去繁就简,先写一个敌机类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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
}
}

代码public EnemyPlane(int x) {//构造器....开始,初始化只接收x坐标,因为敌机一开始是从顶部出来所以纵坐标y必然是0。此类只提供getter而没有setter,也就是说只能在初始化时确定敌机的横坐标x,后续是不需要更改坐标了,只要连续调用第17行的fly方法即可让飞机跟雨点一样往下砸。

好了,我们开始绘制敌机动画了,先实例化出50架吧。

1
2
3
4
5
6
7
8
9
10
11
12
public class Client {
public static void main(String[] args) {
List<EnemyPlane> enemyPlanes = new ArrayList<EnemyPlane>();

for (int i = 0; i < 50; i++) {
//此处随机位置产生敌机
EnemyPlane ep = new EnemyPlane(new Random().nextInt(200));
enemyPlanes.add(ep);
}

}
}

注意代码EnemyPlane ep = new EnemyPlane(new Random().nextInt(200));觉不觉得每个迭代都实例化new出一个对象存在性能问题呢?答案是肯定的,这个实例化的过程是得不偿失的,构造方法会被调用50次,cpu被极大浪费了,内存被极大浪费了,尤其对于游戏来说性能瓶颈绝对是大忌,这会造成用户体验问题,谁也不希望玩游戏会卡帧吧。

那到底什么时候去new?游戏场景初始化就new敌机(如以上代码)?这关会出现500个敌机那我们一次都new出来吧?浪费内存!那我们实时的去new,每到一个地方才new出来一个!浪费CPU!如果敌机线程过多造成CPU资源耗尽,每出一个敌机游戏会卡一下,试想一下这种极端情况下,游戏对象实例很多的话就是在作死。

打飞机
打飞机

解决方案到底是什么呢?好,原型模式Prototype!上代码!我们把上面的敌机类改造一下,让它支持原型拷贝。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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,为了让克隆后的实例重新修改x坐标
public void setX(int x) {
this.x = x;
}

//为了保证飞机飞行的连贯性
//这里我们关闭setY方法,不支持随意更改Y纵坐标
// public void setY(int y) {
// this.y = y;
// }

//重写克隆方法
@Override
public EnemyPlane clone() throws CloneNotSupportedException {
return (EnemyPlane)super.clone();
}
}

setX()方法为了保证克隆飞机的个性化,因为它们出现的位置是不同的。克隆方法重写我们调用了父类Object的克隆方法,这里JVM会进行内存操作直接拷贝原始数据流,简单粗暴,不会有其他更多的复杂操作(类加载,实例化,初始化等等),速度远远快于实例化操作。OK,我们看怎么克隆这些敌机,做一个造飞机的工厂吧。

1
2
3
4
5
6
7
8
9
10
11
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.getInstance(int x)并声明x坐标位置,一架敌机很快地就做好了,并且我们保证是在敌机出现的时候再去克隆,确保不要一开局就全部克隆出来,如此一来,既保证了实时性节省了内存空间,又保证了敌机实例化的速度,游戏绝不会卡帧!

最后,还要强调一点就是浅拷贝和深拷贝的问题。假如我们的敌机类里有一颗子弹bullet可以射击我们的主角,如下。

1
2
3
4
5
6
7
public class EnemyPlane implements Cloneable{
private Bullet bullet = new Bullet();
private int x;//敌机横坐标
private int y = 0;//敌机纵坐标

//之后代码省略……
}

我们都知道Java中的变量分为原始类型和引用类型,所谓浅拷贝只是拷贝原始类型的指,比如坐标x, y的指会被拷贝到克隆对象中,对于对象bullet也会被拷贝,但是请注意拷贝的只是地址而已,那么多个地址其实真正指向的对象还是同一个bullet。

由于我们调用父类Objectclone方法进行的是浅拷贝,所以此处的bullet并没有被克隆成功,比如我们每架敌机必须携带的子弹是不同的实例,那么我们就必须进行深拷贝,于是我们的代码就得做这样的改动。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class EnemyPlane implements Cloneable{
private Bullet bullet = new Bullet();

public void setBullet(Bullet bullet) {
this.bullet = bullet;
}

@Override
protected EnemyPlane clone() throws CloneNotSupportedException {
// 先克隆出敌机,其中子弹还未进行克隆。
EnemyPlane clonePlane = (EnemyPlane) super.clone();
// 对子弹进行深拷贝
clonePlane.setBullet(this.bullet.clone());
return clonePlane;
}

//之后代码省略……
}

相信大家看注释就能懂了,这里就不做过多解释,当然对于Bullet类也同样实现了克隆接口,代码不用再写了吧?相信大家都学会了举一反三。至此,我们的每个敌机携带的弹药也同样被克隆完毕了,再也不必担心游戏的流畅性了。