本文由 Prefert 發表在 ScalaCool 團隊部落格。
第一個生物是怎麼誕生的? 從科學角度推測:是由第一個細胞從核糖核酸(RNA)不斷的新陳代謝演變而來的。
第一個細胞其實是非常孤獨的,但幸好它掌握了「分裂」與「分化」的本領,一定條件下可以一分為二,由此才能快速演變,出現現在的人類。
在開發過程中,我們也常有類似的場景,本文將以細胞分裂為例來介紹原型模式。
定義
「四人幫」設計模式中提及的 原型模式 定義如下:
用原型例項指向建立物件的種類,並且通過拷貝這些原型建立新的物件。
從定義中我們可以知道,原型模式中核心點就是 原型類 和 拷貝 。
看到拷貝,有些同學腦中可能會浮現下面這張圖:
可事實並沒有這麼簡單。
Java 實現
回到開頭的例子,假設細胞沒有分裂能力,每個細胞產生的過程和時間是一樣的,這無疑是費時的。
這也是「原型模式」第一個要解決的問題 — 通過拷貝加速效率。
在 Java 中所有的 class
都繼承自 java.lang.Object
類,Object
提供了一個 clone()
方法,通過它,就能實現物件的拷貝。
淺拷貝
我們利用 Cloneable
介面,來實現細胞的克隆:
public class Cell implements Cloneable {
private String dna;
private Organelle organelle; // 細胞器
... // 省略 get set 與 建構函式
@Override
public String toString() {
return "Cell: {" +
"DNA = " + dna + '\'' +
"Organelle = " + organelle.toString() +
'}';
}
@Override
public Cell clone() {
Cell cellCopy = null;
try {
cellCopy = (Cell) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return cellCopy;
}
}
public class Organelle {
private String cytoplasm; // 細胞質
private String nucleus; // 細胞核
...// 省略get、set、toString() 與建構函式
}複製程式碼
以上我們便能呼叫 clone()
方法對複雜物件進行拷貝,以此來實現分裂的功能。
測試:
Cell cellA, cellB;
cellA = new Cell("AAAGTCTGAC", new Organelle("細胞質", "細胞核"));
System.out.println(cellA);
cellB = cellA.clone();
System.out.println(cellB);
System.out.println("cellA == cellB ? " + (cellA == cellB));
System.out.println("cellA-class == cellB-class? :" + (cellA.getClass() == cellB.getClass()));複製程式碼
看起來不錯!但問題出現了:這裡的 clone
只能拷貝到細胞本身資訊,但不拷貝細胞的引用,不同細胞中包含的細胞器是一樣的。
這其實是「淺拷貝」和「深拷貝」的問題。看看它們的區別:
淺拷貝
僅僅複製原有物件的值,而不復制它對其他物件的引用。深拷貝
原有物件的值和引用都被複制。
驗證:
System.out.println("cellA.Organelle == cellB.Organelle ? " + (cellA.getOrganelle() == cellB.getOrganelle()));複製程式碼
輸出:
cellA.Organelle == cellB.Organelle ? true複製程式碼
可見,當前 clone()
方法執行的是淺拷貝,Java 中所有的物件都儲存在全域性共享的堆中。
只要能拿到某個物件的引用,引用者就可以隨意修改物件,這顯然是不好的。
接下來我為大家介紹一下深拷貝如何實現。
深拷貝
說到深拷貝,一般有兩種實現方案:
1. 改變 clone
方法
既然問題出在細胞器(Organelle
)的引用沒有被複制,為其手動新增上即可。
首先修改引用類,使其支援 clone
:
public class Organelle implements Cloneable {
... // 省略相同程式碼
@Override
protected Object clone() throws CloneNotSupportedException {
Object object = null;
try {
object = super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return object;
}複製程式碼
其次在 Cell
類的 clone()
方法中複製細胞器的引用:
@Override
public Cell clone() throws CloneNotSupportedException {
Cell cellCopy = null;
try {
cellCopy = (Cell) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
if (cellCopy != null) {
cellCopy.organelle = (Organelle) organelle.clone();
}
return cellCopy;
}複製程式碼
測試結果:
cellA.organelle == cellB.organelle ? false複製程式碼
雖然功能是實現了,但是每個引用物件都要重寫 clone()
,太糟糕了!
2. 序列化物件
序列化是一個將物件寫到流的過程,寫到流中的物件是原有物件的一個拷貝,而原物件仍然存在於記憶體中。
與 Cloneable
實現類似,需要序列化的類要求實現序列化介面。
public class Organelle implements Serializable { ... }
public class Cell implements Serializable {
... // 省略部分程式碼
// 序列化實現深拷貝
public Cell deepClone() throws CloneNotSupportedException, IOException, ClassNotFoundException {
// 序列化(將物件寫入流中)
ByteArrayOutputStream bos=new ByteArrayOutputStream();
ObjectOutputStream oos=new ObjectOutputStream(bos);
oos.writeObject(this);
// 反序列化(將物件從流中取出)
ByteArrayInputStream bis=new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois=new ObjectInputStream(bis);
return (Cell)ois.readObject();
}
}複製程式碼
注意:Cloneable 與 Serializable 介面都是 「marker Interface」,即它們只是標識介面,沒有定義任何方法。
對比而言,序列化的實現方式不需要重寫多個類的 clone()
方法,比第一種更加簡便。
接下去看看 Scala 中如何實現原型模式。
Scala 實現
在 Scala 中,你用類似 Java 的方式來實現(Scala 提供了呼叫 Java 中 Cloneable
和 Serializable
的特質)
trait Cloneable extends java.lang.Cloneable
trait Serializable extends Any with java.io.Serializable複製程式碼
當然,Scala 中每個 case class
都擁有一個 copy()
方法,它會返回拷貝自原有例項的新例項,並且可以在拷貝的過程中改變一些值。
同樣以細胞為例:
case class Cell(dna: String, organelle: Organelle)
case class Organelle(cytoplasm: String, nucleus: String)複製程式碼
測試一下:
val initialCell = Cell("AAAGTCTGAC", Organelle("細胞質", "細胞核"))
val cell1 = initialCell.copy()
val cell2 = initialCell.copy()
val cell3 = initialCell.copy(dna = "1234") // 可以在拷貝的時候重新賦值
System.out.println(s"cell1: ${cell1}")
System.out.println(s"cell2: ${cell2}")
System.out.println(s"cell3: ${cell3}")
System.out.println(s"cell1 and cell2 are equal: ${cell1 == cell2}")
// 輸出
Cell 1: Cell(AAAGTCTGAC,Organelle(細胞質,細胞核))
Cell 2: Cell(AAAGTCTGAC,Organelle(細胞質,細胞核))
Cell 3: Cell(1234,Organelle(細胞質,細胞核))
cell1 and cell2 are equal: true複製程式碼
對比 Scala 和 Java 的實現程式碼,有沒有發現 Scala 是如此的簡潔。
誒? 為什麼 cell1
和 cell1
相等? 這會不會導致上面淺拷貝的問題呢?不存在的。
由於 case class
引數預設為 val
,兩個 case class
物件持有相同引用,但也不允許修改。
總結
通過以上內容,我們對原型模式已有一些瞭解,一般來說原型模式中參與者有以下三類:
- 抽象原型類:宣告克隆方法的介面,是所有具體原型類的公共父類,可以是抽象類、介面、甚至具體實現類(對應上面的
Cloneable
和Serializable
介面)。 - 具體原型類:實現抽象原型類宣告的克隆方法,返回自己的一個克隆物件(
Cell.class
|Cell.class
)。 - 客戶類:建立物件並克隆(
Test.class
)。
以下為 Java 與 Scala 中的實現方式對比:
拷貝方式 | Java | Scala |
---|---|---|
淺拷貝 | 具體原型類實現 Cloneable |
具體原型類實現 Cloneable 或 具體原型類為 case class |
深拷貝 | 具體原型類 + 引用類實現 Cloneable 或 Serializable |
具體原型類 + 引用類實現 Cloneable 或 Serializable |
當然原型模式通常還可以解決以下問題:
- 建立新物件成本較大(如初始化需要佔用較長的時間,佔用太多的 CPU 資源或網路資源),新的物件可以通過原型模式對已有物件進行復制來獲得,如果是相似物件,則可以對其成員變數稍作修改。
- 如果系統要儲存物件的狀態,而物件的狀態變化很小,或者物件本身佔用記憶體較少時,可以使用原型模式配合備忘錄模式來實現。
如有錯誤和講述不恰當的地方還請指出,不勝感激!