23種設計模式-原型模式(3)

壹陣上古風發表於2018-09-27

原創作者: chenssy 
出處: 
http://www.cnblogs.com/chenssy/ 
本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連線,否則保留追究法律責任的權利。

以前聽過這樣一句話:“程式設計師的最高境界就是Ctrl+C、Ctrl+V”,我們先不論這句話的對錯,就論這個過程,這個過程我們都知道無非就是複製一個物件,然後將其不斷地貼上。這樣的過程我們可以將其稱之為“克隆”。再如我們應聘的時候列印了那麼多的簡歷。

1111

       克隆我們都清楚,就是用一個物體複製若干個一模一樣物體。同樣,在物件導向系統中,我們同樣可以利用克隆技術來克隆出若干個一模一樣的物件。在應用程式中,有些物件比較複雜,其建立過程過於複雜,而且我們又需要頻繁的利用該物件,如果這個時候我們按照常規思維new該物件,那麼務必會帶來非常多的麻煩,這個時候我們就希望可以利用一個已有的物件來不斷對他進行復制就好了,這就是程式設計中的“克隆”。這裡原型模式就可以滿足我們的“克隆”,在原型模式中我們可以利用過一個原型物件來指明我們所要建立物件的型別,然後通過複製這個物件的方法來獲得與該物件一模一樣的物件例項。這就是原型模式的設計目的。

一、模式定義

 一、模式定義

     通過前面的簡單介紹我們就可以基本確定原型模式的定義了。所謂原型模式就是用原型例項指定建立物件的種類,並且通過複製這些原型建立新的物件。

      在原型模式中,所發動建立的物件通過請求原型物件來拷貝原型物件自己來實現建立過程,當然所發動建立的物件需要知道原型物件的型別。這裡也就是說發動建立的物件只需要知道原型物件的型別就可以獲得更多的原型例項物件,至於這些原型物件時如何建立的根本不需要關心。

      講到原型模式了,我們就不得不區分兩個概念:深拷貝、淺拷貝。

      淺拷貝:使用一個已知例項對新建立例項的成員變數逐個賦值,這個方式被稱為淺拷貝。

     深拷貝:當一個類的拷貝構造方法,不僅要複製物件的所有非引用成員變數值,還要為引用型別的成員變數建立新的例項,並且初始化為形式引數例項值。

11111

      對於深拷貝和淺拷貝的詳細情況,請參考這裡:漸析java的淺拷貝和深拷貝

二、模式結構

二、模式結構     

下圖是原型模式的UML結構圖:

2222

       原型模式主要包含如下三個角色:

       Prototype:抽象原型類。宣告克隆自身的介面。 
       ConcretePrototype:具體原型類。實現克隆的具體操作。 
       Client:客戶類。讓一個原型克隆自身,從而獲得一個新的物件。

      我們都知道Object是祖宗,所有的Java類都繼承至Object,而Object類提供了一個clone()方法,該方法可以將一個java物件複製一份,因此在java中可以直接使用clone()方法來複制一個物件。但是需要實現clone的Java類必須要實現一個介面:Cloneable.該介面表示該類能夠複製且具體複製的能力,如果不實現該介面而直接呼叫clone()方法會丟擲CloneNotSupportedException異常。如下:

public class PrototypeDemo implements Cloneable{
  public Object clone(){
    Object object = null;
    try {
      object = super.clone();
    } catch (CloneNotSupportedException exception) {
      System.err.println("Not support cloneable");
    }
    return object;
    }
    ……
}

      Java中任何實現了Cloneable介面的類都可以通過呼叫clone()方法來複制一份自身然後傳給呼叫者。一般而言,clone()方法滿足: 
      (1) 對任何的物件x,都有x.clone() !=x,即克隆物件與原物件不是同一個物件。 
      (2) 對任何的物件x,都有x.clone().getClass()==x.getClass(),即克隆物件與原物件的型別一樣。 
      (3) 如果物件x的equals()方法定義恰當,那麼x.clone().equals(x)應該成立。

三、模式實現

三、模式實現     


 
public abstract class Prototype implements Cloneable {
    protected ArrayList<String> list = new ArrayList<String>();
 
    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
 
    public abstract void show();
}
  1.  

