1. 程式人生 > >23中設計模式之_原型模式(深/淺拷貝)

23中設計模式之_原型模式(深/淺拷貝)

前言

原型模式其實java Object中已經提供了一個Clone( )方法,平時很少用到,最近讀Retrofit原始碼時候看到有這種使用方式。

定義

原型模式就是在系統clone()標記的基礎上,對Clone()進行復寫,不同的操作可以產生兩種拷貝模式。

UML類圖

這裡寫圖片描述

原始碼分析

今天我們來講原型模式,這個模式的簡單程度是僅次於單例模式和迭代器模式,非常簡單,但是要使
用好這個模式還有很多注意事項。我們通過一個例子來解釋一下什麼是原型模式。
現在電子賬單越來越流行了,比如你的信用卡,到月初的時候銀行就會發一份電子郵件到你郵箱中,
說你這個月消費了多少,什麼時候消費的,積分是多少等等,這個是每個月發一次,但是還有一種也是銀
行發的郵件你肯定有印象:廣告信,現在各大銀行的信用卡部門都在拉攏客戶,電子郵件是一種廉價、快
捷的通訊方式,你用紙質的廣告信那個費用多高呀,比如我今天推出一個信用卡刷卡抽獎活動,通過電子
賬單系統可以一個晚上傳送給 600 萬客戶,為什麼要用電子賬單系統呢?直接找個發垃圾郵件不就解決問
題了嗎?是個好主意,但是這個方案在金融行業是行不通的,銀行發這種郵件是有要求的,一是一般銀行
都要求個性化服務,發過去的郵件上總有一些個人資訊吧,比如“XX 先生”,“XX 女士”等等,二是郵件的
到達率有一定的要求,由於大批量的傳送郵件會被接收方郵件伺服器誤認是垃圾郵件,因此在郵件頭要增
加一些偽造資料,以規避被反垃圾郵件引擎誤認為是垃圾郵件;從這兩方面考慮廣告信的傳送也是電子賬單系統(電子賬單系統一般包括:賬單分析、賬單生成器、廣告信管理、傳送佇列管理、傳送機、退信處
理、報表管理等)的一個子功能,我們今天就來考慮一下廣告信這個模組是怎麼開發的。那既然是廣告信,
肯定需要一個模版,然後再從資料庫中把客戶的資訊一個一個的取出,放到模版中生成一份完整的郵件,
然後扔給傳送機進行傳送處理,我們來看類圖:
這裡寫圖片描述


在類圖中 MailTem是廣告信的模板,一般都是從資料庫取出,生成一個 BO 或者是 DTO,我們這裡
使用一個靜態的值來做代表;Mail 類是一封郵件類,傳送機傳送的就是這個類,我們先來看看我們的程式:
貼程式碼:

Mail 就是一個業務物件,我們再來看業務場景類是怎麼呼叫的:

  package com.weichao.prototy;
    
    import java.util.Random;
    
    /**
     * 原型模式 銀行電子廣告
     * 
     * @author weichyang
     * 
     *         1.有什麼弊端 2.單執行緒,傳送 600萬封需要多長時間 3.改用多執行緒
     * 
     */
    public class CopyOfClient {
    
        public static int MAX_COUNT = 5;
    
    	public static void main(String[] args) {
    		/* 傳送郵件 */
    		final Mail mail = new Mail(new MailTemp());
    		mail.setTail("xxx銀行的所有版權");
    
    		for (int i = 0; i < MAX_COUNT; i++) {
    			mail.setSub(getRandString(5) + " 先生(女士) ");
    			mail.setReceiver(getRandString(5) + "@" + getRandString(8) + ".com");
    			sendMail(mail);
    
    		}
    	}
    
    	public static void sendMail(Mail mail) {
    		System.out.println("標題: " + mail.getSub() + "\t收件人"
    				+ mail.getReceiver() + "\t....傳送成功! ");
    	}
    
    	public static String getRandString(int maxLength) {
    		String source = "abcdefghijklmnopqrskuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
    		StringBuffer sb = new StringBuffer();
    		Random rand = new Random();
    		for (int i = 0; i < maxLength; i++) {
    			sb.append(source.charAt(rand.nextInt(source.length())));
    		}
    		return sb.toString();
    	}
    
    }

Mail

 package com.weichao.prototy;
    
    public class Mail  {
    
    	public String receiver;// 接收者
    	public String tail;// 結尾備註
    	private String context; // 內容
    	private String sub; // 標題
    
    	public Mail(MailTemp mTemp) {
    		this.context = mTemp.getMainContentString();
    		this.sub = mTemp.getSubString();
    	}
    
    	public String getReceiver() {
    		return receiver;
    	}
    
    	public void setReceiver(String receiver) {
    		this.receiver = receiver;
    	}
    
    	public String getTail() {
    		return tail;
    	}
    
    	public void setTail(String tail) {
    		this.tail = tail;
    	}
    
    	public String getContext() {
    		return context;
    	}
    
    	public void setContext(String context) {
    		this.context = context;
    	}
    
    	public String getSub() {
    		return sub;
    	}
    
    	public void setSub(String sub) {
    		this.sub = sub;
    	}
    
    
    }

