23种设计模式——原型模式

原型模式虽然是创建型的模式,但是与工程模式没有关系,从名字即可看出,该模式的思想就是将一个对象作为原型,对其进行复制、克隆,产生一个和原对象类似的新对象。

一、应用场景

假设有这样一种情景:如果你正在开发一个银行管理系统,其中有一个功能是在客户端查看某人的账户余额,你采用简单工厂模式,由AccountFactory负责根据用户传入的用户名创建用户账号的对象,然后返回给客户端,具体代码如下:

Client(客户端):

public class Test {
	public static void main(String[] args) {
		String name = "laowang";
		AccountFactory af = new AccountFactory();
		PersonAccount pa = af.getPersonAccount(name);
		System.out.println(name+"的账户余额是:"+pa.getAccount());
	}
}

AccountFactory(账号工厂类):

public class AccountFactory {
	PersonAccount personAccount = null;
	public PersonAccount getPersonAccount(String name){
		if(personAccount == null){
			personAccount = new PersonAccount();
			personAccount.setName(name);
			personAccount.setAccount(1000);
			personAccount.setAddress("北京");
		}
		return personAccount;
	}
}

PersonAccount(账号类):

public class PersonAccount {
	private int account;//账户余额
	private String name;//账户名
	private String address;//账户拥有者地址
	public int getAccount() {
		return account;
	}
	public void setAccount(int account) {
		this.account = account;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public String getAddress() {
		return address;
	}
	public void setAddress(String address) {
		this.address = address;
	}
}

现在我们来运行下,我们惊喜地发现运行结果为:

laowang的账户余额是:1000

运行成功,没有任何错误,就目前状态来看,代码很完美。可是,事情往往没有你想象的那么简单。下面我们来在客户端高点小破坏,加入,我就是laowang,一查看账户余额,怎么只剩以前了,一定是上个月网购花太多了,怎么办呢?灵机一动,有了,我们在客户端加几行代码吧,改动后的客户端如下:

public class Test {
	public static void main(String[] args) {
		String name = "laowang";
		AccountFactory af = new AccountFactory();
		PersonAccount pa = af.getPersonAccount(name);
		System.out.println(name+"的账户余额是:"+pa.getAccount());
		pa.setAccount(100000);//添加的代码
		//重新查询我的账户余额
		pa = af.getPersonAccount(name);
		System.out.println(name+"的账户余额(改过后的)是:"+pa.getAccount());
	}
}

我们再来运行一下,发现结果如下:

laowang的账户余额是:1000
laowang的账户余额(改过后的)是:100000

只改动一行,就变成了10万块。但是,如果你是银行你肯定忍不了这样的事情发生,那么究竟怎么避免和解决这种漏洞呢?之所以会产生这样的错误,是因为我们返回给客户端的PersonAccount对象是引用类型的数据,客户端的改变会导致真实的数据改变。我们知道如果能够像值传递一样,不传递地址(引用),就可以避免这样的错误。那么究竟如何做呢?这里,就要用到我们的神奇克隆术——Java深、浅Clone。

二、Java浅Clone

我们首先来看一下,使用Java的Clone机制如何改善我们的系统,具体的代码改变如下:

PsersionAccount(账号类):

public class PersonAccount implements Cloneable{
	private int account;//账户余额
	private String name;//账户名
	private String address;//账户拥有者地址
	//增加Clone方法
	public Object clone(){
		PersonAccount personAccount = null;
		try{
			personAccount = (PersonAccount)super.clone();
		}catch(CloneNotSupportedException e){
			e.printStackTrace();
		}
		return personAccount;
	}
        //省略了get、set方法
}

AccountFactory(账号工厂类):

public class AccountFactory {
	PersonAccount personAccount = null;
	public PersonAccount getPersonAccount(String name){
		if(personAccount == null){
			personAccount = new PersonAccount();
			personAccount.setName(name);
			personAccount.setAccount(1000);
			personAccount.setAddress("北京");
		}
		return (PersonAccount) personAccount.clone();//改动的地方
	}
}

上面两个类中,PersonAccount类增加了一个方法,实现了一个接口。AccountFactory中的返回语句发生了变动。改动很简单,我们再来运行一下我们的程序,我们发现,现在输出为:

laowang的账户余额是:1000
laowang的账户余额(改过后的)是:1000

我们看到:此时,用户是无法通过客户端来改变用户的账户余额的,我们已经成功解决了第一个麻烦。下面,我们就来解释下,我们到底是如何解决的。

首先,我们来看一下改变比较大的PersonAccount类:我们可以看到PersonAccount类implements了一个Cloneable接口,那么这个接口究竟做了什么呢?我们来看一下Java源码,如下:

/*
 * Copyright (c) 1995, 2004, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 */
package java.lang;
/**
 * A class implements the <code>Cloneable</code> interface to
 * indicate to the {@link java.lang.Object#clone()} method that it
 * is legal for that method to make a
 * field-for-field copy of instances of that class.
 * <p>
 * Invoking Object's clone method on an instance that does not implement the
 * <code>Cloneable</code> interface results in the exception
 * <code>CloneNotSupportedException</code> being thrown.
 * <p>
 * By convention, classes that implement this interface should override
 * <tt>Object.clone</tt> (which is protected) with a public method.
 * See {@link java.lang.Object#clone()} for details on overriding this
 * method.
 * <p>
 * Note that this interface does <i>not</i> contain the <tt>clone</tt> method.
 * Therefore, it is not possible to clone an object merely by virtue of the
 * fact that it implements this interface.  Even if the clone method is invoked
 * reflectively, there is no guarantee that it will succeed.
 *
 * @author  unascribed
 * @see     java.lang.CloneNotSupportedException
 * @see     java.lang.Object#clone()
 * @since   JDK1.0
 */
public interface Cloneable {
}

我们从上面的源码看到,Cloneable接口中其实一个方法也没有,其实这个接口只是一个标志,声明implements Cloneable的类只是表示这个类实现了Clone方法,我们知道所有的类都默认继承了java.lang.Object类,所以,我们去看看这个父类的clone方法是如何实现的。看一下java源码,如下:

    /* @return     a clone of this instance.
     * @exception  CloneNotSupportedException  if the object's class does not
     *               support the {@code Cloneable} interface. Subclasses
     *               that override the {@code clone} method can also
     *               throw this exception to indicate that an instance cannot
     *               be cloned.
     * @see java.lang.Cloneable
     */
    protected native Object clone() throws CloneNotSupportedException;

从上图,我们可以获得三个问题的答案:

第一个:我们看到clone()方法的访问权限为protected,这就是我们为什么要在子类中覆盖重写父类clone方法,并且将访问修饰符改为public的原因,因为只有这样才能实现包外非子类的自由方法,当然了,如果你想要自己的方法限制为包类或子类访问的话,可以不改变修饰符。

第二个:我们看到close()方法依旧没有方法体,但是我们发现clone()为native方法,native是什么意思呢?native方法表示这个方法是有非java语言来编写的,大多数为本地方法,大多数与操作系统等底层实现有关,当然了,效率也就比普通的方法高很多。其实,这也就是我们为什么要调用父类clone()方法,而不是自己new一个对象,然后进行一次按复制来完成一个对象的拷贝。调用父类的clone方法不仅效率较高,而且操作简单,特别是在对象属性较多的情况下,更加明显。

第三个:我们看到该方法会抛出一个异常CloneNotSuppotedException,我们看到上面的方法注释中说明了,如果我们没有生命Implements Cloneable接口的话,就会抛出这个异常,其实,这也是我们为什么要声明Implements Cloneable接口,即使它其实一个方法也没有。

到这里,我们基本解决并解释了第一个问题,如果你觉得这里就万事大吉了

其实事情往往没有你想象的name简单

我们再来搞点破坏。我们知道,正常情况下,我们会有一个单独的User类来负责管理存储用户信息,其实PersonAccount类中,应该持有一个User类的对象,通过这个对象获取用户信息,而不是直接将name,address等用户信息直接存储在PersonAccount类中,特别是那些和Account无关的用户信息等。那么,我们来改造下我们的系统:

User类:

public class User {
	private String name;//账户名
	private String address;//账户拥有者地址
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public String getAddress() {
		return address;
	}
	public void setAddress(String address) {
		this.address = address;
	}
}

PersonAccount类:主要增加了User属性,删除name、address属性

public class PersonAccount implements Cloneable{
	private int account;//账户余额
	private User user;
	//增加Clone方法
	public Object clone(){
		PersonAccount personAccount = null;
		try{
			personAccount = (PersonAccount)super.clone();
		}catch(CloneNotSupportedException e){
			e.printStackTrace();
		}
		return personAccount;
	}
	public int getAccount() {
		return account;
	}
	public void setAccount(int account) {
		this.account = account;
	}
	public User getUser() {
		return user;
	}
	public void setUser(User user) {
		this.user = user;
	}
}

AccountFactory类:改变name赋值为生成User对象

public class AccountFactory {
	PersonAccount personAccount = null;
	public PersonAccount getPersonAccount(String name){
		if(personAccount == null){
			personAccount = new PersonAccount();
			personAccount.setAccount(1000);
			//生成User对象
			User user = new User();
			user.setName(name);
			user.setAddress(name+"的家庭地址");
			personAccount.setUser(user);
		}
		//改动的地方
		return (PersonAccount) personAccount.clone();
	}
}

客户端代码:

public class Test {
	public static void main(String[] args) {
		String name = "laowang";
		AccountFactory af = new AccountFactory();
		PersonAccount pa = af.getPersonAccount(name);
		
		//修改后的代码
		User user = pa.getUser();
		System.out.println(user.getName()+":"+user.getAddress());
		System.out.println("账户余额:"+pa.getAccount());
		System.out.println("**********************************");
		
		//尝试修改账户余额
		pa.setAccount(100000);
		//再调用查询功能
		pa = af.getPersonAccount(name);
		User user1 = pa.getUser();
		System.out.println(user1.getName()+":"+user1.getAddress());
		System.out.println("账户余额:"+pa.getAccount());
		System.out.println("**********************************");
		
		//尝试修改用户信息类User
		user1.setName("我自己");
		user1.setAddress("我自己的住址");
		//再调用查询功能
		pa = af.getPersonAccount(name);
		User user2 = pa.getUser();
		System.out.println(user2.getName()+":"+user2.getAddress());
		System.out.println("账户余额:"+pa.getAccount());
		System.out.println("**********************************");
	}
}

以上的全部代码主要展示了我们增加User类之后的系统,我们来运行一下,发现结果如下:

laowang:laowang的家庭地址
账户余额:1000
**********************************
laowang:laowang的家庭地址
账户余额:1000
**********************************
我自己:我自己的住址
账户余额:1000
**********************************

从上面的结果可以看出,我们加入User类之后,依旧能够防止对账户余额(Account)的修改,但是,这种情况下,竟然可以修改用户信息(User),将别人的账户改为自己的账户,据为己有,这肯定是不行的。

之所以会产生上面的结果,是因为我们这里采用的是Java的浅Clone,如果我们采用Java的深Clone的话,会在一定程度上避免上面的错误产生。那么究竟上面是Java浅Clone,什么是Java深Clone,它们又有怎样的区别呢?我们继续往下看。

三、Java深Clone

在解释Java深Clone、浅Clone的区别浅,我们先来解释下Java中Clone方法的底层实现机制。由于clone方法类型为native,我们并不能看到它的具体代码实现,那么它的底层究竟是如果实现的呢?

其实,是这样的,在执行clone操作的时候,底层会申请出一块和原来对象所占用空间一样大小的存储空间,然后将原来对象所占空间的所有数据都原样拷贝到新申请到的空间,这样就获得了一个和原来一模一样的对象。但是,我们需要注意的是,在拷贝过程中,值类型的数据,当然没有问题,比如int型,String型等其他的基本数据类型。但,对于引用类型的数据,如对象呢。在原来的地址空间中存储的就是一个指向真实对象的引用,拷贝到新的地址空间之后,引用还是指向同一个真实对象。如下图:

那么现在我们对clone得到的对象中的Account等值类型数据进行更改的时候,并不会影响原有的对象中的数据,但是,当我们对clone得到的对象中的user等引用类型数据进行更改的时候,因为指向的是同一个真实对象,那么就一定会影响到原有的对象中的数据,这就是所谓的浅clone。

那么,我们如何避免这种情况呢,那就是在对PsersonAccount对象进行clone操作时,对其中的user镀锡等所有引用类型的数据也同样进行一次clone操作,当然了,这样操作的前提是user类能够像personAccount类一样实现cloneable接口,并重写父类的clone方法,这就是深clone。

下面,我们来看一下,就目前我们的状况:存在简单嵌套的情况下,我们应该如何进行深度Clone呢。

User类:

public class User implements Cloneable{
	private String name;//账户名
	private String address;//账户拥有者地址
	public Object clone(){
		User user = null;
		try{
			user = (User)super.clone();
		}catch(CloneNotSupportedException e){
			e.printStackTrace();
		}
		return user;
	}
    	//省略get、set方法
}

PersonAccount类:

public class PersonAccount implements Cloneable{
	private int account;//账户余额
	private User user;
	//增加Clone方法
	public Object clone(){
		PersonAccount personAccount = null;
		try{
			personAccount = (PersonAccount)super.clone();
			User user = (User)personAccount.getUser().clone();
			personAccount.setUser(user);
		}catch(CloneNotSupportedException e){
			e.printStackTrace();
		}
		return personAccount;
	}
	//省略get、seet方法
}

此时,深clone的操作情况如下:

此时,我们再来运行一下客户端的代码,我们发现,运行结果如下:

laowang:laowang的家庭地址
账户余额:1000
**********************************
laowang:laowang的家庭地址
账户余额:1000
**********************************
laowang:laowang的家庭地址
账户余额:1000
**********************************

我们发现,采用深clone之后,无论是值类型数据还是引用类型数据,都无法在客户端进行修改。到这里,我们的系统比一开始的原始系统要安全健壮多了。

当然了,如果user类中还持有其他引用数据类型的数据,那么对象这些数据,也必须采用同样的深clone操作,也就是说存在于引用链中的所有引用类型数据,都必须进行深clone操作。你可能会说,这要是很多层引用嵌套,岂不是会呈现爆炸式地复杂度增加,不可否认,确实是这样的。也正是因为这个原因,所以一般在引用嵌套层数较少的情况下才使用深clone,在嵌套层数过多时,往往会导致复杂度过高而无法使用深度clone。此时,也许会因为在复杂度和深度clone之间的权衡中,做出一种“不伦不类”的做法,那就是一部分引用类型的数据使用clone操作,一部分引用类型数据直接使用new并以此按项复制的方式进行手动clone。当然,不用说,也知道这是一种不太好的做法。

那么我们应该如何应对嵌套层数较多,数据信息内容较复杂的对象的clone操作呢?也许采用序列化的方式,是一个不错的选择,那么如何通过序列化方式进行相关操作呢?

四、序列化实现深克隆

再次更改系统代码

PersonAccount类:

public class PersonAccount implements Cloneable{
	private int account;//账户余额
	private User user;
	//增加Clone方法
	public Object clone(){
		PersonAccount personAccount = null;
		try{
			//输出流
			ByteArrayOutputStream bout = new ByteArrayOutputStream();
			ObjectOutputStream oos = new ObjectOutputStream(bout);
			oos.writeObject(this);
			oos.close();
			//输入流
			ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
			ObjectInputStream ois = new ObjectInputStream(bin);
			personAccount = (PersonAccount) ois.readObject();
			ois.close();
		}catch(Exception e){
			e.printStackTrace();
		}
		return personAccount;
	}
	//省略get、seet方法
}

接下来我们看下运行结果:

laowang:laowang的家庭地址
账户余额:1000
**********************************
laowang:laowang的家庭地址
账户余额:1000
**********************************
laowang:laowang的家庭地址
账户余额:1000
**********************************

到这里克隆就讲完啦~整理来自于https://blog.csdn.net/qiumengchen12/article/details/45022919

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章