系列導讀:完成創建型模式的學習,我們來看最后一個創建型模式——原型模式。它通過復制已有對象來創建新對象,是一種獨特的創建方式。
解決什么問題:通過復制現有對象來創建新對象,而不是重新實例化。適用于對象創建成本高、需要保持狀態的場景。
在實際開發中,有時候創建一個對象的成本很高,比如需要從數據庫查詢大量數據、進行復雜計算、或者建立網絡連接等。如果需要創建多個相似的對象,每次都重新執行這些操作就太浪費了。
原型模式提供了一個聰明的解決方案:先創建一個原型對象,然后通過復制這個原型來創建新對象。這樣既保留了對象的狀態,又避免了重復的創建成本。
本文在系列中的位置:
- 前置知識:建造者模式
- 系列角色:創建型模式收尾
- 難度等級:★★★☆☆(需要理解深拷貝和淺拷貝)
- 后續學習:結構型模式:適配器模式
目錄
- 1. 模式概述
- 2. 使用場景
- 3. 優缺點分析
- 4. 實際應用案例
- 5. 結構與UML類圖
- 6. 代碼示例
- 7. 測試用例
- 8. 常見誤區與反例
- 9. 最佳實踐
- 10. 參考資料與延伸閱讀
1. 模式概述
原型模式(Prototype Pattern)是一種創建型設計模式。它通過克隆現有對象來創建新對象,而不是通過實例化類來創建。適用于對象創建成本高、狀態復雜或需批量復制的場景。
1.1 定義
用原型實例指定創建對象的種類,并且通過拷貝這些原型創建新的對象。
1.2 目的
- 對象復制,提升效率
- 避免子類膨脹,簡化擴展
- 支持動態配置和運行時擴展
2. 使用場景
原型模式在實際開發中應用廣泛,常見場景包括:
- 文檔/模板復制:如Word、PPT、設計稿等模板批量生成。
- 游戲開發:如角色、道具、場景等對象池批量克隆。
- 緩存/對象池:如數據庫連接、線程、任務等對象池管理。
- 配置/環境克隆:如系統配置、環境參數的快速復制。
真實業務背景舉例:
- OA系統支持一鍵復制審批單、合同、報表等,原型模式可高效實現。
- 游戲引擎批量生成怪物、NPC、道具等,原型模式提升性能。
- 云平臺環境模板、配置模板的快速克隆。
3. 優缺點分析
3.1 優點
- 性能優化:減少重復創建開銷,提升系統響應速度。
- 簡化擴展:無需大量子類,支持動態擴展和運行時配置。
- 靈活性高:可動態注冊、批量復制,適應多變需求。
3.2 缺點
- 深拷貝復雜:對象關系復雜時,深拷貝實現難度大。
- 克隆限制:如不可變對象、資源句柄等難以克隆。
- 維護成本:需保證克隆對象狀態一致性,易出錯。
4. 實際應用案例
- 文檔/模板復制:如Word、PPT、設計稿等模板批量生成。
- 游戲開發:如角色、道具、場景等對象池批量克隆。
- 緩存/對象池:如數據庫連接、線程、任務等對象池管理。
- 配置/環境克隆:如系統配置、環境參數的快速復制。
5. 結構與UML類圖
@startuml
package "Prototype Pattern" #DDDDDD {interface Prototype {+ clone(): Prototype}class ConcretePrototype implements Prototype {- field: String+ clone(): Prototype}class PrototypeRegistry {+ addPrototype(key: String, prototype: Prototype): void+ getPrototype(key: String): Prototype- prototypes: Map<String, Prototype>}Prototype <|.. ConcretePrototypePrototypeRegistry o-- Prototype : prototypes
}
@enduml
6. 代碼示例
6.1 基本結構示例
業務背景: 實現原型模式的基本結構,支持對象克隆和注冊表管理。
package com.example.patterns.prototype;import java.util.HashMap;
import java.util.Map;
import java.util.Objects;// 原型接口,定義克隆方法
public interface Prototype {/*** 克隆當前對象* @return 克隆后的新對象*/Prototype clone();
}// 具體原型實現,支持基本屬性克隆
public class ConcretePrototype implements Prototype {private String field;private int value;public ConcretePrototype(String field, int value) { this.field = field;this.value = value;}// 拷貝構造函數,用于克隆private ConcretePrototype(ConcretePrototype other) {this.field = other.field;this.value = other.value;}@Overridepublic Prototype clone() { return new ConcretePrototype(this); }// Getter和Setter方法public String getField() { return field; }public void setField(String field) { this.field = field; }public int getValue() { return value; }public void setValue(int value) { this.value = value; }@Overridepublic String toString() {return "ConcretePrototype{field='" + field + "', value=" + value + "}";}@Overridepublic boolean equals(Object o) {if (this == o) return true;if (o == null || getClass() != o.getClass()) return false;ConcretePrototype that = (ConcretePrototype) o;return value == that.value && Objects.equals(field, that.field);}@Overridepublic int hashCode() {return Objects.hash(field, value);}
}// 原型注冊表,管理原型對象
public class PrototypeRegistry {private static final Map<String, Prototype> prototypes = new HashMap<>();/*** 注冊原型對象*/public static void addPrototype(String key, Prototype prototype) {if (key == null || prototype == null) {throw new IllegalArgumentException("Key and prototype cannot be null");}prototypes.put(key, prototype);}/*** 獲取原型的克隆對象*/public static Prototype getPrototype(String key) {Prototype prototype = prototypes.get(key);if (prototype == null) {throw new IllegalArgumentException("Prototype not found for key: " + key);}return prototype.clone();}/*** 移除原型*/public static void removePrototype(String key) {prototypes.remove(key);}/*** 清空所有原型*/public static void clear() {prototypes.clear();}
}
6.2 深拷貝實現示例
業務背景: 實現包含復雜對象的深拷貝,避免引用共享問題。
// 復雜對象示例:員工信息包含地址對象
public class Address implements Cloneable {private String city;private String street;private String zipCode;public Address(String city, String street, String zipCode) {this.city = city;this.street = street;this.zipCode = zipCode;}// 深拷貝實現@Overridepublic Address clone() {try {return (Address) super.clone();} catch (CloneNotSupportedException e) {// 這種情況不應該發生,因為我們實現了Cloneablethrow new RuntimeException("Clone not supported", e);}}// Getter和Setter方法public String getCity() { return city; }public void setCity(String city) { this.city = city; }public String getStreet() { return street; }public void setStreet(String street) { this.street = street; }public String getZipCode() { return zipCode; }public void setZipCode(String zipCode) { this.zipCode = zipCode; }@Overridepublic String toString() {return "Address{city='" + city + "', street='" + street + "', zipCode='" + zipCode + "'}";}
}// 員工原型,包含復雜對象引用
public class Employee implements Prototype {private String name;private String department;private Address address;private List<String> skills;public Employee(String name, String department, Address address) {this.name = name;this.department = department;this.address = address;this.skills = new ArrayList<>();}// 深拷貝構造函數private Employee(Employee other) {this.name = other.name;this.department = other.department;// 深拷貝地址對象this.address = other.address != null ? other.address.clone() : null;// 深拷貝技能列表this.skills = new ArrayList<>(other.skills);}@Overridepublic Prototype clone() {return new Employee(this);}public void addSkill(String skill) {skills.add(skill);}// Getter和Setter方法public String getName() { return name; }public void setName(String name) { this.name = name; }public String getDepartment() { return department; }public void setDepartment(String department) { this.department = department; }public Address getAddress() { return address; }public void setAddress(Address address) { this.address = address; }public List<String> getSkills() { return skills; }@Overridepublic String toString() {return "Employee{name='" + name + "', department='" + department + "', address=" + address + ", skills=" + skills + "}";}
}
6.3 序列化克隆實現
業務背景: 使用序列化方式實現深拷貝,適用于復雜對象圖。
import java.io.*;// 支持序列化克隆的基類
public abstract class SerializablePrototype implements Prototype, Serializable {private static final long serialVersionUID = 1L;@Overridepublic Prototype clone() {try {// 使用序列化進行深拷貝ByteArrayOutputStream bos = new ByteArrayOutputStream();ObjectOutputStream oos = new ObjectOutputStream(bos);oos.writeObject(this);oos.close();ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());ObjectInputStream ois = new ObjectInputStream(bis);Prototype cloned = (Prototype) ois.readObject();ois.close();return cloned;} catch (IOException | ClassNotFoundException e) {throw new RuntimeException("Serialization clone failed", e);}}
}// 使用序列化克隆的配置對象
public class Configuration extends SerializablePrototype {private String environment;private Map<String, String> properties;private List<String> servers;public Configuration(String environment) {this.environment = environment;this.properties = new HashMap<>();this.servers = new ArrayList<>();}public void addProperty(String key, String value) {properties.put(key, value);}public void addServer(String server) {servers.add(server);}// Getter方法public String getEnvironment() { return environment; }public Map<String, String> getProperties() { return properties; }public List<String> getServers() { return servers; }@Overridepublic String toString() {return "Configuration{environment='" + environment + "', properties=" + properties + ", servers=" + servers + "}";}
}
6.4 實際業務場景:文檔模板系統
業務背景: 實現文檔模板系統,支持快速復制和定制不同類型的文檔。
// 文檔接口
public interface Document extends Prototype {void setTitle(String title);void setContent(String content);String getTitle();String getContent();
}// 報告文檔原型
public class ReportDocument implements Document {private String title;private String content;private String author;private Date createDate;private List<String> sections;public ReportDocument() {this.createDate = new Date();this.sections = new ArrayList<>();// 預設報告模板結構this.sections.add("摘要");this.sections.add("背景");this.sections.add("分析");this.sections.add("結論");}// 拷貝構造函數private ReportDocument(ReportDocument other) {this.title = other.title;this.content = other.content;this.author = other.author;this.createDate = new Date(); // 新文檔使用當前時間this.sections = new ArrayList<>(other.sections);}@Overridepublic Prototype clone() {return new ReportDocument(this);}@Overridepublic void setTitle(String title) { this.title = title; }@Overridepublic void setContent(String content) { this.content = content; }@Overridepublic String getTitle() { return title; }@Overridepublic String getContent() { return content; }public void setAuthor(String author) { this.author = author; }public String getAuthor() { return author; }public List<String> getSections() { return sections; }@Overridepublic String toString() {return "ReportDocument{title='" + title + "', author='" + author + "', sections=" + sections.size() + ", createDate=" + createDate + "}";}
}// 合同文檔原型
public class ContractDocument implements Document {private String title;private String content;private String partyA;private String partyB;private Date effectiveDate;private BigDecimal amount;public ContractDocument() {this.effectiveDate = new Date();this.amount = BigDecimal.ZERO;}private ContractDocument(ContractDocument other) {this.title = other.title;this.content = other.content;this.partyA = other.partyA;this.partyB = other.partyB;this.effectiveDate = new Date(); // 新合同使用當前日期this.amount = other.amount;}@Overridepublic Prototype clone() {return new ContractDocument(this);}@Overridepublic void setTitle(String title) { this.title = title; }@Overridepublic void setContent(String content) { this.content = content; }@Overridepublic String getTitle() { return title; }@Overridepublic String getContent() { return content; }// 合同特有方法public void setPartyA(String partyA) { this.partyA = partyA; }public void setPartyB(String partyB) { this.partyB = partyB; }public void setAmount(BigDecimal amount) { this.amount = amount; }@Overridepublic String toString() {return "ContractDocument{title='" + title + "', partyA='" + partyA + "', partyB='" + partyB + "', amount=" + amount + "}";}
}// 文檔管理器
public class DocumentManager {private static final PrototypeRegistry registry = new PrototypeRegistry();static {// 初始化文檔模板ReportDocument reportTemplate = new ReportDocument();reportTemplate.setTitle("月度報告模板");reportTemplate.setAuthor("系統管理員");ContractDocument contractTemplate = new ContractDocument();contractTemplate.setTitle("標準合同模板");contractTemplate.setPartyA("甲方公司");registry.addPrototype("report", reportTemplate);registry.addPrototype("contract", contractTemplate);}public static Document createDocument(String type, String title) {Document doc = (Document) registry.getPrototype(type);doc.setTitle(title);return doc;}
}// 客戶端使用示例
public class DocumentClient {public static void main(String[] args) {// 創建報告文檔Document report1 = DocumentManager.createDocument("report", "Q1財務報告");report1.setContent("第一季度財務分析內容...");Document report2 = DocumentManager.createDocument("report", "Q2財務報告");report2.setContent("第二季度財務分析內容...");// 創建合同文檔Document contract1 = DocumentManager.createDocument("contract", "軟件開發合同");contract1.setContent("軟件開發合同條款...");System.out.println("Report 1: " + report1);System.out.println("Report 2: " + report2);System.out.println("Contract: " + contract1);}// 總結:通過原型模式,文檔系統可快速創建各種類型文檔,提升效率和一致性。
}## 7. 測試用例**業務背景:** 驗證原型模式的核心功能,包括基本克隆、深拷貝和注冊表管理。```java
import org.junit.Test;
import static org.junit.Assert.*;public class PrototypePatternTest {@Testpublic void testBasicClone() {// 測試基本克隆功能ConcretePrototype original = new ConcretePrototype("test", 42);ConcretePrototype cloned = (ConcretePrototype) original.clone();// 驗證克隆對象與原對象不是同一個實例assertNotSame(original, cloned);// 驗證克隆對象的內容相同assertEquals(original.getField(), cloned.getField());assertEquals(original.getValue(), cloned.getValue());assertEquals(original, cloned);}@Testpublic void testPrototypeRegistry() {// 清空注冊表PrototypeRegistry.clear();// 注冊原型ConcretePrototype prototype = new ConcretePrototype("template", 100);PrototypeRegistry.addPrototype("test", prototype);// 獲取克隆對象Prototype cloned = PrototypeRegistry.getPrototype("test");ConcretePrototype clonedConcrete = (ConcretePrototype) cloned;assertEquals("template", clonedConcrete.getField());assertEquals(100, clonedConcrete.getValue());assertNotSame(prototype, cloned);}@Testpublic void testDeepCopy() {// 測試深拷貝Address address = new Address("北京", "中關村大街", "100080");Employee original = new Employee("張三", "技術部", address);original.addSkill("Java");original.addSkill("Spring");Employee cloned = (Employee) original.clone();// 驗證深拷貝:修改克隆對象的地址不影響原對象cloned.getAddress().setCity("上海");cloned.addSkill("Python");assertEquals("北京", original.getAddress().getCity());assertEquals("上海", cloned.getAddress().getCity());assertEquals(2, original.getSkills().size());assertEquals(3, cloned.getSkills().size());}@Testpublic void testDocumentSystem() {// 測試文檔模板系統Document report1 = DocumentManager.createDocument("report", "Q1報告");Document report2 = DocumentManager.createDocument("report", "Q2報告");// 驗證不同的文檔實例assertNotSame(report1, report2);assertEquals("Q1報告", report1.getTitle());assertEquals("Q2報告", report2.getTitle());// 驗證報告文檔的特定屬性assertTrue(report1 instanceof ReportDocument);assertTrue(report2 instanceof ReportDocument);}@Testpublic void testRegistryExceptionHandling() {// 測試注冊表異常處理assertThrows(IllegalArgumentException.class, () -> {PrototypeRegistry.addPrototype(null, new ConcretePrototype("test", 1));});assertThrows(IllegalArgumentException.class, () -> {PrototypeRegistry.addPrototype("test", null);});assertThrows(IllegalArgumentException.class, () -> {PrototypeRegistry.getPrototype("nonexistent");});}
}
8. 常見誤區與反例
8.1 常見誤區
-
誤區1 :淺拷貝導致引用共享
// 錯誤示例:簡單的字段復制導致引用共享 public class BadEmployee implements Prototype {private Address address;public Prototype clone() {BadEmployee clone = new BadEmployee();clone.address = this.address; // 淺拷貝,共享引用return clone;} }
正確做法:實現深拷貝,確保引用對象也被克隆。
-
誤區2 :原型注冊表未做防護
// 錯誤示例:直接返回原型對象 public static Prototype getPrototype(String key) {return prototypes.get(key); // 返回原始對象,可能被修改 }
正確做法:始終返回克隆對象,保護原型不被修改。
-
誤區3 :忽略克隆中的資源管理
// 錯誤示例:資源句柄直接克隆 public class BadResource implements Prototype {private FileInputStream inputStream;public Prototype clone() {BadResource clone = new BadResource();clone.inputStream = this.inputStream; // 資源不能共享return clone;} }
8.2 反例分析
-
反例1 :不可變對象使用原型模式
對于String
、Integer
等不可變對象,使用原型模式沒有意義,應直接重用。 -
反例2 :系統資源句柄克隆
文件句柄、網絡連接、數據庫連接等系統資源不能簡單克隆,需要重新創建。 -
反例3 :過度使用原型模式
對于簡單對象,直接使用構造函數創建更簡單高效,不需要原型模式。
9. 最佳實踐
9.1 設計原則
-
深拷貝實現 :優先實現深拷貝,避免引用共享問題
// 推薦:使用拷貝構造函數 private Employee(Employee other) {this.name = other.name;this.address = other.address != null ? other.address.clone() : null;this.skills = new ArrayList<>(other.skills); }
-
注冊表防護 :注冊表應返回克隆對象,保護原型安全
// 推薦:防護性克隆 public static Prototype getPrototype(String key) {Prototype prototype = prototypes.get(key);return prototype != null ? prototype.clone() : null; }
-
異常與資源管理 :妥善處理克隆過程中的異常和資源
// 推薦:完善的異常處理 @Override public Prototype clone() {try {MyClass cloned = (MyClass) super.clone();cloned.resource = createNewResource(); // 重新創建資源return cloned;} catch (CloneNotSupportedException e) {throw new RuntimeException("Clone failed", e);} }
9.2 性能優化
-
克隆策略選擇 :根據對象復雜度選擇合適的克隆策略
- 簡單對象:拷貝構造函數
- 復雜對象:序列化克隆
- 不可變部分:引用復制
-
延遲克隆 :對于大對象,考慮寫時復制(Copy-on-Write)策略
// 推薦:寫時復制優化 public class LazyCloneList implements Prototype {private List<String> data;private boolean isCloned = false;private void ensureCloned() {if (!isCloned) {data = new ArrayList<>(data);isCloned = true;}}public void add(String item) {ensureCloned();data.add(item);} }
9.3 架構設計
-
與工廠模式結合 :原型注冊表可與工廠模式結合,提供統一的對象創建接口
-
線程安全考慮 :在多線程環境下,確保原型注冊表和克隆操作的線程安全
// 推薦:線程安全的注冊表 public class ThreadSafeRegistry {private static final ConcurrentHashMap<String, Prototype> prototypes = new ConcurrentHashMap<>(); }
-
版本控制 :為原型對象添加版本信息,支持向后兼容的序列化克隆
10. 參考資料與延伸閱讀
- 《設計模式:可復用面向對象軟件的基礎》GoF
- Effective Java(中文版)
- https://refactoringguru.cn/design-patterns/prototype
- https://www.baeldung.com/java-prototype-pattern
本文為設計模式系列第8篇,后續每篇將聚焦一個設計模式或設計原則,深入講解實現與應用,敬請關注。