1. 程式人生 > >設計模式(java)- 原型模式

設計模式(java)- 原型模式

1. 簡介

  原型模式又稱克隆模式,它允許一個物件可以創建出另一個與自身型別相同的物件。客戶不需要知道建立的細節,就可以獲取到需要的複製品。
  在java中通過繼承Cloneable的介面實現原型模式,在c++中則可以通過過載賦值函式進行實現。
  在實現原型模式的時候,我們會根據實際情況和業務模型對其做不同的實現,一般分為淺拷貝和深拷貝兩種方式來對原物件進行復制,從而創建出一個新的物件。

2. 類圖

  原型模式的設計類圖很簡單,它需要一個原型類,繼承Cloneable,通過實現介面中的clone方法來實現對原型的物件建立。如下所示:
在這裡插入圖片描述

3. 例項

  首先,我們建立一個原型發票類,然後實現發票的clone方法,從而複製出另一張發票物件。

3.1 原型類

3.1.1 發票類:

class EInvoice implements Cloneable {
	private String strName;
	private String strCompanyName;
	private String strId;
	private String strDate;
	private float  fAmount;
	
	public EInvoice clone() {
		EInvoice invoice = null;
		
		try {
			invoice = (EInvoice)super.clone();
		} catch
(CloneNotSupportedException e) { // TODO Auto-generated catch block e.printStackTrace(); } return invoice; } public String getName() { return this.strName; } public void setName(String strName) { this.strName = strName; } }

3.1.2 客戶呼叫:

		EInvoice invoice= new EInvoice();
		invoice.
setName("張三"); EInvoice invoiceClone = invoice.clone(); System.out.println(invoiceClone.getName());

  如上,發票類繼承了Cloneable介面,然後通過實現clone方法來實現對自身型別的物件建立。這裡直接呼叫父類的clone方法,進行向下型別轉換完成發票物件的建立。父類的克隆方法屬於淺拷貝的方式。

3.2 淺拷貝

  淺拷貝,顧名思義,就是拷貝一些面子上的東西,而沒有拷貝實質的裡子。即當一個物件內部足夠複雜時,例如內部有其他型別的例項A(引用資料型別),如果進行淺拷貝的話,實際上只是拷貝了這個物件的引用,即拷貝出來的物件中的A例項與原物件中A例項指向了同一塊記憶體,如果我們改變了原物件中例項A的內容,拷貝後的物件中的例項A的內容也會發生同樣的變化,這點在C++中通常是指標的拷貝,而非指標所指向的記憶體拷貝。

3.2.1 發票集合類

class EInvoiceCollection implements Cloneable {
	private ArrayList<EInvoice> invoiceList;
	
	public EInvoiceCollection() {
		invoiceList = new ArrayList<EInvoice>();
	}
	
	public void addInvoice(EInvoice invoice) {
		invoiceList.add(invoice);
	}
	
	public EInvoiceCollection clone() {
		EInvoiceCollection invoiceCollection = null;
		
		try {
			invoiceCollection = (EInvoiceCollection)super.clone();
		} catch (CloneNotSupportedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		return invoiceCollection;
	}
	
	public void printAllInvoiceName() {
		if (this.invoiceList == null) {
			return;
		}
		
		int nSize = this.invoiceList.size();
		for (int i = 0; i < nSize; i++) {
			String strName = this.invoiceList.get(i).getName();
			System.out.println(strName);
		}
	}
}

  如上,是一個發票集合類,內部含有一個ArrayList集合,同樣對其進行了繼承Cloneable介面,然後實現clone方法。通過客戶程式碼呼叫和輸出,驗證一下其是否為淺拷貝:

public class EPrototype {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		EInvoice invoice= new EInvoice();
		invoice.setName("張三");
		EInvoice invoiceClone = invoice.clone();
		System.out.println(invoiceClone.getName());
		
		EInvoiceCollection invoiceCollection = new EInvoiceCollection();
		for (int i = 0; i < 3; i++) {
			invoiceCollection.addInvoice(invoice.clone());
		}
		
		EInvoiceCollection invoiceCollClone = invoiceCollection.clone();
		invoiceClone.setName("李四");
		invoiceCollClone.addInvoice(invoiceClone);
		
		System.out.println("原型發票集合:");
		invoiceCollection.printAllInvoiceName();
		
		System.out.println("淺拷貝後發票集合:");
		invoiceCollClone.printAllInvoiceName();
	}

}

輸出結果如下:

張三
原型發票集合:
張三
張三
張三
李四
淺拷貝後發票集合:
張三
張三
張三
李四

  根據輸出結果,可以看到,淺拷貝和原型的發票集合的結果是一致的。也就是對拷貝後的發票集合新增一個發票,原型也跟著新增了一個發票。這就是淺拷貝,兩個物件中的ArrayList是同一個,拷貝出來的物件擁有ArrayList的引用,所以改變拷貝出來的物件中的ArrayList,其實就是改變原型中的ArrayList。在一些特殊的業務場景中,淺拷貝是很有用的,但使用者一定要知道其是個淺拷貝,專門服務於業務而做的設計。

3.3 深拷貝

  有淺拷貝,對應的就有深拷貝,深拷貝其實就是將引用型別所指向的內容拷貝到另一個物件中,這兩個物件除了型別一致外,他們所佔用的記憶體地址是不一樣的。對兩者中的任意一個修改都不會影響到另外一個。
  一般的,如果一個類很複雜,內部有很多引用型別的成員,我們會禁用深拷貝。但是對於類不是很複雜,內部有少量的引用型別的成員,如果業務有需求,可以提供深拷貝的方法,這些都是根據業務需求來變化。現提供兩種深拷貝的方法。

3.3.1 窮舉呼叫成員的clone方法

// Deep cloning
class EInvoiceCollection implements Cloneable {
	ArrayList<EInvoice> invoiceList;
	
	public EInvoiceCollection() {
		invoiceList = new ArrayList<EInvoice>();
	}
	
	public void addInvoice(EInvoice invoice) {
		invoiceList.add(invoice);
	}
	
	public EInvoiceCollection clone() {
		EInvoiceCollection invoiceCollection = null;
		
		try {
			invoiceCollection = (EInvoiceCollection)super.clone();
		} catch (CloneNotSupportedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		invoiceCollection.invoiceList = (ArrayList<EInvoice>) this.invoiceList.clone();
		return invoiceCollection;
	}
	
	public void printAllInvoiceName() {
		if (this.invoiceList == null) {
			return;
		}
		
		int nSize = this.invoiceList.size();
		for (int i = 0; i < nSize; i++) {
			String strName = this.invoiceList.get(i).getName();
			System.out.println(strName);
		}
	}
}

還是呼叫之前的客戶程式碼,輸出結果:

張三
原型發票集合:
張三
張三
張三
淺拷貝後發票集合:
張三
張三
張三
李四

  如果內部的引用型別的物件類本身也提供了深拷貝的方法,就可以在聚合類中一個個的呼叫成員的Clone方法實現深拷貝,顯然這種方法是比較麻煩的,有些引用型別成員沒有clone方法就不行了,且引用型別成員多的話,也會容易出錯,維護麻煩。

3.3.2 序列化物件

interface ECloneable extends Cloneable, Serializable {
	
}

class EInvoice implements ECloneable {
	private String strName;
	private String strCompanyName;
	private String strId;
	private String strDate;
	private float  fAmount;
	
	public EInvoice clone() {
		EInvoice invoice = null;
		
		try {
			invoice = (EInvoice)super.clone();
		} catch (CloneNotSupportedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		return invoice;
	}
	
	public String getName() {
		return this.strName;
	}
	
	public void setName(String strName) {
		this.strName = strName;
	}
}

class EInvoiceCollection implements ECloneable {
	ArrayList<EInvoice> invoiceList;
	
	public EInvoiceCollection() {
		invoiceList = new ArrayList<EInvoice>();
	}
	
	public void addInvoice(EInvoice invoice) {
		invoiceList.add(invoice);
	}
	
	public EInvoiceCollection clone() {
		EInvoiceCollection invoiceCollection = null;
		ByteArrayOutputStream byteOS = new ByteArrayOutputStream();
		ObjectOutputStream objectOS;
		try {
			objectOS = new ObjectOutputStream(byteOS);
			objectOS.writeObject(this);
		} catch (IOException e) {
			e.printStackTrace();
		}  
		
		ByteArrayInputStream byteIS = new ByteArrayInputStream(byteOS.toByteArray()); 
		ObjectInputStream objectIS;
		try {
			objectIS = new ObjectInputStream(byteIS);
			invoiceCollection = (EInvoiceCollection) objectIS.readObject();
		} catch (IOException e) {
			e.printStackTrace();
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
		
		return invoiceCollection;
	}
	
	public void printAllInvoiceName() {
		if (this.invoiceList == null) {
			return;
		}
		
		int nSize = this.invoiceList.size();
		for (int i = 0; i < nSize; i++) {
			String strName = this.invoiceList.get(i).getName();
			System.out.println(strName);
		}
	}
}

客戶程式碼仍舊不變,輸出結果如下:

張三
原型發票集合:
張三
張三
張三
淺拷貝後發票集合:
張三
張三
張三
李四

  如上,通過繼承Serializable介面在clone方法中使用位元組流將物件先寫入物件輸出流,這一過程其實就是拷貝原型的堆記憶體中的內容到另個記憶體中,然後再轉換回原型型別,實現深拷貝。

4. 總結

  原型模式相對比較簡單,理解了淺拷貝和深拷貝就會很好的利用原型模式,如果你對c++中的淺拷貝和深拷貝熟悉些,那麼對java中的淺拷貝和深拷貝就不難理解了。