第二部分:創建型模式 - 5. 原型模式 (Prototype Pattern)
我們已經探討了單例、工廠方法、抽象工廠和生成器模式。現在,我們來看創建型模式的最后一個主要成員——原型模式。這種模式關注的是通過復制現有對象來創建新對象,而不是通過傳統的構造函數實例化。
- 核心思想:用原型實例指定創建對象的種類,并且通過拷貝這些原型創建新的對象。
原型模式 (Prototype Pattern)
“用原型實例指定創建對象的種類,并且通過拷貝這些原型創建新的對象。”
想象一下細胞分裂:一個細胞(原型)可以通過分裂(克隆)產生一個新的、與自身幾乎完全相同的細胞。或者,在繪圖軟件中,你畫了一個復雜的圖形(原型),然后可以通過“復制”和“粘貼”操作快速創建多個相同的圖形副本,再對副本進行微調。
原型模式的核心就是“克隆”或“復制”。當創建一個對象的成本很高(例如,需要復雜的計算、數據庫查詢或網絡請求)時,如果已經有一個相似的對象存在,通過復制這個現有對象來創建新對象可能會更高效。
1. 目的 (Intent)
原型模式的主要目的:
- 提高性能:當創建新對象的成本較大時(如初始化時間長、資源消耗多),通過復制已有的原型實例來創建新對象,可以避免重復執行這些昂貴的初始化操作。
- 簡化對象創建:如果一個對象的創建過程比較復雜,或者需要依賴某些運行時的狀態,通過克隆一個已配置好的原型對象可以簡化新對象的創建。
- 動態添加或刪除產品:可以在運行時通過注冊原型實例來動態地增加或刪除系統中可用的產品類型,而無需修改工廠類(如果與工廠模式結合使用)。
- 避免與產品具體類耦合:客戶端可以只知道抽象的原型接口,通過克隆來獲取新對象,而無需知道具體的實現類名。
2. 生活中的例子 (Real-world Analogy)
-
復印文件:
- 原型 (Prototype):原始文件(比如一份合同模板)。
- 克隆操作 (Clone):復印機復印的過程。
- 新對象 (Cloned Object):復印出來的文件副本。
你不需要重新打字排版來得到一份新的合同,只需要復印原件,然后在副本上修改少量信息(如簽約方、日期)即可。
-
生物克隆:如克隆羊多莉。多莉就是通過復制現有羊的細胞(原型)而創建的。
-
制作模具和鑄件:
- 原型:一個精心制作的模具。
- 克隆操作:使用模具進行澆筑。
- 新對象:通過模具生產出來的多個相同鑄件。
-
游戲中的敵人復制:在一個游戲中,當需要生成大量相同類型的敵人時,可以先創建一個敵人對象作為原型,并設置好其初始屬性(如生命值、攻擊力、模型等)。之后需要新的敵人時,直接克隆這個原型,而不是每次都從頭加載資源和設置屬性。
3. 結構 (Structure)
原型模式的結構相對簡單,通常包含以下角色:
- Prototype (抽象原型):聲明一個克隆自身的接口(通常是一個
clone()
方法)。 - ConcretePrototype (具體原型):實現 Prototype 接口,重寫
clone()
方法來復制自身。這個類是實際被復制的對象。 - Client (客戶端):讓一個原型克隆自身從而創建一個新的對象。客戶端不需要知道具體的原型類名,只需要通過抽象原型接口來操作。
克隆過程: - 客戶端持有一個抽象原型對象的引用。
- 當客戶端需要一個新的對象時,它調用原型對象的
clone()
方法。 - 具體原型類實現
clone()
方法,創建一個當前對象的副本并返回。 - 客戶端得到一個新的對象,這個新對象與原型對象具有相同的初始狀態(屬性值)。
4. 深拷貝 vs. 淺拷貝 (Deep Copy vs. Shallow Copy)
這是原型模式中一個非常重要的概念。
-
淺拷貝 (Shallow Copy):
- 當復制一個對象時,只復制對象本身和其中的基本數據類型成員的值。
- 如果對象包含對其他對象的引用(引用類型成員),則只復制這些引用,而不復制引用所指向的對象。因此,原對象和副本中的引用類型成員將指向內存中的同一個對象。
- 修改副本中的引用類型成員所指向的對象,會影響到原對象中對應的成員(因為它們指向同一個東西)。
-
深拷貝 (Deep Copy):
- 當復制一個對象時,不僅復制對象本身和基本數據類型成員,還會遞歸地復制所有引用類型成員所指向的對象。
- 原對象和副本中的引用類型成員將指向不同的、內容相同的對象。
- 修改副本中的引用類型成員所指向的對象,不會影響到原對象。
選擇深拷貝還是淺拷貝取決于具體需求。如果希望副本的修改不影響原型,或者原型和副本需要獨立地管理其引用的對象,那么應該使用深拷貝。如果共享引用的對象是不可變的,或者業務邏輯允許共享,那么淺拷貝可能就足夠了,并且性能通常更高。
在Java中,Object
類的 clone()
方法默認執行的是淺拷貝。要實現深拷貝,需要在 clone()
方法中對引用類型的字段也進行遞歸克隆。
在Go中,沒有內建的 clone()
方法。復制通常通過創建一個新實例并手動復制字段值來完成。對于引用類型(如切片、映射、指針),需要特別注意是復制引用還是復制底層數據。
5. 適用場景 (When to Use)
- 當一個系統應該獨立于它的產品創建、構成和表示時,并且你想要在運行時指定實例化的類。
- 當要實例化的類是在運行時指定時,例如,通過動態裝載。
- 為了避免創建一個與產品類層次平行的工廠類層次時(即不想為了創建不同產品而創建一堆工廠類)。
- 當一個類的實例只能有幾種不同狀態組合中的一種時。建立相應數目的原型并克隆它們可能比每次用合適的狀態手工實例化該類更方便一些。
- 創建對象的成本很高:例如,對象初始化需要大量計算、I/O操作或網絡通信。
- 需要創建大量相似對象:只有少量屬性不同的對象。
- 系統需要在運行時動態添加或修改可創建的對象類型。
6. 優缺點 (Pros and Cons)
優點:
- 性能提升:對于創建成本高的對象,克隆比重新創建更快。
- 簡化對象創建:可以復制一個已經初始化好的復雜對象,避免重復的初始化邏輯。
- 靈活性高:可以在運行時動態地獲取和復制原型對象。
- 對客戶端隱藏具體類型:客戶端只需要知道抽象原型接口即可創建對象。
缺點:
- 需要為每個類實現克隆方法:每個需要作為原型的類都必須實現
clone()
方法,這可能比較繁瑣,特別是當類層次結構很深或包含許多字段時。 - 深拷貝實現復雜:正確實現深拷貝可能比較復雜,需要仔細處理所有引用類型的成員,以避免意外共享或循環引用問題。
- 可能違反開閉原則(如果克隆邏輯復雜):如果克隆邏輯非常復雜且依賴于具體類的內部結構,當具體類修改時,克隆方法可能也需要修改。
7. 實現方式 (Implementations)
讓我們通過一個圖形繪制的例子來看看原型模式的實現。假設我們有不同形狀(圓形、矩形)的對象,它們可以被克隆。
抽象原型 (Shape)
// shape.go
package shapeimport "fmt"// Shape 抽象原型接口
type Shape interface {Clone() ShapeDraw()SetID(id string)GetID() string
}
// Shape.java
package com.example.shape;// 抽象原型接口
// Java 中通常讓原型類實現 Cloneable 接口并重寫 clone() 方法
public interface Shape extends Cloneable { // Cloneable 是一個標記接口Shape cloneShape(); // 自定義一個更明確的克隆方法名void draw();void setId(String id);String getId();
}
具體原型 (Circle, Rectangle)
// circle.go
package shapeimport "fmt"// Circle 具體原型
type Circle struct {ID stringRadius intX, Y int // 圓心坐標
}func NewCircle(id string, radius, x, y int) *Circle {return &Circle{ID: id, Radius: radius, X: x, Y: y}
}func (c *Circle) SetID(id string) { c.ID = id }
func (c *Circle) GetID() string { return c.ID }// Clone 實現淺拷貝,因為 Circle 的字段都是值類型或string (string在Go中是值類型行為)
func (c *Circle) Clone() Shape {return &Circle{ID: c.ID + "_clone", // 給克隆體一個新IDRadius: c.Radius,X: c.X,Y: c.Y,}
}func (c *Circle) Draw() {fmt.Printf("Drawing Circle [ID: %s, Radius: %d, Center: (%d,%d)]\n", c.ID, c.Radius, c.X, c.Y)
}// rectangle.go
package shapeimport "fmt"// Rectangle 具體原型
type Rectangle struct {ID stringWidth, Height intX, Y int // 左上角坐標
}func NewRectangle(id string, width, height, x, y int) *Rectangle {return &Rectangle{ID: id, Width: width, Height: height, X: x, Y: y}
}func (r *Rectangle) SetID(id string) { r.ID = id }
func (r *Rectangle) GetID() string { return r.ID }// Clone 實現淺拷貝
func (r *Rectangle) Clone() Shape {return &Rectangle{ID: r.ID + "_clone",Width: r.Width,Height: r.Height,X: r.X,Y: r.Y,}
}func (r *Rectangle) Draw() {fmt.Printf("Drawing Rectangle [ID: %s, Width: %d, Height: %d, TopLeft: (%d,%d)]\n", r.ID, r.Width, r.Height, r.X, r.Y)
}
// Circle.java
package com.example.shape;// 具體原型
public class Circle implements Shape {private String id;private int radius;private Point center; // 假設 Point 是一個自定義的可變類public Circle(String id, int radius, int x, int y) {this.id = id;this.radius = radius;this.center = new Point(x, y);System.out.println("Circle created with ID: " + id);}// 私有構造,用于克隆private Circle(String id, int radius, Point center) {this.id = id;this.radius = radius;this.center = center; // 注意這里,如果是淺拷貝,center會被共享}@Overridepublic void setId(String id) { this.id = id; }@Overridepublic String getId() { return this.id; }public int getRadius() { return radius; }public Point getCenter() { return center; }public void setCenter(int x, int y) { this.center.setX(x); this.center.setY(y); }@Overridepublic Shape cloneShape() {System.out.println("Cloning Circle with ID: " + this.id);Circle clonedCircle = null;try {// 默認的 Object.clone() 是淺拷貝clonedCircle = (Circle) super.clone(); // 為了實現深拷貝,需要對可變引用類型字段進行單獨克隆clonedCircle.id = this.id + "_clone"; // 通常給克隆體新IDclonedCircle.center = (Point) this.center.clone(); // 假設 Point 也實現了 Cloneable 和 clone()} catch (CloneNotSupportedException e) {// This should not happen if we implement Cloneablee.printStackTrace();}return clonedCircle;}@Overridepublic void draw() {System.out.printf("Drawing Circle [ID: %s, Radius: %d, Center: %s]%n", id, radius, center);}
}// Rectangle.java
package com.example.shape;public class Rectangle implements Shape {private String id;private int width;private int height;private Point topLeft; // 可變引用類型public Rectangle(String id, int width, int height, int x, int y) {this.id = id;this.width = width;this.height = height;this.topLeft = new Point(x,y);System.out.println("Rectangle created with ID: " + id);}@Overridepublic void setId(String id) { this.id = id; }@Overridepublic String getId() { return this.id; }public Point getTopLeft() { return topLeft; }public void setTopLeft(int x, int y) { this.topLeft.setX(x); this.topLeft.setY(y); }@Overridepublic Shape cloneShape() {System.out.println("Cloning Rectangle with ID: " + this.id);Rectangle clonedRectangle = null;try {clonedRectangle = (Rectangle) super.clone();clonedRectangle.id = this.id + "_clone";clonedRectangle.topLeft = (Point) this.topLeft.clone(); // 深拷貝 Point} catch (CloneNotSupportedException e) {e.printStackTrace();}return clonedRectangle;}@Overridepublic void draw() {System.out.printf("Drawing Rectangle [ID: %s, Width: %d, Height: %d, TopLeft: %s]%n", id, width, height, topLeft);}
}// Point.java (輔助類,用于演示深拷貝)
package com.example.shape;public class Point implements Cloneable {private int x;private int y;public Point(int x, int y) { this.x = x; this.y = y; }public int getX() { return x; }public void setX(int x) { this.x = x; }public int getY() { return y; }public void setY(int y) { this.y = y; }@Overridepublic String toString() { return "(" + x + "," + y + ")"; }@Overrideprotected Object clone() throws CloneNotSupportedException {// Point 只包含基本類型,所以 super.clone() 已經是深拷貝效果了// 如果 Point 內部還有其他引用類型,則需要進一步處理return super.clone();}
}
原型管理器 (可選, PrototypeManager / ShapeCache)
有時會引入一個原型管理器類,用于存儲和檢索原型實例。客戶端向管理器請求一個特定類型的原型,然后克隆它。
// shape_cache.go
package shapeimport "fmt"// ShapeCache 原型管理器
type ShapeCache struct {prototypes map[string]Shape
}func NewShapeCache() *ShapeCache {cache := &ShapeCache{prototypes: make(map[string]Shape)}cache.loadCache()return cache
}// loadCache 初始化原型實例并存儲
func (sc *ShapeCache) loadCache() {circle := NewCircle("circle1", 10, 0, 0)rectangle := NewRectangle("rect1", 20, 10, 0, 0)sc.prototypes[circle.GetID()] = circlesc.prototypes[rectangle.GetID()] = rectanglefmt.Println("ShapeCache: Prototypes loaded.")
}// GetShape 克隆并返回指定ID的原型
func (sc *ShapeCache) GetShape(id string) (Shape, error) {prototype, found := sc.prototypes[id]if !found {return nil, fmt.Errorf("prototype with id '%s' not found", id)}return prototype.Clone(), nil
}// AddShape 允許運行時添加新的原型
func (sc *ShapeCache) AddShape(id string, shape Shape) {sc.prototypes[id] = shapefmt.Printf("ShapeCache: Prototype '%s' added.\n", id)
}
// ShapeCache.java
package com.example.shape;import java.util.Hashtable;// 原型管理器
public class ShapeCache {private static Hashtable<String, Shape> shapeMap = new Hashtable<>();public static Shape getShape(String shapeId) throws CloneNotSupportedException {Shape cachedShape = shapeMap.get(shapeId);if (cachedShape == null) {System.err.println("ShapeCache: Prototype with ID '" + shapeId + "' not found.");return null;}System.out.println("ShapeCache: Returning clone of prototype with ID: " + shapeId);return cachedShape.cloneShape(); // 調用我們自定義的克隆方法}// loadCache 會加載每種形狀的實例,并將它們存儲在 Hashtable 中public static void loadCache() {System.out.println("ShapeCache: Loading initial prototypes...");Circle circle = new Circle("circle-proto", 10, 0, 0);shapeMap.put(circle.getId(), circle);Rectangle rectangle = new Rectangle("rect-proto", 20, 10, 5, 5);shapeMap.put(rectangle.getId(), rectangle);System.out.println("ShapeCache: Prototypes loaded.");}// 允許運行時添加新的原型public static void addPrototype(String id, Shape shape) {shapeMap.put(id, shape);System.out.println("ShapeCache: Prototype '" + id + "' added.");}
}
客戶端使用
// main.go (示例用法)
/*
package mainimport ("fmt""./shape"
)func main() {cache := shape.NewShapeCache()// 從緩存獲取原型并克隆circle1, err := cache.GetShape("circle1")if err != nil {fmt.Println("Error:", err)return}circle1.Draw() // ID: circle1_clonerect1, err := cache.GetShape("rect1")if err != nil {fmt.Println("Error:", err)return}rect1.Draw() // ID: rect1_clone// 修改克隆體的屬性circle1.SetID("myCustomCircle")// 如果是 Circle 類型,可以進行類型斷言來訪問特定屬性if c, ok := circle1.(*shape.Circle); ok {c.Radius = 100c.X = 50}circle1.Draw() // ID: myCustomCircle, Radius: 100, Center: (50,0)// 原始原型不受影響originalCircle, _ := cache.GetShape("circle1") // 再次獲取會重新克隆originalCircleProto := cache.prototypes["circle1"] // 直接訪問原型 (不推薦直接修改原型)fmt.Println("--- Original prototype vs new clone from cache ---")originalCircleProto.Draw() // ID: circle1, Radius: 10originalCircle.Draw() // ID: circle1_clone, Radius: 10 (新克隆的)// 運行時添加新原型trianglePrototype := shape.NewTriangle("triangle-proto", 5, 10) // 假設有 Triangle 類型cache.AddShape(trianglePrototype.GetID(), trianglePrototype)clonedTriangle, _ := cache.GetShape("triangle-proto")if clonedTriangle != nil {clonedTriangle.Draw()}
}// 假設添加一個 Triangle 類型 (triangle.go)
/*
package shape
import "fmt"
type Triangle struct { ID string; Base, Height int }
func NewTriangle(id string, base, height int) *Triangle { return &Triangle{id, base, height} }
func (t *Triangle) SetID(id string) { t.ID = id }
func (t *Triangle) GetID() string { return t.ID }
func (t *Triangle) Clone() Shape { return &Triangle{t.ID + "_clone", t.Base, t.Height} }
func (t *Triangle) Draw() { fmt.Printf("Drawing Triangle [ID: %s, Base: %d, Height: %d]\n", t.ID, t.Base, t.Height) }
*/
// Main.java (示例用法)
/*
package com.example;import com.example.shape.Circle;
import com.example.shape.Rectangle;
import com.example.shape.Shape;
import com.example.shape.ShapeCache;public class Main {public static void main(String[] args) {ShapeCache.loadCache(); // 加載原型try {System.out.println("--- Cloning and using shapes ---");Shape clonedCircle1 = ShapeCache.getShape("circle-proto");if (clonedCircle1 != null) {clonedCircle1.draw(); // ID: circle-proto_clone}Shape clonedRectangle1 = ShapeCache.getShape("rect-proto");if (clonedRectangle1 != null) {clonedRectangle1.draw(); // ID: rect-proto_clone}System.out.println("\n--- Modifying a cloned shape ---");// 修改克隆體的屬性if (clonedCircle1 != null) {clonedCircle1.setId("myCustomCircle");if (clonedCircle1 instanceof Circle) {Circle customCircle = (Circle) clonedCircle1;customCircle.setCenter(100, 100); // 修改 Point 對象}clonedCircle1.draw(); // ID: myCustomCircle, Center: (100,100)}System.out.println("\n--- Verifying original prototype is unchanged ---");// 原始原型不受影響 (因為我們實現了深拷貝 Point)Shape originalCircleProto = ShapeCache.shapeMap.get("circle-proto"); // 直接訪問原型 (不推薦)if (originalCircleProto != null) {System.out.print("Original Prototype in Cache: ");originalCircleProto.draw(); // ID: circle-proto, Center: (0,0)}Shape newlyClonedCircle = ShapeCache.getShape("circle-proto");if (newlyClonedCircle != null) {System.out.print("Newly Cloned from Cache: ");newlyClonedCircle.draw(); // ID: circle-proto_clone, Center: (0,0)}// 演示如果 Point 是淺拷貝會發生什么// 如果 Circle.cloneShape() 中對 center 只是 clonedCircle.center = this.center;// 那么修改 customCircle.setCenter(100,100) 會同時修改 originalCircleProto 的 centerSystem.out.println("\n--- Adding a new prototype at runtime ---");Circle newProto = new Circle("circle-large-proto", 50, 10, 10);ShapeCache.addPrototype(newProto.getId(), newProto);Shape clonedLargeCircle = ShapeCache.getShape("circle-large-proto");if(clonedLargeCircle != null) {clonedLargeCircle.draw();}} catch (CloneNotSupportedException e) {e.printStackTrace();}}
}
*/
8. 總結
原型模式通過復制(克隆)現有對象來創建新對象,從而在特定場景下(如對象創建成本高、需要大量相似對象)提供了一種高效且靈活的對象創建方式。核心在于實現 clone()
方法,并正確處理深拷貝與淺拷貝的問題。當與原型管理器結合使用時,還可以實現運行時的動態產品配置。
記住它的核心:克隆現有對象,高效創建。