[学习笔记]设计模式之Prototype
写在前面
为方便读者,本文已添加至索引:
在笔记Builder模式中,我们曾见到了最初用于创建平行世界的函数createWorld,并且它是Mage类的成员函数(毕竟是专属于魔导士的强大咒语嘛)。然而在上篇笔记Singleton模式中,时の魔导士组建了一个极为强大的WorldMgr议会来代替他维持世界。“如果他们甚至连改造地形的能力都没有的话,会让人很苦恼呢……”魔导士心想,“或许我可以给他们提供一套地图编辑器……或者说世界改造器,就像暴雪那帮家伙的星际争霸。”对于如何设计一套通用的世界改造器,时の魔导士打算引入Prototype模式。它能极大地减少系统中类的数目,同时也更易于在其中添加新的环境因素。
要点梳理
- 目的分类
- 对象创建型模式
- 范围准则
- 对象(该模式处理对象间的关系,这些关系在运行时刻是可以变化的,更具动态性)
- 主要功能
- 用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象
- 适用情况
- 当一个系统应该独立于它的产品创建、构成和表示时
- 当要实例化的类是在运行时刻指定时,例如,通过动态装载
- 为了避免创建一个与产品类层次平行的工厂类层次时
- 当一个类的实例只能有几个不同状态组合中的一种时。建立相应数目的原型并克隆它们可能比每次用合适的状态手工实例化该类更方便一些。
- 参与部分
- Prototype:声明一个克隆自身的接口
- ConcretePrototype:实现一个克隆自身的操作
- Client:让一个原型克隆自身从而创建一个新的对象
- 协作过程
- 客户请求一个原型克隆自身
- UML图
示例分析 - 世界改造工具套装内部测试版
首先,我们提供WorldPrototypeEditor类,它将使用要创建的对象的原型来初始化,这样我们就不需要仅仅为了改变它所创建的地形的类而再生成子类了:
1 class WorldPrototypeEditor { 2 public: 3 WorldPrototypeEditor(Mountain*, Ocean*, Plant*); 4 5 virtual Mountain* makeMountain(); 6 virtual Ocean* makeOcean(); 7 virtual Plant* makePlant(Mountain*); 8 private: 9 Mountain* _prototypeMountain; 10 Ocean* _prototypeOcean; 11 Plant* _prototypePlant; 12 }
由于是内部测试版,我们仅仅考虑最简单的地形改造器啦,能造山,造水,然后在山上种些小树苗什么的就完事了。它的的构造器只初始化它的原型:
1 WorldPrototypeEditor::WorldPrototypeEditor(Mountain* m, Ocean* o, Plant* p) 2 { 3 _prototypeMountain = m; 4 _prototypeOcean = o; 5 _prototypePlant = p; 6 }
用于创建山、海洋和树的成员函数是相似的:每个都要克隆一个原型,然后初始化。让我们来看看makeMountain和makeTree的定义:
1 Mountain* WorldPrototypeEditor::makeMountain() 2 { 3 return _prototypeMountain->clone(); 4 } 5 6 Plant* WorldPrototypeEditor::makePlant(Mountain* m) 7 { 8 Plant* p = _prototypePlant->clone(); 9 p->addTo(m); 10 return p; 11 }
因此,当泰坦们开始着手编辑世界地形的时候,只需使用基本地形构件的原型进行初始化,就可以由WorldPrototypeEditor来创建一个原型的或缺省的世界:
1 WorldPrototypeEditor simpleWorldEditor; 2 World* world = WorldMgr::getInstance()->editWorld(simpleWorldEditor);
一个可以被用作原型的对象,例如Plant的实例,必须支持clone操作。它还必须有一个拷贝构造器用于克隆。它可能还需要一个独立的操作来重新初始化内部的状态:
1 class Plant { 2 public: 3 Plant(); 4 Plant(const Plant&); 5 6 virtual void initialize(Category*); 7 virtual Plant* clone() const; 8 private: 9 Category* _cg; 10 } 11 12 Plant::Plant(const Plant& other) { 13 _cg = other._cg; 14 } 15 16 void Plant::initialize(Category* c) 17 { 18 _cg = c; 19 } 20 21 Plant* Plant::clone() const { 22 return new Plant(*this); 23 }
例子中私有成员变量_cg仅仅是决定了植物的种类,但是如果我们想为这个编辑器添加一些扩展包。比如说,增加一些特别的植物:苹果树怎么样?AppleTree必须重定义clone,并且实现相应的构造器。
1 class AppleTree : public Plant { 2 public: 3 AppleTree(); 4 AppleTree(const AppleTree&); 5 6 virtual Plant* clone() const; 7 int hasApple(); 8 private: 9 int _apple; 10 } 11 12 AppleTree::AppleTree(const AppleTree& other) : Plant(other) { 13 _apple = other._apple; 14 } 15 16 Plant* AppleTree::clone() const { 17 return new AppleTree(*this); 18 }
在这个情况下,我们可以用带苹果树扩展包的原型集合来初始化WorldPrototypeEditor,见下面的调用:
WorldPrototypeEditor appleWorldEditor(new Mountain(), new Ocean(), new AppleTree());
我们可以看一个简单的UML图来更加直观地感受:
特点总结
Prototype模式有许多和Abstract Factory、Builder模式一样的效果:它对客户隐藏了具体的产品类,因此减少了客户需要知道的名字的数目。此外,这些模式使客户无需改变即可使用与特定应用相关的类。当然Prototype模式还有它独到的一面:
- 运行时刻增加和删除产品。Prototype允许只通过客户注册原型实例就可以将一个新的具体产品类并入系统。它比其他创建型模式更为灵活,因为客户可以在运行时刻建立和删除原型。
- 改变值以指定新对象。高度动态的系统允许我们通过对象复合定义新的行为。例如,通过为一个对象变量指定值,并且不定义新的类。我们通过实例化已有类并且将这些实例注册为客户对象的原型,就可以有效定义新类别的对象。客户可以将职责代理给原型,从而表现出新的行为。
- 改变结构以指定新对象。许多应用由部件和子部件来创建对象。例如电路设计编辑器就是由子电路来构造电路的。为方便起见,这样的应用通常允许我们实例化复杂的、用户定义的结构,比方说,一次又一次的重复使用一个特定的子电路。对于Prototype模式,我们仅需将这个子电路作为一个原型增加到可用的电路元素选择板中。
- 减少子类的构造。Factory Method经常产生一个与产品类层次平行的Creator类层次。Prototype模式使得我们克隆一个原型而不是请求一个工厂方法去产生一个新的对象,因此我们根本不需要Creator类层次。这一优点主要适用于像C++这样不将类作为一级类对象的语言。
- 用类动态配置。应用一些运行时刻环境允许我们动态将类装载到应用中。
对于有些语言,例如JavaScript,它就提供了一个等价于原型的东西(对象),原型模式几乎是它所固有的特性,无处不在。
写在最后
今天的笔记就到这里了,欢迎大家批评指正!如果觉得可以的话,好文推荐一下,我会非常感谢的!