MailTemp

    package com.weichao.prototy;
    
    public class MailTemp {
    
    	public String subString;// 標題
    	public String mainContentString; // 廣告內容
    
    	public String getSubString() {
    		return "xxxxxxxxxxxxx賬單";
    	}
    
    	public String getMainContentString() {
    		return "xxx" + "(先生/女士)";
    	}
    
    }

執行結果:

標題: OcqZc 先生(女士) 收件人[email protected] …傳送成功!
標題: qunOc 先生(女士) 收件人[email protected] …傳送成功!
標題: arBDA 先生(女士) 收件人[email protected] …傳送成功!
標題: VgaMg 先生(女士) 收件人[email protected] …傳送成功!
標題: TxuHD 先生(女士) 收件人[email protected] …傳送成功!

由於是隨機數,每次執行都由所差異,不管怎麼樣,我們這個電子賬單傳送程式時寫出來了,也能發
送出來了,我們再來仔細的想想,這個程式是否有問題?你看,你這是一個執行緒在執行,也就是你傳送是
單執行緒的, 那按照一封郵件發出去需要 0.02 秒 (夠小了,你還要到資料庫中取資料呢), 600 萬封郵件需要…
我算算(掰指頭計算中…),恩,是 33 個小時,也就是一個整天都發送不完畢,今天傳送不完畢,明天的
賬單又產生了,積累積累,激起甲方人員一堆抱怨,那怎麼辦?
好辦,把 sendMail 修改為多執行緒,但是你只把 sendMail 修改為多執行緒還是有問題的呀,你看哦,產
生第一封郵件物件,放到執行緒 1 中執行,還沒有傳送出去;執行緒 2 呢也也啟動了,直接就把郵件物件 mail的收件人地址和稱謂修改掉了,執行緒不安全了,好了,說到這裡,你會說這有 N 多種解決辦法,我們不多
說,我們今天就說一種,使用原型模式來解決這個問題,使用物件的拷貝功能來解決這個問題,類圖稍作
修改,如下圖:

這裡寫圖片描述

這裡貼出來修改的地方

在Mail中實現

 public class Mail implements Cloneable {
    ...
    ...
    ...
    	// 進行淺拷貝
    	@Override
    	protected Mail clone() throws CloneNotSupportedException {
    		Mail mail = (Mail) super.clone();
    		return mail;
    	}
    
    }

Client呼叫的地方

  public class Client {
    
    	public static int MAX_COUNT = 5;
    
    	public static void main(String[] args) {
    		/* 傳送郵件 */
    		final Mail mail = new Mail(new MailTemp());
    		mail.setTail("xxx銀行的所有版權");
    
    		for (int i = 0; i < MAX_COUNT; i++) {
    			Mail cloneMail;
    			try {
    				cloneMail = mail.clone();
    				cloneMail.setSub(getRandString(5) + " 先生(女士) ");
    				cloneMail.setReceiver(getRandString(5) + "@" + getRandString(8)
    						+ ".com");
    				sendMail(cloneMail);
    			} catch (CloneNotSupportedException e) {
    				// TODO Auto-generated catch block
    				e.printStackTrace();
    			}
    		}
    	}
    }
    
    }

執行結果不變,一樣完成了電子廣告信的傳送功能,而且 sendMail 即使是多執行緒也沒有關係,看到mail.clone()這個方法了嗎?把物件拷貝一份,產生一個新的物件,和原有物件一樣,然後再修改細節的資料,如設定稱謂,設定收件人地址等等。這種不通過 new 關鍵字來產生一個物件,而是通過物件拷貝來實現的模式就叫做原型模式,這個模式的核心是一個clone( )方法,通過這個方法進行物件的拷貝,Java 提供了一個 Cloneable 介面
來標示這個物件是可拷貝的,為什麼說是“標示”呢?翻開 JDK 的幫助看看 Cloneable 是一個方法都沒有
的,這個介面只是一個標記作用,在 JVM 中具有這個標記的物件才有可能被拷貝,那怎麼才能從“有可能被拷貝”轉換為“可以被拷貝”呢?方法是覆蓋clone()方法,是的,你沒有看錯是重寫 clone()方法,看看我們上面 Mail 類:

   @Override
    public Mail clone(){}

在 clone()方法上增加了一個註解@Override, 沒有繼承一個類為什麼可以重寫呢?在 Java 中所有類的
老祖宗是誰?對嘛,Object 類,每個類預設都是繼承了這個類,所以這個用上@Override是非常正確的。原型模式雖然很簡單,但是在 Java 中使用原型模式也就是 clone 方法還是有一些注意事項的,我們通過幾個例子一個一個解說(如果你對 Java 不是很感冒的話,可以跳開以下部分)。

