一、引言
与工厂模式相同,原型模式(Prototype)也是创建型模式。原型模式通过一个对象(原型对象)克隆出多个一模一样的对象。实际上,该模式与其说是一种设计模式,不如说是一种创建对象的方法(对象克隆),尤其是创建给定类的对象(实例)过程很复杂(例如,要设置许多成员变量的值)时,使用这种设计模式就比较合适。
二、原型模式
也就是说,原型模式是为了使你能够复制已有对象, 而又无需使代码依赖它们所属的类。
如果你有一个对象, 并希望生成与其完全相同的一个复制品, 你该如何实现呢? 首先, 你必须新建一个属于相同类的对象。 然后, 你必须遍历原始对象的所有成员变量, 并将成员变量值复制到新对象中。
不错! 但有个小问题。 并非所有对象都能通过这种方式进行复制, 因为有些对象可能拥有私有成员变量, 它们在对象本身以外是不可见的。直接复制还有另外一个问题。 因为你必须知道对象所属的类才能创建复制品, 所以代码必须依赖该类。 即使你可以接受额外的依赖性, 那还有另外一个问题: 有时你只知道对象所实现的接口, 而不知道其所属的具体类, 比如可向方法的某个参数传入实现了某个接口的任何对象。克隆可能会在父类和子类之间进行,并且可能是动态的,很明显通过父类的拷贝构造函数无法实现对子类对象的拷贝,其实这就是一个多态,我们需要给父类提供一个克隆函数并且是一个虚函数。
仍然使用闯关打怪兽的案例来解释。下面是一个怪兽类。我们想让怪物父类拥有clone
自己的能力。
// 怪物父类
class Monster {
public:
// 构造函数
Monster(int life, int magic, int attack)
: m_life(life), m_magic(magic), m_attack(attack) {}
virtual ~Monster() {} // 虚析构函数
virtual unique_ptr<Monster> clone() const = 0; //clone
protected:
int m_life; // 生命值
int m_magic; // 魔法值
int m_attack; // 攻击力
};
clone
函数意味着调用该成员函数就会从当前类对象复制出一个完全相同的对象(通过克隆自已来创建出新对象),这当然也是一种创建该类所属对象的方式。这三种怪物实现父类的clone
方法。
// 亡灵类
class M_Undead : public Monster {
public:
M_Undead(int life, int magic, int attack)
: Monster(life, magic, attack) {
cout << "一只亡灵类怪物来到了这个世界" << endl;
}
unique_ptr<Monster> clone() const override {
cout<<" 亡灵类被克隆了 "<<endl;
return make_unique<M_Undead>(*this); // 克隆自身
}
};
// 元素类
class M_Element : public Monster {
public:
M_Element(int life, int magic, int attack)
: Monster(life, magic, attack) {
cout << "一只元素类怪物来到了这个世界" << endl;
}
unique_ptr<Monster> clone() const override {
cout<<" 元素类被克隆了 "<<endl;
return make_unique<M_Element>(*this); // 克隆自身
}
};
// 机械类
class M_Mechanic : public Monster {
public:
M_Mechanic(int life, int magic, int attack)
: Monster(life, magic, attack) {
cout << "一只机械类怪物来到了这个世界" << endl;
}
unique_ptr<Monster> clone() const override {
cout<<" 机械类被克隆了 "<<endl;
return make_unique<M_Mechanic>(*this); // 克隆自身
}
};
既然是克隆,那么上述M_Undead
、M_Element
、M_Mechanic
中的clone
成员函数的实现体是需要修改的。例如,某个机械类怪物因为被主角砍了一刀失去了100点生命值,导致该怪物对象的m_life
成员变量(生命值)从原来的400变成300,那么调用clone
方法克隆出来的新机械类怪物对象也应该是300
点生命值,所以此时M_Mechanic
类中clone
成员函数中的代码行return new M_Mechanic(400,0,110);
就不合适,因为这样会创建(克隆)出一个400点生命值的新怪物,不符合clone这个成员函数的本意(复制出一个完全相同的对象)。
克隆对象自身实际上是需要调用类的拷贝构造函数的。如果程序员在类中没有定义自已的拷贝构造函数,那么编译器会在必要的时候(但不是一定)合成出一个拷贝构造函数。因此,**在使用原型模式的时候要注意深拷贝和浅拷贝的问题。**下面添加拷贝构造函数。
// 亡灵类
// 拷贝构造函数
M_Undead::M_Undead(const M_Undead& other) : Monster(other) {
cout << "亡灵类被拷贝了" << endl;
}
为了方便,我们仅写一个。这里我们需要确保能正确编写拷贝构造函数,这样调用clone
才能正确的克隆出对象。
unique_ptr<Monster> undead = make_unique<M_Undead>(100, 50, 20);
unique_ptr<Monster> undeadClone = undead->clone(); // 克隆亡灵怪物
unique_ptr<Monster> element = make_unique<M_Element>(80, 70, 30);
unique_ptr<Monster> elementClone = element->clone(); // 克隆元素怪物
unique_ptr<Monster> mechanic = make_unique<M_Mechanic>(120, 40, 50);
unique_ptr<Monster> mechanicClone = mechanic->clone(); // 克隆机械怪物
原型模式将克隆过程委派给被克隆的实际对象。 模式为所有支持克隆的对象声明了一个通用接口, 该接口能够克隆对象, 同时又无需将代码和对象所属类耦合。 通常情况下, 这样的接口中仅包含一个
clone
方法。所有的类对
clone
方法的实现都非常相似。 该方法会创建一个当前类的对象, 然后将原始对象所有的成员变量值复制到新建的类中。 甚至可以复制私有成员变量, 因为绝大部分编程语言都允许对象访问其同类对象的私有成员变量。支持克隆的对象即为原型。 当对象有几十个成员变量和几百种类型时, 对其进行克隆甚至可以代替子类的构造。
原型模式就是能够复制已有的对象,而又无需使代码依赖它们所属的类。换种说法,就是通过已有对象克隆出另一个新的对象,并且克隆这个对象不需要使用构造函数。
原型模式的UML图中,包含两种角色。
- 抽象原型类(Prototype):所有具体原型类的父类,在其中声明克隆方法。这里指Monster类。
- 具体原型类(oncretePrototype):实现在抽象原型类中声明的克隆方法,在克隆方法中返回自己的一个克隆对象。这里指M_Undead类、M_Element类和M_Mechanic类。
引入原型模型的定义:用原型实例指定创建对象的种类,并且通过复制这些原型创建新的对象。简单来说,就是通过克隆来创建新的对象实例。
原型模式结构
- 原型 (Prototype) 接口将对克隆方法进行声明。 在绝大多数情况下, 其中只会有一个名为
clone
克隆的方法。 - 具体原型 (Concrete Prototype) 类将实现克隆方法。 除了将原始对象的数据复制到克隆体中之外, 该方法有时还需处理克隆过程中的极端情况, 例如克隆关联对象和梳理递归依赖等等。
- 客户端 (Client) 可以复制实现了原型接口的任何对象。
原型注册表 (Prototype Registry) 提供了一种访问常用原型的简单方法, 其中存储了一系列可供随时复制的预生成对象。 最简单的注册表原型是一个 名称 → 原型
的哈希表。 但如果需要使用名称以外的条件进行搜索, 你可以创建更加完善的注册表版本。
三、总结
原型模式与工厂方法模式在创建对象时的主要区别在于它们如何处理对象的创建过程和状态复制。
原型模式通过复制现有对象(原型)来创建新对象,新对象的初始状态与原型对象相同,这避免了复杂的设置过程。当对象的内部数据复杂且多变时,原型模式比工厂方法模式更合适,因为它可以直接克隆当前状态,无需额外的设置代码。例如,在游戏中创建一个具有特定状态的怪物分身,使用原型模式可以快速复制这些状态。
工厂方法模式和原型模式在创建对象时都不需要知道具体的类名,但它们的工作方式不同。工厂方法模式通过调用创建接口来创建新对象,而原型模式通过克隆现有对象。如果对象的创建成本较高,或者需要避免复杂的初始化逻辑,原型模式是一个更好的选择。总结来说,两种模式都能解耦对象的创建过程,但原型模式在处理动态和复杂状态的对象时更为高效。
因此,如果对象的内部数据比较复杂且多变并且在创建对象的时候希望保持对象的当前状态,那么用原型模式显然比原型模式更合适。
工厂方法模式与原型模式在创建对象时的异同点:
- 前面范例中创建怪物对象时,这两种模式其实都不需要程序员知道所创建对象所属的类名;
- 工厂方法模式是调用相应的创建接口,例如使用
createMonster
接口来创建新的怪物对象,该接口中采用代码行``new类名(参数)`来完成对象的最终创建工作,这仍旧是属于根据类名来生成新对象; - 型模式是调用例如
clone
(程序员可以修改成任意其他名字)接口来创建新的怪物对象,按照惯例,这个接口一般不带任何参数,以免破坏克隆接口的统一性。该接口中采用的是代码行new类名(*this)
完成对类拷贝构造函数的调用来创建对象,所以这种创建对象的方式是根据现有对象来生成新对象
当然,也可以把原型模式看成是一种特殊的工厂方法模式(工厂方法模式的变体),这也是可以的一把原型对象所属的类本身(例如,M_Undead、M_Element、M_Mechanic)看成是创建克隆对象的工厂,而工厂方法指的自然就是克隆方法(clone)。
有时候原型可以作为备忘录模式的一个简化版本, 其条件是需要在历史记录中存储的对象的状态比较简单, 不需要链接其他外部资源, 或者链接可以方便地重建。原型并不基于继承, 因此没有继承的缺点。 另一方面, 原型需要对被复制对象进行复杂的初始化。 工厂方法基于继承, 但是它不需要初始化步骤。
在大量使用组合模式和装饰模式的设计时,可以通过原型模式来复制复杂结构, 而非从零开始重新构造。