public class ShallowClone extends Prototype {
    @Override
    public Prototype clone(){
        Prototype prototype = null;
        try {
            prototype = (Prototype)super.clone();
        }
        catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return prototype;
    }
 
    @Override
    public void show(){
        System.out.println("淺克隆");
    }
}
public class DeepClone extends Prototype {
    @SuppressWarnings("unchecked")
    @Override
    public Prototype clone() {
        Prototype prototype = null;
        try {
            prototype = (Prototype)super.clone();
        }
        catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        prototype.list = (ArrayList<String>) this.list.clone();
        return prototype;
    }
 
    @Override
    public void show(){
        System.out.println("深克隆");
    }
}
public class Client {
    public static void main(String[] args) {
        ShallowClone cp = new ShallowClone();
        ShallowClone clonecp = (ShallowClone) cp.clone();
        clonecp.show();
        System.out.println(clonecp.list == cp.list);
 
        DeepClone cp2 = new DeepClone();
        DeepClone clonecp2 = (DeepClone) cp2.clone();
        clonecp2.show();
        System.out.println(clonecp2.list == cp2.list);
    }
}

執行結果:
淺克隆

true

深克隆

false

四、模式優缺點

四、模式優缺點

      1、如果建立新的物件比較複雜時,可以利用原型模式簡化物件的建立過程,同時也能夠提高效率。

      2、可以使用深克隆保持物件的狀態。

      3、原型模式提供了簡化的建立結構。

缺點 

      1、在實現深克隆的時候可能需要比較複雜的程式碼。

      2、需要為每一個類配備一個克隆方法,而且這個克隆方法需要對類的功能進行通盤考慮,這對全新的類來說不是很難,但對已有的類進行改造時,不一定是件容易的事,必須修改其原始碼,違背了“開閉原則”。

五、模式使用場景

      1、如果建立新物件成本較大,我們可以利用已有的物件進行復制來獲得。

      2、如果系統要儲存物件的狀態,而物件的狀態變化很小,或者物件本身佔記憶體不大的時候,也可以使用原型模式配合備忘錄模式來應用。相反,如果物件的狀態變化很大,或者物件佔用的記憶體很大,那麼採用狀態模式會比原型模式更好。 
      3、需要避免使用分層次的工廠類來建立分層次的物件,並且類的例項物件只有一個或很少的幾個組合狀態,通過複製原型物件得到新例項可能比使用建構函式建立一個新例項更加方便。

六、模式總結

      1、原型模式向客戶隱藏了建立物件的複雜性。客戶只需要知道要建立物件的型別,然後通過請求就可以獲得和該物件一模一樣的新物件,無須知道具體的建立過程。

      2、克隆分為淺克隆和深克隆兩種。

      3、我們雖然可以利用原型模式來獲得一個新物件,但有時物件的複製可能會相當的複雜,比如深克隆。

 

clone方法淺拷貝問題:

Java中物件的克隆,為了獲取物件的一份拷貝,我們可以利用Object類的clone()方法。Object類裡的clone方法是淺拷貝。

必須要遵循下面三點: 
1.在派生類中覆蓋基類的clone()方法,並宣告為public【Object類中的clone()方法為protected的】。 
2.在派生類的clone()方法中,呼叫super.clone()。 
3.在派生類中實現Cloneable介面。 

先看以下程式碼:

public class Person implements Cloneable{
    /** 姓名 **/
    private String name;
    
    /** 電子郵件 **/
    private Email email;
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    public Email getEmail() {
        return email;
    }
 
    public void setEmail(Email email) {
        this.email = email;
    }
    
    public Person(String name,Email email){
        this.name  = name;
        this.email = email;
    }
    
    public Person(String name){
        this.name = name;
    }
 
    protected Person clone() {
        Person person = null;
        try {
            person = (Person) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        
        return person;
    }
}
 
 
public class Email {
	private Object name;
	private String content;
	
	public Email(Object name, String content) {
		this.name = name;
		this.content = content;
	}
 
