設計模式--原型模式及其程式設計思想

渊渟岳發表於2024-11-27

原型模式(Prototype Pattern)

原型模式的核心思想是透過複製(克隆)現有物件來建立新物件

原型模式通常涉及兩個角色:原型物件和具體原型物件。原型物件是需要被複制的物件,而具體原型物件是實現了克隆方法的原型物件。

在Java中,原型模式通常透過實現Cloneable介面和重寫clone()方法來實現。當需要建立新物件時,可以直接呼叫原型物件的clone()方法來得到一個新的物件副本,而無需呼叫建構函式或者暴露物件建立的具體細節。

原型模式在一些場景中非常有用,例如:

  • 簡化物件建立:在物件的構造過程比較複雜或時,使用原型模式可以很容易地複製整個物件結構,而不需要關心物件的具體組成和組裝方式。
  • 隱藏具體實現:客戶端可以針對抽象介面進行程式設計,而不需要知道具體的實現細節。
  • 透過為每個執行緒建立 獨立的物件例項 ,來避免不同執行緒對該物件的操作可能會產生資料衝突或者不一致。
  • 在某些情況下,物件的建立過程可能是受限的,而原型模式可以繞過這些限制。

簡單案例

案例概述

簡單實現原型模式,觀察原型建立的物件是不是同一個物件,內容是否相同

// 原型物件和具體原型物件
public class ConcretePrototype implements Cloneable {
private String name;

public ConcretePrototype(String name) {
this.name = name;
}

public String getName() {
return name;
}

@Override
public ConcretePrototype clone() {
try {
return (ConcretePrototype) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
return null;
}
}

}

// 測試程式碼
class PrototypePatternDemo {
public static void main(String[] args) {
ConcretePrototype original = new ConcretePrototype("Prototype1");
// 克隆出新物件
ConcretePrototype cloned = original.clone();
// 觀察輸出的地址和內容
System.out.println(original+"|"+"Original: " + original.getName());
System.out.println(cloned+"|"+"Cloned: " + cloned.getName());
}
}

測試結果:

image

上面的簡單案例是屬於原型模式中的淺複製,除此之外原型模式還有深複製

淺複製

淺複製:建立的新物件和原始物件的基本資料型別屬性會被直接複製,而對於引用型別的屬性,原始物件和克隆物件會共享相同的引用。所以,如果克隆物件和原始物件共享了某些引用型別的屬性,對其中一個物件的修改會影響到另一個物件。

淺複製示例

public class ShallowCopy {

public static void main(String[] args) throws CloneNotSupportedException {
Address addr = new Address("New York");
Person p1 = new Person("John", 25, addr);
System.out.println("複製修改前--地址:"+ p1 + "|" + p1.name + " - " + p1.address.city); // John - New York

Person p2 = (Person) p1.clone();
p2.name = "Denny";
p2.address.city = "Los Angeles"; // 修改了引用型別的屬性
// 引用物件的值被修改後原始物件的值也發生了改變
System.out.println("原始物件地址:"+ p1 + "|" + p1.name + " - " + p1.address.city); // John - Los Angeles
System.out.println("複製物件地址:"+ p2 + "|" + p2.name + " - " + p2.address.city); // Denny - Los Angeles
}
}

class Person implements Cloneable {
String name;
int age;
Address address; // 引用型別

public Person(String name, int age, Address address) {
this.name = name;
this.age = age;
this.address = address;
}

@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone(); // 淺複製
}
}

class Address {
String city;

public Address(String city) {
this.city = city;
}
}

測試結果:

在這個例子中,p1p2 是兩個不同的 Person 物件,但是它們共享同一個 Address 物件的引用。修改 p2address 屬性時,也會影響到 p1

image

深複製

深複製:建立的新物件不僅會複製原物件的基本資料型別屬性,還會遞迴地複製物件中所有引用型別的屬性。深複製確保原始物件和克隆物件之間完全獨立,修改一個物件不會影響另一個物件。

深複製方式1--一個個手動克隆

對應欄位為引用物件的進行一個個例項化新物件並複製物件資料,或者層層往下clone()。適用於,引用物件少的情況。


