零基础设计模式——第二部分:创建型模式 - 原型模式
第二部分:创建型模式 - 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()
方法,并正确处理深拷贝与浅拷贝的问题。当与原型管理器结合使用时,还可以实现运行时的动态产品配置。
记住它的核心:克隆现有对象,高效创建。