物件拷貝時,類的建構函式是不會被執行的。 一個實現了 Cloneable 並重寫了 clone 方法的類 A,有一個無參構造或有參構造 B,通過 new 關鍵字產生了一個物件 S,再然後通過 S.clone()方式產生了一個新的
物件 T,那麼在物件拷貝時建構函式 B 是不會被執行的,
物件拷貝時確實建構函式沒有被執行,這個從原理來講也是可以講得通的,Object 類的 clone 方法的
原理是從記憶體中(具體的說就是堆記憶體)以二進位制流的方式進行拷貝,重新分配一個記憶體塊,那建構函式
沒有被執行也是非常正常的了。

淺拷貝和深拷貝問題。 再解釋什麼是淺拷貝什麼是深拷貝前,我們先來看個例子

 package ShallowCopy;
    import java.util.ArrayList;
    
    /**
     * 1.淺拷貝拷貝外層物件,物件裡面的引用物件不進行拷貝。
     * 2.深拷貝需要進行內部的拷貝(人為進行拷貝)。
     * @author weichyang
     * 
     */
    public class ShallowOne implements Cloneable {
    
    	public ArrayList<String> getShallowCopyArrayList() {
    		return shallowCopyArrayList;
    	}
    
    	public void setShallowCopyArrayList(ArrayList<String> shallowCopyArrayList) {
    		this.shallowCopyArrayList = shallowCopyArrayList;
    	}
    
    	ArrayList<String> shallowCopyArrayList = new ArrayList<String>();
    
    	@SuppressWarnings("unchecked")
    	@Override
    	protected ShallowOne clone() throws CloneNotSupportedException {
    
    		//只是clone()屬於淺拷貝
    		ShallowOne shallowOne = (ShallowOne)     super.clone();
    		return shallowOne;
    	}
    
    }

呼叫

    package ShallowCopy;
    
    import java.util.ArrayList;
    
    /**
     * 拷貝 原來 list 進行操作,原來的list中元素同樣會增加 1.前拷貝 只拷貝基礎資料型別 2.深拷貝,拷貝所有,需要手動進行操作
     * 
     * @author weichyang
     * 
     */
    
    public class Client {
    
    	public static void main(String[] args) {
    
    		ShallowOne shallowOne = new ShallowOne();
    
    		ArrayList<String> strings = shallowOne.getShallowCopyArrayList();
    
    		strings.add("張三");
    		ArrayList<String> cloneObject = (ArrayList<String>) strings.clone();
    
    		cloneObject.add("李四");
    
    		System.out.println(cloneObject.toString());
    	}
    }

大家猜想一下執行結果應該是什麼?是就一個“張三”嗎?執行結果如下:
[張三, 李四]
怎麼會有李四呢?是因為 Java 做了一個偷懶的拷貝動作,Object 類提供的方法 clone 只是拷貝本物件,
其物件內部的陣列、引用物件等都不拷貝,還是指向原生物件的內部元素地址,這種拷貝就叫做淺拷貝
確實是非常淺,兩個物件共享了一個私有變數,你改我改大家都能改,是一個種非常不安全的方式,在實
際專案中使用還是比較少的。你可能會比較奇怪,為什麼在 Mail 那個類中就可以使用使用 String 型別,
而不會產生由淺拷貝帶來的問題呢?內部的陣列和引用物件才不拷貝,其他的原始型別比如int,long,String(Java 就希望你把 String 認為是基本型別,String 是沒有 clone 方法的)等都會被拷貝的。
淺拷貝是有風險的,那怎麼才能深入的拷貝呢?我們修改一下我們的程式

 public class ShallowOne implements Cloneable {
    
    
    	protected ShallowOne clone() throws CloneNotSupportedException {
    
    		//只是clone()屬於淺拷貝
    		ShallowOne shallowOne = (ShallowOne) super.clone();
    		
    		//手動操作屬於深拷貝
    		 this.shallowCopyArrayList = (ArrayList<String>)
    		 this.shallowCopyArrayList
    		 .clone();
    
    		return shallowOne;
    	}
    
    }

結果就是

[張三]

深拷貝,兩種物件互為獨立,屬於單獨物件

Eg:final 型別修飾的成員變數不能進行深拷貝

原型模式適合在什麼場景使用?一是類初始化需要消化非常多的資源,這個資源包括資料、硬體資源等;二是通過 new 產生一個物件需要非常繁瑣的資料準備或訪問許可權,則可以使用原型模式;三是一個物件需要提供給其他物件訪問,而且各個呼叫者可能都需要修改其值時,可以考慮使用原型模式拷貝多個物件供呼叫者使用。在實際專案中,原型模式很少單獨出現,一般是和工廠方法模式一起出現,通過 clone的方法建立一個物件,然後由工廠方法提供給呼叫者。原型模式先產生出一個包含大量共有資訊的類,然後可以拷貝出副本,修正細節資訊,建立了一個完整的個性物件。