C++设计模式创建型模式———生成器模式
文章目录
- 一、引言
- 二、生成器/建造者模式
- 三、总结
一、引言
上一篇文章我们介绍了工厂模式,工厂模式的主要特点是生成对象。当对象较简单时,可以使用简单工厂模式或工厂模式;而当对象相对复杂时,则可以选择使用抽象工厂模式。
工厂用于生产各种对象,这些对象通常是兄弟类,继承自同一个基类。兄弟子类通过实现基类接口,展现不同的行为,并由工厂函数创建。然而,工厂模式在创建对象时并不关注构造细节,处理复杂对象生成时可能显得力不从心。抽象工厂模式专注于生成一系列相关对象,但在对象构造复杂时,其能力也有限。
相较于工厂模式,生成器模式同样用于对象的生成,但更侧重于构造细节,增加了额外的构建流程,以便处理复杂对象的构建需求。
生成器模式也是一种创建型设计模式, 使我们能够分步骤创建复杂对象。 该模式允许使用相同的创建代码生成不同类型和形式的对象。 也就是说,生成器模式就是为了生成一个复杂的对象。
化繁为简,逐个击破。分步骤创建复杂的对象,并且允许使用相同的代码生成不同类型和形式的对象。
假设有这样一个复杂对象, 在对其进行构造时需要对诸多成员变量和嵌套对象进行繁复的初始化工作。 这些初始化代码通常深藏于一个包含众多参数且让人基本看不懂的构造函数中; 甚至还有更糟糕的情况, 那就是这些代码散落在客户端代码的多个位置。
假设我们要建一个房子,房子由许多部分组成,如门和墙壁。生成器模式可以将对象构造的代码从产品类中抽取出来,并放在一个名为生成器的独立对象中。
该模式会将对象构造过程划分为一组步骤, 比如 buildWalls
创建墙壁和 buildDoor
创建房门创建房门等。 每次创建对象时, 都需要通过生成器对象执行一系列步骤。 重点在于我们无需调用所有步骤, 而只需调用创建特定对象配置所需的那些步骤即可。
当你需要创建不同形式的产品时, 其中的一些构造步骤可能需要不同的实现。 例如, 木屋的房门可能需要使用木头制造, 而城堡的房门则必须使用石头制造。
在这种情况下, 可以创建多个不同的生成器, 用不同方式实现一组相同的创建步骤。 然后就可以在创建过程中使用这些生成器 (例如按顺序调用多个构造步骤) 来生成不同类型的对象。
我们通过写出不同生成器以不同方式执行相同的任务。
假设我们需要一个木门+石墙的房子。也需要一个石头门+钢铁的房子。在调用同一组步骤后, 第一个建造者会给你一栋普通房屋, 第二个会给你一座小城堡。但是, 只有在调用构造步骤的客户端代码可以通过通用接口与建造者进行交互时, 这样的调用才能返回需要的房屋。
然后我们可以进一步的把用于创建一系列生成器的步骤抽取出来,成为一个单独的主管类。主管类去定义创建步骤的执行顺序。而生成器去提供这些步骤的实现。
但是即使没有主管类,我们的用户也可以直接以特定的顺序去调用创建步骤。但是如果有主管类,主管类可以完全隐藏产品的构造细节。客户端只需要将一个生成器与主管类关联,然后使用主管类去构造产品,就能从生成器处获得构造结果了。
二、生成器/建造者模式
这里还是以游戏中的怪物类来讲解。怪物同样分为亡灵类怪物、元素类怪物、机械类怪物。
在创建怪物对象的过程中,有一个创建步骤非常烦琐,把怪物模型创建出来用于显示给玩家。策划规定,任何一种怪物都由头部、躯干(包括颈部、尾巴等)、肢体3个部位组成,在制作怪物模型时,头部、躯干、肢体模型分开制作。每个部位模型都会有一些位置和方向信息,用于挂接在其他部位模型上,比如将头部挂接到躯干部,再将肢体挂接到躯干部就可以构成一个完整的怪物模型。当然,一些在水中的怪物可能不包含四肢,那么将肢体挂接到躯干部这个步骤什么都不做即可。
之所以在制作怪物模型时将头部、躯干、肢体模型分开制作,是便于同类型怪物的3个
组成部位进行互换。试想一下,如果针对亡灵类怪物制作了3个头部、3个躯干以及3个肢体,则最多可以组合出27个外观不同的亡灵类怪物,这既节省了游戏制作成本,又节省了游戏运行时对内存的消耗。
程序需要先把怪物模型载入内存并进行装配以保证正确地显示给玩家看。所以程序需
要进行如下编码步骤:
-
将怪物的躯干模型信息读人内存并提取其中的位置和方向信息;
-
将怪物的头部和四肢模型信息读人内存并提取其中的位置和方向信息;
-
将头部和四肢模型以正确的位置和方向挂接(Mount)到躯干部位,从而装配出完整的怪物模型。
我们实现一个Monster父类。
class Monster
{
public:
Monster(int life, int magic, int attack)
:m_life(life), m_magic(magic), m_attack(attack)
{}// 创建怪物的纯虚函数,具体实现将在子类中进行
void Assemble(string strmodelno)
//参数:模型编号,形如“1253679201245”等,每种位的组合都有一些特别的含义,这里无须深究
{
LoadTrunkModel(strmodelno.substr(4, 3));
//载人躯干模型,截取某部分字符串以表示躯干模型的编号
LoadHeadkModel(strmodelno.substr(7, 3));
//载人头部模型并挂接到躯干模型上
LoadLimbsModel(strmodelno.substr(10, 3));
//载人四肢模型并挂接到躯干模型上
}
virtual void LoadTrunkModel(const string& strno) = 0;
virtual void LoadHeadkModel(const string& strno) = 0;
virtual void LoadLimbsModel(const string& strno) = 0;
virtual~Monster() {}
protected:
int m_life;
int m_magic;
int m_attack;
};
在上述代码中做了很多简化,只是大致的实现代码,在Assemble成员函数中实现了载人一个怪物模型的固定流程一分别载入了躯干、头部、四肢模型并将它们装配到一起,游戏中所有怪物的载入都遵循该流程(其中的代码是稳定的,不发生变化。
因为亡灵类怪物、元素类怪物、机械类怪物的外观差别巨大,所以虽然这3类怪物的载人流程相同,但不同种类怪物的细节载人差别很大,所以,将LoadTrunkModel
、LoadHeadModel
、LoadLimbsModel
(构建模型的子步骤)成员函数写为虚函数以方便在Monster的子类中重新实现。
// 亡灵怪物类
class M_Undead : public Monster {
public:
M_Undead(int life, int magic, int attack)
: Monster(life, magic, attack) {
cout << "一只亡灵类怪物来到了这个世界" << endl;
}
void LoadTrunkModel(const string& strno) override {
cout << "载入亡灵躯干模型:" << strno << endl;
}
void LoadHeadkModel(const string& strno) override {
cout << "载入亡灵头部模型:" << strno << endl;
}
void LoadLimbsModel(const string& strno) override {
cout << "载入亡灵四肢模型:" << strno << endl;
}
};
// 元素类怪物
class M_Element : public Monster {
public:
M_Element(int life, int magic, int attack)
: Monster(life, magic, attack) {
cout << "一只元素类怪物来到了这个世界" << endl;
}
void LoadTrunkModel(const string& strno) override {
cout << "载入元素躯干模型:" << strno << endl;
}
void LoadHeadkModel(const string& strno) override {
cout << "载入元素头部模型:" << strno << endl;
}
void LoadLimbsModel(const string& strno) override {
cout << "载入元素四肢模型:" << strno << endl;
}
};
// 机械类怪物
class M_Mechanic : public Monster {
public:
M_Mechanic(int life, int magic, int attack)
: Monster(life, magic, attack) {
cout << "一只机械类怪物来到了这个世界" << endl;
}
void LoadTrunkModel(const string& strno) override {
cout << "载入机械躯干模型:" << strno << endl;
}
void LoadHeadkModel(const string& strno) override {
cout << "载入机械头部模型:" << strno << endl;
}
void LoadLimbsModel(const string& strno) override {
cout << "载入机械四肢模型:" << strno << endl;
}
};
这时在main
函数中加入如下代码,创建一个怪物对象并对齐进行生成:
unique_ptr<Monster> undead = make_unique<M_Undead>(100, 50, 20);
undead->Assemble("1253679201245"); // 传入模型编号进行组装
/*
我们会看到如下输入结结果:
一只亡灵类怪物来到了这个世界
载入亡灵躯干模型:679
载入亡灵头部模型:201
载入亡灵四肢模型:245
*/
上述这些代码用于创建怪物对象以显示给玩家看,但怪物的创建比较复杂,严格地说,应该是怪物模型的载入过程比较复杂,需要按顺序分别载入躯干、头部、四肢模型并实现不同部位模型之间的挂接。但是,目前的代码并不能称为生成器模式。通过对过程进一步拆分还可以进一步提高灵活性。
这里将Assemble
、LoadTrunkModel
、LoadHeadModel
、LoadLimbsModel
这些与模型载人与挂接步骤相关的成员函数称为构建过程相关函数。
考虑到Monster
类中要实现的逻辑功能可能较多,如果把构建过程相关函数提取出来(分离)放到一个单独的类中,不但可以减少Monster
类中的代码量,还可以增加构建过程相关代码的独立性,日后游戏中任何由头部、躯干、肢体3个部位组成并需要将头部挂接到躯干部,再将肢体挂接到躯干部的生物,都可以通过这个单独的类实现模型的构建。
// 怪物基类
class Monster {
public:
virtual ~Monster() {}
};
// 亡灵怪物类
class M_Undead : public Monster {
public:
M_Undead() {
cout << "一只亡灵类怪物来到了这个世界" << endl;
}
};
// 元素怪物类
class M_Element : public Monster {
public:
M_Element() {
cout << "一只元素类怪物来到了这个世界" << endl;
}
};
// 机械怪物类
class M_Mechanic : public Monster {
public:
M_Mechanic() {
cout << "一只机械类怪物来到了这个世界" << endl;
}
};
// 怪物构建器基类
class MonsterBuilder {
public:
virtual ~MonsterBuilder() {}
// 载入不同部分模型的纯虚函数
virtual void LoadTrunkModel(const string& strno) = 0;
virtual void LoadHeadkModel(const string& strno) = 0;
virtual void LoadLimbsModel(const string& strno) = 0;
// 组装模型
void Assemble(const string& strmodelno) {
LoadTrunkModel(strmodelno.substr(4, 3)); // 躯干模型
LoadHeadkModel(strmodelno.substr(7, 3)); // 头部模型
LoadLimbsModel(strmodelno.substr(10, 3)); // 四肢模型
}
// 获取构建结果
unique_ptr<Monster> GetResult() { return move(m_pMonster); }
protected:
unique_ptr<Monster> m_pMonster; // 使用智能指针管理怪物对象
};
// 亡灵怪物构建器
class UndeadBuilder : public MonsterBuilder {
public:
UndeadBuilder() {
m_pMonster = make_unique<M_Undead>(); // 创建亡灵怪物
}
void LoadTrunkModel(const string& strno) override {
cout << "载入亡灵类怪物的躯干部位模型,需要m_pMonster指针调用M_Undead类或其父类中其他诸多成员函数,逻辑代码......" << strno << endl;
具体要做的事情其实是委托给怪物子类来完成,委托指把本该自已实现的功能转给其他类实现
}
void LoadHeadkModel(const string& strno) override {
cout << "载入亡灵类怪物的头部模型,需要m_pMonster指针调用M_Undead类或其父类中其他诸多成员函数,逻辑代码......" << strno << endl;
}
void LoadLimbsModel(const string& strno) override {
cout <<"载入亡灵类怪物的四肢模型,需要m_pMonster指针调用M_Undead类或其父类中其他诸多成员函数,逻辑代码......" << strno << endl;
}
};
// 元素怪物构建器
class ElementBuilder : public MonsterBuilder {
public:
ElementBuilder() {
m_pMonster = make_unique<M_Element>(); // 创建元素怪物
}
void LoadTrunkModel(const string& strno) override {
cout << "载入元素类怪物的躯干部位模型,需要m_pMonster指针调用M_Undead类或其父类中其他诸多成员函数,逻辑代码......" << strno << endl;
具体要做的事情其实是委托给怪物子类来完成,委托指把本该自已实现的功能转给其他类实现
}
void LoadHeadkModel(const string& strno) override {
cout << "载入元素类怪物的头部模型,需要m_pMonster指针调用M_Undead类或其父类中其他诸多成员函数,逻辑代码......" << strno << endl;
}
void LoadLimbsModel(const string& strno) override {
cout << "载入元素类怪物的四肢模型,需要m_pMonster指针调用M_Undead类或其父类中其他诸多成员函数,逻辑代码......" << strno << endl;
}
};
// 机械怪物构建器
class MechanicBuilder : public MonsterBuilder {
public:
MechanicBuilder() {
m_pMonster = make_unique<M_Mechanic>(); // 创建机械怪物
}
void LoadTrunkModel(const string& strno) override {
cout << "载入机械类怪物的躯干部位模型,需要m_pMonster指针调用M_Undead类或其父类中其他诸多成员函数,逻辑代码......" << strno << endl;
具体要做的事情其实是委托给怪物子类来完成,委托指把本该自已实现的功能转给其他类实现
}
void LoadHeadkModel(const string& strno) override {
cout << "载入机械类怪物的头部模型,需要m_pMonster指针调用M_Undead类或其父类中其他诸多成员函数,逻辑代码......" << strno << endl;
}
void LoadLimbsModel(const string& strno) override {
cout << "载入机械类怪物的四肢模型,需要m_pMonster指针调用M_Undead类或其父类中其他诸多成员函数,逻辑代码......" << strno << endl;
}
};
在上述代码中,可以注意到,在MonsterBuilder
类中放置了一个指向Monster
类的成员变量智能指针m_pMonster
,同时引人GetResult
成员函数用于返回这个m_pMonster
指针,也就是说,当一个复杂的对象通过构建器构建完成后,可以通过GetResult
返回。
重点观察MonsterBuilder
类中的Assemble
成员函数,前面曾经提过,该成员函数中的代码是稳定的,不会发生变化。所以可以继续把Assemble
成员函数的功能拆出到一个新类中(这步拆分也不是必需的)。创建新类MonsterDirector
(扮演一个指挥者角色),将
MonsterBuilder
类中的Assemble
成员函数整个迁移到MonsterDirector
类中并按照惯例重新命名为Construct
,同时,在MonsterDirector
类中放置一个指向MonsterBuilder
类的成员变量指针m_pMonsterBuilder
,同时对Construct
成员函数的代码进行调整(注意也增加了返回值)。完整的MonsterDirector
类代码如下:
//指挥者类
class MonsterDirector {
public:
// 使用 unique_ptr 作为构造函数的参数
MonsterDirector(std::unique_ptr<MonsterBuilder> ptmpBuilder)
: m_pMonsterBuilder(std::move(ptmpBuilder)) {}
// 使用构建器创建怪物
unique_ptr<Monster> ConstructMonster(const string& modelNumber) {
m_pMonsterBuilder->LoadTrunkModel(modelNumber.substr(0, 3)); // 组装躯干
m_pMonsterBuilder->LoadHeadkModel(modelNumber.substr(3, 3)); // 组装头部
m_pMonsterBuilder->LoadLimbsModel(modelNumber.substr(6, 3)); // 组装四肢
return m_pMonsterBuilder->GetResult();
}
void SetBuilder() {
//指定新的生成器
}
private:
unique_ptr<MonsterBuilder> m_pMonsterBuilder; // 指向生成器类的父类
};
我们可以这样使用:
string modelNumber = "1253679201245"; // 模型编号
// 创建指挥者,并传入智能指针
auto undeadBuilder = std::make_unique<UndeadBuilder>();
MonsterDirector director(std::move(undeadBuilder));
// 构造怪物
director.ConstructMonster(modelNumber);
引入生成器模型的定义:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
在上述范例中,MonsterBuilder
类是对象的构建,而Monster
类是对象的表示,这两个类是相互分离的。构建过程是指MonsterDirector
类中的Construct
成员函数所代表的怪物模型的载人和装配(挂接)过程,该过程稳定不会发生变化(稳定的算法),所以只要传递给MonsterDirector
不同的构建器子类(M_UndeadBuilder
、M_ElementBuilder
、M_MechanicBuilder
),就会构建出不同的怪物,可以随时调用MonsterDirector
类的SetBuilder
成员函数为MonsterDirector
(指挥者)指定一个新的构建器以创建不同种类的怪物对象。
我们可以发现生成器模式包含四种角色。
- 抽象构建器(Builder):为创建一个产品对象的各个部件指定抽象接口
(LoadTrunkModel
、LoadHeadModel
、LoadLimbsModel
),同时,也会指定一个接口(GetResult
)用于返回所创建的复杂对象。这里指MonsterBuilder
类。 - 具体构建器(Concrete Builder):实现了
Builder
接口以创建(构造和装配)该产品的各个部件,定义并明确其所创建的复杂对象,有时也可以提供一个方法用于返回创建好的复杂对象。这里指M_UndeadBuilder
、M_ElementBuilder
、M_MechanicBuilder
类。 - 产品(Product):指的是被构建的复杂对象,其包含多个部件,由具体构建器创建该产品的内部表示并定义它的装配过程。这里指
M_Undead
、M_Element
、M_Mechanic
类。 - 指挥者(Director):又称主管类,这里指
MonsterDirector
类。该类有一个指向抽象构建器的指针(m_pMonsterBuilder
),利用该指针可以在Construct
成员函数中调用构建器对象中“构建和装配产品部件”的方法来完成复杂对象的构建,只要指定不同的具体构建器,用相同的构建过程就会构建出不同的产品。同时,Construct
成员函数还控制复杂对象的构建次序(例如,在Construct
成员函数中对LoadTrunkModel
、LoadHeadModel
、LoadLimbsModel
的调用是有先后次序的)。在使用这段内容时,只需要生成一个具体的构建器对象,并利用该构建器对象创建指挥者对象并调用指挥者类的Construct
成员函数,就可以构建一个复杂的对象。
前面已经说过,从MonsterBuilder
分拆出MonsterDirector
这步不是必需的,不做分拆可以看作生成器模式的一种退化情形,当然,此时客户端就需要直接针对构建器进行编码了。一般的建议是:如果MonsterBuilder
类本身非常庞大、非常复杂,则进行分拆,否则可以不进行分拆,总之,复杂的东西就考虑做拆解,简单的东西就考虑做合并。
生成器模式结构
生成器模式的核心组成部分如下:
- 生成器接口(Builder):声明通用的产品构造步骤。
- 具体生成器(Concrete Builders):实现生成器接口,提供不同的构造过程,能够生成不遵循同一接口的产品。
- 产品(Products):最终生成的对象,不同生成器构造的产品可以不属于同一类层次结构。
- 主管(Director):定义构造步骤的调用顺序,用于创建和复用特定的产品配置。
- 客户端(Client):将生成器对象与主管类关联,通过主管类调用生成器来构建产品。可以在不同的构建过程中使用不同的生成器。
三、总结
通过上述案例,我们不难看出生成器模式主要用于分布建立一个复杂的对象,其中的构建步骤是一个稳定的算法,而复杂对象各个部分的创建会有不同的变化。
生成器模式的核心要点在于将构建算法和具体的构建算法分离,这样构建算法就可以被重用,通过编写不同的代码又可以很方便地对构建实现进行功能扩展。引入指挥者类后,只要使用不同的生成器,利用相同的构建过程就可以构建出不同的产品。
构建器接口定义的是如何构建各个部件,也就是说,当需要创建具体部件的时候,交给构建器来做。而指挥者有两个作用:
- 负责通过部件以指定的顺序来构建整个产品(控制了构建的过程)。
- 指挥者通过提供Construct接口隔离了客户端(指main主函数中的代码)与具体构建过程必须要调用的类的成员函数之间的关联
对于客户端,只需要知道各种具体的构建器以及指挥者的Construct
接口即可,并不需要知道如何构建具体的产品。想象一个项目开发小组,如果main
中构建产品的代码由普通组员编写,这项工作自然比较轻松,但是,支撑代码编写所运用的设计模式及实现一般是由组长来完成,显然这项工作要复杂得多。
工厂方法模式与生成器模式也有类似之处,但生成器模式侧重于一步步构建一个复杂的产品对象,构建完成后返回所构建的产品,工厂方法模式侧重于多个产品对象(且对象所属的类继承自同一个父类)的构建而无论产品本身是否复杂。
-
生成器模式可以分步创建对象, 暂缓创建步骤或递归运行创建步骤。
-
生成不同形式的产品时, 可以复用相同的制造代码。将一个复杂对象的创建过程封装起来。用同一个构建算法可以构建出表现上完全不同的产品,实现产品构建和产品表现(表示)上的分离。建造者模式也正是通过把产品构建过程独立出来,从而才使构建算法可以被复用。这样的程序结构更容易扩展和复用。
-
单一职责原则。 可以将复杂构造代码从产品的业务逻辑中分离出来。
-
产品的实现可以随时被替换(将不同的构建器提供给指挥者)。而且向客户端隐藏了产品内部的表现。
但是有如下缺点:
- 要求所创建的产品有比较多的共同点,创建步骤(组成部分)要大致相同,如果产品很不相同,创建步骤差异极大,则不适合使用建造者模式,这是该模式使用范围受限的地方。
- 生成器模式涉及很多的类,例如需要组合指挥者和构建器对象,然后才能开始对象的构建工作。
生成器重点关注如何分步生成复杂对象。 抽象工厂专门用于生产一列相关对象。 抽象工厂会马上返回产品, 生成器则允许你在获取产品前执行一些额外构造步骤。你可以在创建复杂组合模式树时使用生成器, 因为这可使其构造步骤以递归的方式运行。可以结合使用生成器和桥接模式: 主管类负责抽象工作, 各种不同的生成器负责实现工作。