	public String getContent() {
		return content;
	}
	public void setContent(String content) {
		this.content = content;
	}
	public Object getName() {
		return name;
	}
	public void setName(Object name) {
		this.name = name;
	}
}
 
 
public class Client {
    public static void main(String[] args) {
        //寫封郵件
        Email email = new Email("請參加會議","請與今天12:30到二會議室參加會議...");
        
        Person person1 =  new Person("張三",email);
        
        Person person2 =  person1.clone();
        person2.setName("李四");
        Person person3 =  person1.clone();
        person3.setName("王五");
        
        System.out.println(person1.getName() + "的郵件內容是:" + person1.getEmail().getContent());
        System.out.println(person2.getName() + "的郵件內容是:" + person2.getEmail().getContent());
        System.out.println(person3.getName() + "的郵件內容是:" + person3.getEmail().getContent());
    }
}
--------------------
Output:
張三的郵件內容是:請與今天12:30到二會議室參加會議...
李四的郵件內容是:請與今天12:30到二會議室參加會議...
王五的郵件內容是:請與今天12:30到二會議室參加會議...

 

在該應用程式中,首先定義一封郵件,然後將該郵件發給張三、李四、王五三個人,由於他們是使用相同的郵件,並且僅有名字不同,所以使用張三該物件類拷貝李四、王五物件然後更改下名字即可。程式一直到這裡都沒有錯,但是如果我們需要張三提前30分鐘到,即把郵件的內容修改下:

public class Client {
    public static void main(String[] args) {
        //寫封郵件
        Email email = new Email("請參加會議","請與今天12:30到二會議室參加會議...");
        
        Person person1 =  new Person("張三",email);
        
        Person person2 =  person1.clone();
        person2.setName("李四");
        Person person3 =  person1.clone();
        person3.setName("王五");
        
        person1.getEmail().setContent("請與今天12:00到二會議室參加會議...");
        
        System.out.println(person1.getName() + "的郵件內容是:" + person1.getEmail().getContent());
        System.out.println(person2.getName() + "的郵件內容是:" + person2.getEmail().getContent());
        System.out.println(person3.getName() + "的郵件內容是:" + person3.getEmail().getContent());
    }
}

 

在這裡同樣是使用張三該物件實現對李四、王五拷貝,最後將張三的郵件內容改變為:請與今天12:00到二會議室參加會議...。但是結果是:

張三的郵件內容是:請與今天12:00到二會議室參加會議...

李四的郵件內容是:請與今天12:00到二會議室參加會議...

王五的郵件內容是:請與今天12:00到二會議室參加會議...

 

這裡我們就有疑惑為什麼李四和王五的郵件內容也發生改變了呢?其實出現問題的關鍵就在於clone()方法上面,我們知道clone()方法是使用Object類的clone()方法,但是該方法存在一個缺陷,他並不會將物件的所有屬性全部拷貝過來,而是有選擇性的拷貝,基本規則如下:

(1)基本型別:

如果變數是基本型別,則拷貝其值,比如Int、float等。

(2)物件:

如果變數是一個例項物件,則拷貝其地址引用,也就是說此時新物件與原來物件是公用該例項變數。

(3)String字串:

如果變數為String字串,則拷貝其引用地址,但是在修改的時候,它會從字串池中重新生成一個新的字串,原有的字串物件保持不變。

基於上面上面的規則,我們很容易發現問題的所在,他們三者公用一個物件,張三修改了該郵件內容,則李四和王五也會修改,所以才會出現上面的情況。對於這種情況我們還是可以解決的,只需要在clone()方法裡面新建一個物件,然後張三引用該物件即可(深拷貝):

protected Person clone() {
        Person person = null;
        try {
            person = (Person) super.clone();
            person.setEmail(new Email(person.getEmail().getObject(),person.getEmail().getContent()));
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        
        return person;
    }

​​​​​​​

所以:淺拷貝只是Java提供的一種簡單的拷貝機制,不便於直接使用。

對於上面的解決方案還是存在一個問題,若我們系統中存在大量的物件是通過拷貝生成的,如果我們每一個類都寫一個clone()方法,並將還需要進行深拷貝,新建大量的物件,這個工程是非常大的,這裡我們可以利用序列化來實現物件的拷貝。

 

原部落格連結:

https://blog.csdn.net/jason0539/article/details/23158081

https://blog.csdn.net/jx_870915876/article/details/52123475

相關文章