设计模式之原型(Prototype)模式
又称: 克隆模式(Clone)
意图
原型模式(Prototype)是创建型设计模式,它允许你复制现有对象,而不使代码依赖于它们的类。
问题描述
假设你有一个对象,你想创建它的精确副本。你会怎么做?首先,你必须创建一个相同类的新对象。然后,必须遍历原始对象的所有字段,并将它们的值复制到新对象上。
很好!但有一个问题。并非所有对象都可以以这种方式复制,因为对象的某些字段可能是私有的,并且从对象本身外部看不到。
这种直接方法还有一个问题。因为必须知道对象的类才能创建重复,因此你的代码将依赖于该类。如果额外的依赖性没有吓到你,还有另一个陷阱。有时你只知道对象遵循的接口,而不知道它的具体类,例如,当方法中的参数接受遵循某个接口的任何对象时。
方案
原型模式将克隆过程委托给正在克隆的实际对象。该模式为所有支持克隆的对象声明了一个通用接口。此接口允许你克隆对象,而无需将代码耦合到该对象的类。通常,这样的接口只包含一个 clone
方法。
clone
方法的实现在所有类中都非常相似。该方法创建当前类的对象,并将旧对象的所有字段值转移到新对象中。你甚至可以复制私有字段,因为大多数编程语言允许对象访问属于同一类的其他对象的私有字段。
支持克隆的对象也叫原型(prototype)。当对象有几十个字段以及上百个可能的配置时,克隆它们可能是子类化的替代方案。
它的工作原理如下:创建一组对象,以各种方式进行配置。当你需要一个像你配置的对象一样的对象时,你只需要克隆一个原型,而不是从头开始构建一个新的对象。
真实世界类比
在现实生活中,原型用于在开始大规模生产产品之前进行各种测试。然而,在这种情况下,原型并不参与任何实际生产,而是扮演一个被动的角色。
由于工业原型并没有真正复制自己,因此与这种模式更接近的类比是有丝分裂细胞的过程(生物学,还记得吗?)。有丝分裂后,形成一对相同的细胞。原始单元格充当原型,并在创建副本时发挥积极作用。
结构
基础实现
Prototype 接口声明了克隆方法。大部分情况下,它只有一个 clone
方法。
具体原型类(Concrete Prototype)实现了克隆方法。除了将原始对象的数据复制到克隆的对象中,该方法也可以处理与克隆链接对象、解开递归依赖关系等相关的克隆过程的一些边缘情况。
客户端(Client)可以生成任何遵守原型接口的对象的副本。
原型注册实现
原型注册(Prototype Registry)类提供了一个简单的方法来获取频繁使用的原型。它存储了一组准备复制的预构建对象。最简单的原型注册是name → prototype
哈希表。但是,如果需要比简单名称更好的搜索条件,则可以构建一个更健壮的注册版本。
伪代码
本例中,原型(Prototype)模式允许你生成几何对象的准确副本,而无需将代码耦合到它们的类。
所有形状(shape)类都遵循相同的接口,这提供了一种克隆方法。子类可以在将自己的字段值复制到结果对象之前调用父类的克隆方法。
// Base prototype.
abstract class Shape is
field X: int
field Y: int
field color: string
// A regular constructor.
constructor Shape() is
// ...
// The prototype constructor. A fresh object is initialized
// with values from the existing object.
constructor Shape(source: Shape) is
this()
this.X = source.X
this.Y = source.Y
this.color = source.color
// The clone operation returns one of the Shape subclasses.
abstract method clone():Shape
// Concrete prototype. The cloning method creates a new object
// in one go by calling the constructor of the current class and
// passing the current object as the constructor's argument.
// Performing all the actual copying in the constructor helps to
// keep the result consistent: the constructor will not return a
// result until the new object is fully built; thus, no object
// can have a reference to a partially-built clone.
class Rectangle extends Shape is
field width: int
field height: int
constructor Rectangle(source: Rectangle) is
// A parent constructor call is needed to copy private
// fields defined in the parent class.
super(source)
this.width = source.width
this.height = source.height
method clone():Shape is
return new Rectangle(this)
class Circle extends Shape is
field radius: int
constructor Circle(source: Circle) is
super(source)
this.radius = source.radius
method clone():Shape is
return new Circle(this)
// Somewhere in the client code.
class Application is
field shapes: array of Shape
constructor Application() is
Circle circle = new Circle()
circle.X = 10
circle.Y = 10
circle.radius = 20
shapes.add(circle)
Circle anotherCircle = circle.clone()
shapes.add(anotherCircle)
// The `anotherCircle` variable contains an exact copy
// of the `circle` object.
Rectangle rectangle = new Rectangle()
rectangle.width = 10
rectangle.height = 20
shapes.add(rectangle)
method businessLogic() is
// Prototype rocks because it lets you produce a copy of
// an object without knowing anything about its type.
Array shapesCopy = new Array of Shapes.
// For instance, we don't know the exact elements in the
// shapes array. All we know is that they are all
// shapes. But thanks to polymorphism, when we call the
// `clone` method on a shape the program checks its real
// class and runs the appropriate clone method defined
// in that class. That's why we get proper clones
// instead of a set of simple Shape objects.
foreach (s in shapes) do
shapesCopy.add(s.clone())
// The `shapesCopy` array contains exact copies of the
// `shape` array's children.
适用
当代码不应该依赖于需要复制的对象的具体类时,可以使用原型模式。
当代码与通过某些接口从第三方代码传递给你的对象一起工作时,这种情况经常发生。这些对象的具体类是未知的,即使你想依赖它们,你也不能依赖它们。
原型(Prototype)模式为客户端代码提供了一个通用接口,用于处理所有支持克隆的对象。这个接口使客户端代码独立于它所克隆的对象的具体类。
当你想减少子类的数量时,可以使用该模式,这些子类只是在初始化各自对象的方式上有所不同。
假设你有一个复杂的类,在使用它之前需要进行费力的配置。有几种常见的配置此类的方法,这些代码分散在应用程序中。为了减少重复,你创建了几个子类,并将每个常见的配置代码放入它们的构造函数中。你解决了重复问题,但现在有很多伪子类。
原型模式允许你使用一组以各种方式配置的预构建对象作为原型。客户端可以简单地查找合适的原型并克隆它,而不是实例化与某些配置匹配的子类。
如何实现
创建原型接口并声明 clone
方法。或者只需将该方法添加到现有类层次结构的所有类中(如果有)。
原型类必须定义接受该类的对象作为参数的替代构造函数。构造函数必须将类中定义的所有字段的值从传递的对象复制到新创建的实例中。如果要更改子类,则必须调用父构造函数,让超类处理其私有字段的克隆。
如果编程语言不支持方法重载,将无法创建单独的“原型”构造函数。因此,必须在 clone
方法中执行将对象的数据复制到新创建的克隆中。尽管如此,将此代码放在常规构造函数中更安全,因为生成的对象在调用新运算符后立即返回完全配置的对象。
克隆方法通常只包含一行:使用构造函数的原型版本运行一个 new
操作符。请注意,每个类都必须显式重写克隆方法,并使用自己的类名和 new
操作符。否则,克隆方法可能会生成父类的对像。
或者,创建一个集中的原型注册表来存储常用原型的目录。
你可以将注册表实现为一个新的工厂类,或者使用静态方法将其放入基本原型类中以获取原型。该方法应该基于客户端代码传递给该方法的搜索条件来搜索原型。其条件可能是一个简单的字符串标记,也可能是一组复杂的搜索参数。找到合适的原型后,注册表应该对其进行克隆并将副本返回给客户端。
最后,将对子类构造函数的直接调用替换为对原型注册表的工厂方法的调用。
优缺点
- ✔️你可以在不耦合其实体类的情况下克隆对象。
- ✔️你可以摆脱重复的初始化代码,转而克隆预构建的原型。
- ✔️你可以更方便地生产复杂对象。
- ✔️在处理复杂对象的配置预设时,你有了一个继承的替代方案。
- ❌克隆具有循环引用的复杂对象可能非常棘手。
与其他模式的关系
许多设计始于工厂方法(不那么复杂,通过子类更可定制)开始,并向抽象工厂、原型或 Builder(更灵活,但更复杂)发展。
抽象工厂通常基于一组工厂方法,不过你也可以使用原型方法在这些类上组合方法。
原型可以帮你将命令副本保存到历史记录
重度使用组合模式(Composite)和装饰器(Decorator)的设计,可以从使用原型(Prototype)中获益。使用该模式,你可以克隆复杂的结构,而不用从头重新构建。
原型(Prototype)不是基于继承,因此它没有继承的缺点。另一方面,原型(Prototype)需要对克隆对象进行复杂的初始化。工厂方法(Factory Method)基于继承但不要求初始化步骤。
有时候原型(Prototype)模式可以是备忘录模式(Memento)一个更简单的替代方案。如果对象(您希望将其状态存储在历史记录中)相当简单,并且没有到外部资源的链接,或者链接很容易重新建立,则此方法有效。
抽象工厂(Abstract Factories)、建造者(Builder)和原型(Prototype)可以作为单例(Singleton)实现。
代码示例
index.php: 概念示例
<?php
namespace RefactoringGuru\Prototype\Conceptual;
/**
* The example class that has cloning ability. We'll see how the values of field
* with different types will be cloned.
*/
class Prototype
{
public $primitive;
public $component;
public $circularReference;
/**
* PHP has built-in cloning support. You can `clone` an object without
* defining any special methods as long as it has fields of primitive types.
* Fields containing objects retain their references in a cloned object.
* Therefore, in some cases, you might want to clone those referenced
* objects as well. You can do this in a special `__clone()` method.
*/
public function __clone()
{
$this->component = clone $this->component;
// Cloning an object that has a nested object with backreference
// requires special treatment. After the cloning is completed, the
// nested object should point to the cloned object, instead of the
// original object.
$this->circularReference = clone $this->circularReference;
$this->circularReference->prototype = $this;
}
}
class ComponentWithBackReference
{
public $prototype;
/**
* Note that the constructor won't be executed during cloning. If you have
* complex logic inside the constructor, you may need to execute it in the
* `__clone` method as well.
*/
public function __construct(Prototype $prototype)
{
$this->prototype = $prototype;
}
}
/**
* The client code.
*/
function clientCode()
{
$p1 = new Prototype();
$p1->primitive = 245;
$p1->component = new \DateTime();
$p1->circularReference = new ComponentWithBackReference($p1);
$p2 = clone $p1;
if ($p1->primitive === $p2->primitive) {
echo "Primitive field values have been carried over to a clone. Yay!\n";
} else {
echo "Primitive field values have not been copied. Booo!\n";
}
if ($p1->component === $p2->component) {
echo "Simple component has not been cloned. Booo!\n";
} else {
echo "Simple component has been cloned. Yay!\n";
}
if ($p1->circularReference === $p2->circularReference) {
echo "Component with back reference has not been cloned. Booo!\n";
} else {
echo "Component with back reference has been cloned. Yay!\n";
}
if ($p1->circularReference->prototype === $p2->circularReference->prototype) {
echo "Component with back reference is linked to original object. Booo!\n";
} else {
echo "Component with back reference is linked to the clone. Yay!\n";
}
}
clientCode();
Output.txt: 执行结果
Primitive field values have been carried over to a clone. Yay!
Simple component has been cloned. Yay!
Component with back reference has been cloned. Yay!
Component with back reference is linked to the clone. Yay!