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

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