public class DeepCopy {
public static void main(String[] args) throws CloneNotSupportedException {
Address addr = new Address("New York");
Person p1 = new Person("John", 25, addr);
System.out.println("複製修改前--地址:"+ p1 + "|" + p1.name + " - " + p1.address.city); // John - New York

Person p2 = (Person) p1.clone();
p2.name = "Denny";
p2.address.city = "Los Angeles"; // 修改了引用型別的屬性
// 引用物件的值被修改後原始物件的值--不受影響
System.out.println("原始物件地址:"+ p1 + "|" + p1.name + " - " + p1.address.city); // John - Los Angeles
System.out.println("複製物件地址:"+ p2 + "|" + p2.name + " - " + p2.address.city); // Denny - Los Angeles
}
}
class Person implements Cloneable {
String name;
int age;
Address address; // 引用型別

public Person(String name, int age, Address address) {
this.name = name;
this.age = age;
this.address = address;
}

@Override
protected Object clone() throws CloneNotSupportedException {
Person cloned = (Person) super.clone();
cloned.address = new Address(this.address.city); // 手動克隆引用型別的屬性
return cloned;
}
}

class Address {
String city;

public Address(String city) {
this.city = city;
}
}

測試結果:

在這個例子中,p1p2 是兩個獨立的 Person 物件,且它們的 address 屬性是獨立的。修改 p2address 不會影響 p1

image

深複製方式2--序列化

序列化是將物件轉換為位元組流的過程,而反序列化則是將位元組流轉換回物件。透過序列化和反序列化,可以實現在不依賴於類構造器的情況下建立一個完全獨立的物件副本。這種方式適用於物件較為複雜且具有多個引用的情況。

package org.example.prototype;

import java.io.*;

public class DeepCopySerial {

public static void main(String[] args) {
Address addr = new Address("New York");
Person p1 = new Person("Jack", 25, addr);
System.out.println("原始物件地址:"+ p1 + "|" + p1.name + " - " + p1.address.city); // Jack - New York

// 使用深複製方法複製物件
Person p2 = p1.deepClone();
p2.name = "Denny";
p2.address.city = "Los Angeles"; // 修改引用型別的屬性

// 列印兩個物件的狀態
System.out.println("原始物件地址:"+ p1 + "|" + p1.name + " - " + p1.address.city); // Jack - New York
System.out.println("複製物件地址:"+ p2 + "|" + p2.name + " - " + p2.address.city); // Denny - Los Angeles
}
}

class Address implements Serializable {
String city;

public Address(String city) {
this.city = city;
}
}

class Person implements Serializable {
String name;
int age;
Address address;

public Person(String name, int age, Address address) {
this.name = name;
this.age = age;
this.address = address;
}

// 序列化深複製方法
public Person deepClone() {
try {
// 將當前物件序列化到位元組陣列流
ByteArrayOutputStream byteOutStream = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(byteOutStream);
out.writeObject(this);

// 從位元組陣列流反序列化為新物件
ByteArrayInputStream byteInStream = new ByteArrayInputStream(byteOutStream.toByteArray());
ObjectInputStream in = new ObjectInputStream(byteInStream);
return (Person) in.readObject();
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
return null;
}
}
}

測試結果:

image

優點:

  • 簡便和通用:無論物件的深度如何複雜,序列化和反序列化都可以正確地複製物件。
  • 無需手動處理每個欄位:不需要像手動實現深複製那樣去關心物件的欄位及其引用關係。

缺點:

  • 效能開銷:序列化和反序列化過程相對較慢,尤其是對於大型物件。
  • 依賴Serializable:物件必須實現 Serializable 介面,如果類沒有實現這個介面,無法進行序列化。

深複製方式3--第三方庫

使用第三方庫進行複製,比如Apache Commons Lang 提供了 SerializationUtils.clone() 方法,這個方法透過序列化來實現深複製,使用起來非常簡便。

import org.apache.commons.lang3.SerializationUtils;
import java.io.Serializable;

public class DeepCopyWithLibrary {
public static void main(String[] args) {
Address addr = new Address("New York");
Person p1 = new Person("Jack", 25, addr);
System.out.println("原始物件地址:"+ p1 + "|" + p1.name + " - " + p1.address.city); // Jack - New York

// 使用 Apache Commons 庫進行深複製
Person p2 = SerializationUtils.clone(p1);

p2.name = "Denny";
p2.address.city = "Los Angeles"; // 修改引用型別的屬性

// 列印兩個物件的狀態
System.out.println("原始物件地址:"+ p1 + "|" + p1.name + " - " + p1.address.city); // Jack - New York
System.out.println("複製物件地址:"+ p2 + "|" + p2.name + " - " + p2.address.city); // Denny - Los Angeles
}
}


class Address implements Serializable {
String city;

public Address(String city) {
this.city = city;
}
}

class Person implements Serializable {
String name;
int age;
Address address;

public Person(String name, int age, Address address) {
this.name = name;
this.age = age;
this.address = address;
}
}

測試結果和序列化類似。

看看JDK原始碼怎麼寫的

java.util.Calendar類的複製原始碼,這裡說的就是對應方式1,一個個手動克隆,透過new的方式和層層呼叫clone()方法other.zone = (TimeZone) zone.clone();來實現。

public Object clone()
{
try {
Calendar other = (Calendar) super.clone();

other.fields = new int[FIELD_COUNT];
other.isSet = new boolean[FIELD_COUNT];
other.stamp = new int[FIELD_COUNT];
for (int i = 0; i < FIELD_COUNT; i++) {
other.fields[i] = fields[i];
other.stamp[i] = stamp[i];
other.isSet[i] = isSet[i];
}
other.zone = (TimeZone) zone.clone();
return other;
}
catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError(e);
}
}

java.util.Date類也類似

public Object clone() {
Date d = null;
try {
d = (Date)super.clone();
if (cdate != null) {
d.cdate = (BaseCalendar.Date) cdate.clone();
}
} catch (CloneNotSupportedException e) {} // Won't happen
return d;
}

java.util.concurrent.ConcurrentSkipListSet類,透過new的方式來實現

public ConcurrentSkipListSet<E> clone() {
try {
@SuppressWarnings("unchecked")
ConcurrentSkipListSet<E> clone =
(ConcurrentSkipListSet<E>) super.clone();
clone.setMap(new ConcurrentSkipListMap<E,Object>(m));
return clone;
} catch (CloneNotSupportedException e) {
throw new InternalError();
}
}

還有很多很多,可以自行去檢視學習。

那Spring框架怎麼使用原型模式的呢?

在Spring框架的應用??

在Spring容器中,當應用程式需要獲取一個原型作用域的Bean時,Spring容器會呼叫相應的BeanFactory來建立該Bean的例項。在這種情況下,實際上是使用了工廠模式來建立Bean的例項,而不是原型設計模式

具體來說,Spring框架中的BeanFactory是一種工廠模式的實現,它負責根據配置資訊和需求來建立和管理Bean例項。當我們呼叫getBean()方法來獲取Bean時,實際上是委託給了Spring容器中的BeanFactory來建立一個新的例項物件。每次呼叫getBean()方法都會導致BeanFactory建立一個新的例項物件,這與工廠模式的特徵相符。

在Spring中,原型作用域的Bean確實是在每次呼叫getBean()方法時都會建立一個新的例項物件,這與原型設計模式的概念相似。但是在Spring框架中,並沒有直接使用原型設計模式來實現原型作用域的Bean,而是透過工廠模式來實現的。

@Bean
@Scope("prototype") // 這裡的原型作用域其實是透過工廠模式建立新例項
public MyBean myPrototypeBean() {
return new MyBean();
}

雖然不是使用原型模式,但同樣可以實現原型模式程式設計的核心思想,並且更容易與 Spring 容器的其他功能結合。比如深複製方式2(序列化)並沒有實現Cloneable介面下的clone()方法,但同樣也可以實現原型模式一樣。

總結

我可以實現你提倡的思想,但不一定要走你所說的道路。

還是這句話:設計模式不是簡單地將一個固定的程式碼框架套用到專案中,而是一種嚴謹的程式設計思想,旨在提供解決特定問題的經驗和指導。

超實用的SpringAOP實戰之日誌記錄

軟考中級--軟體設計師毫無保留的備考分享

單例模式及其思想

2023年下半年軟考考試重磅訊息

透過軟考後卻領取不到實體證書?

計算機演算法設計與分析(第5版)

Java全棧學習路線、學習資源和麵試題一條龍

軟考證書=職稱證書?

什麼是設計模式?

相關文章