三、建造者模式——工厂模式
3.1 工厂模式
创建一个类对象的传统方式是使用关键字new, 因为用new 创建的类对象是一个堆对象,可以实现多态。工厂模式通过把创建对象的代码包装起来,实现创建对象的代码与具体 的业务逻辑代码相隔离的目的(将对象的创建和使用进行解耦)。试想,如果创建一个类 A 的对象,可能会写出
A* pobja = new A();
这样的代码行,但当给类A 的构造函数增加 一个参数时,所有利用new 创建类 A 对象的代码行全部需要修改,如果通过工厂模式把创 建类 A 对象的代码统一放到某个位置,则对于诸如给类 A 的构造函数增加参数之类的问 题,只需要修改一个位置就可以了。
工厂模式属于创建型模式, 一般可以细分为3种:简单工厂模式、工厂方法模式和抽象 工厂模式。每种都有不同的特色和应用场景,本章将会逐一介绍。在讲解过程中,还会引出 面向对象程序设计的一个重要原则——开闭原则,并对该原则进行细致的阐述。
3.1.1 简单工厂模式
简单工厂(Simple Factory)模式在四人组写的《设计模式——可复用面向对象软件的基 础》中并没有出现,所以可以认为这并不算是一个标准的设计模式,但因为其应用场合比较 多,所以在这里专门介绍一下。此外,有些书籍并不把简单工厂模式看成一种设计模式,只 是看成一种编程手法,这也没什么问题,在笔者看来,更倾向把简单工厂模式看成一种编程 手法或者编程技巧。
之所以叫简单工厂模式,是因为该模式与其他两种工厂模式(工厂方法模式和抽象工厂 模式)比较而言,实现的代码相对简单,作为其他两种工厂模式学习的基础非常合适。
这里继续以前面的单机闯关打斗类游戏的开发为例来阐述工厂模式。游戏中的主角需 要通过攻击并杀死怪物来进行闯关,策划规定,在该游戏中,暂时有3类怪物(后面可能会增 加新的怪物种类),分别是亡灵类怪物、元素类怪物、机械类怪物,每种怪物都有一些各自的 特点(细节略),当然,这些怪物还有一些共同特点,例如同主角一样,都有生命值、魔法值、攻 击力3个属性,为此,创建一个 Monster (怪物)类作为父类,而创建 M_Undead (亡灵类怪物)、M_Element(元素类怪物)和 M_Mechanic (机械类怪物)作为子类是合适的。针对怪 物,程序定义了如下几个类:
class Monster {
public:
Monster(int life, int magic, int attack);
virtual ~Monster();
protected:
int m_life;
int m_magic;
int m_attack;
};
class M_Undead :public Monster {
public:
M_Undead(int life,int magic,int attack);
};
class M_Element :public Monster {
public:
M_Element(int life, int magic, int attack);
};
class M_Mechanic :public Monster {
public:
M_Mechanic(int life, int magic, int attack);
};
Monster::Monster(int life, int magic, int attack)
:m_life(life), m_magic(magic), m_attack(attack) {}
Monster::~Monster() {}
M_Undead::M_Undead(int life, int magic, int attack)
: Monster(life, magic, attack)
{
std::cout << "一只亡灵类怪物来到了这个世界" << std::endl;
}
M_Element::M_Element(int life, int magic, int attack)
: Monster(life, magic, attack)
{
std::cout << "一只元素类怪物来到了这个世界" << std::endl;
}
M_Mechanic::M_Mechanic(int life, int magic, int attack)
: Monster(life, magic, attack)
{
std::cout << "一只机械类怪物来到了这个世界" << std::endl;
}
当需要在游戏的战斗场景中产生怪物时,传统方法可以使用new 直接产生各种怪物, 例如在main 主函数中可以加入如下代码:
Monster* pM1 = new M_Undead(300, 50, 80);
Monster* pM2 = new M_Element(200, 80, 100);
Monster* pM3 = new M_Mechanic(400, 0, 110);
delete pM1;
delete pM2;
delete pM3;
上面这种创建怪物的写法虽然合法,但不难看到,当创建不同种类的怪物时,避免不了 直接与多个怪物类(M_Undead 、M_Element 、M_Mechanic) 打交道,这属于一种依赖具体类 的紧耦合,因为需要知道这些类的名字,尤其是随着游戏内容的不断增加,怪物的种类也可 能会不断增加。
如果通过某个扮演工厂角色的类(怪物工厂类)来创建怪物,则意味着创建怪物时不再 使 用new 关键字,而是通过该工厂类来进行,这样的话,即便将来怪物的种类增加,main 主 函数中创建怪物的代码也可以尽量保持稳定。通过工厂类,避免了在main 函数中(也可以 在任何其他函数中)直接使用new 创建对象时必须知道具体类名(这是一种依赖具体类的 紧耦合关系)的情形发生,实现了创建怪物的代码与各个具体怪物类对象要实现的业务逻辑 代码隔离,这就是简单工厂模式的实现思路。当然,和使用new 创建对象的直观性比,显然 简单工厂模式的实现思路是绕了弯的。
下面就创建一个怪物工厂类 MonsterFactory, 用这个工厂类来生产(产生)出各种不同 种类的怪物,代码如下:
class Monster;
class MonsterFactory
{
public:
Monster* createMonster(std::string strmontype);
};
Monster* MonsterFactory::createMonster(std::string strmontype)
{
Monster* prtnobj = nullptr;
if (strmontype == "udd") {
prtnobj = new M_Undead(300, 50, 80);
}
else if (strmontype == "elm") {
prtnobj = new M_Element(200, 80, 100);
}
else if (strmontype == "mec") {
prtnobj = new M_Mechanic(400, 0, 110);
}
return prtnobj;
}
在 main 主函数中,注释掉原有代码,增加如下代码:
MonsterFactory facobj;
Monster* pM1 = facobj.createMonster("udd");
Monster* pM2 = facobj.createMonster("elm");
Monster* pM3 = facobj.createMonster("mec");
delete pM1;
delete pM2;
delete pM3;
代码经过改造后,创建各种怪物时就不必面对(书写)M_Undead 、M_Element 、M_Mechanic 等具体的怪物类,只要面对 MonsterFactory 类即可。当然,其实main 主函数创 建对象时遇到的麻烦(依赖具体怪物类)依旧存在,只是被转嫁给了MonsterFactory类而 已。其实,依赖这件事本身并不会因为引入设计模式而完全消失,程序员能做的是把这种依 赖的范围尽量缩小(例如缩小到 MonsterFactory类的createMonster成员函数中),从而避 免依赖关系遍布整个代码(所有需要创建怪物对象的地方),这就是所谓的封装变化(把容易 变化的代码段限制在一个小范围内),就可以在很大程度上提高代码的可维护性和可扩展 性,否则可能会导致一修改代码就要修改一大片的困境。例如以往如果这样写代码:
Monster*pM1 =new M_Undead(300,50,80);
那么一旦要对圆括号中的参数类型进行修改或者新增参数,则所有涉及new M_Undead的 代码段可能都要修改,但采用简单工厂模式后,只需要修改 MonsterFactory 类 的 createMonster 成员函数,确实省了很多事。
MonsterFactory类的实现也有缺点。最明显的缺点就是当引入新的怪物类型时,需要 修改createMonster 成员函数的源码来增加新的if判断分支,从而支持对新类型怪物的创 建工作,这违反了面向对象程序设计的一个原则-——开闭原则(Open Close Principle OCP)
面向对象程序设计有几大原则比较难理解,讲解时需要相关的代码做讲解支撑才容易 懂,所以笔者尽可能遇到时再讲解。这里提一下开闭原则,开闭原则讲的是代码的扩展性问 题,它是这样解释的:对扩展开放,对修改关闭(封闭)。这个解释太粗糙,如果解释得详细 一点,应该是这样:当增加新功能时,不应该通过修改已经存在的代码来进行(修改 MonsterFactory 类中的createMonster 成员函数就属于修改已经存在的代码范畴),而应该 通过扩展代码(例如增加新类、增加新成员函数等)来进行。开闭原则一般是面向对象程序 设计所追求的目标。
前述通过修改createMonster 成员函数来增加对新类型怪物的支持,违反了开闭原则, 得到的好处是代码阅读起来简单明了,但如果通过扩展代码来增加对新怪物的支持,那么代 码会复杂很多,也会在相当程度上增加对代码的理解难度,具体如何通过扩展代码来践行开 闭原则,后面的讲解中会详细谈到。
请记住,如果if 分支语句并不是很多(此时用简单工厂设计模式就是适合的),例如只 有数个而并不是数十上百个,那么适当地违反开闭原则完全可以接受。当然,如果怪物类型 只有2或3种且不经常变动则不引入工厂类 MonsterFactory 而直接采用new 的方式创建 对象也仍然可以,开发者需要在代码的可读性和可扩展性之间做出权衡,在应用设计模式时 不应该生搬硬套,而是依据实际情形和实际应用场景确定。
引入“简单工厂”设计模式的定义(实现意图):定义一个工厂类(MonsterFactory), 该 类的成员函数(createMonster) 可以根据不同的参数创建并返回不同的类对象,被创建的对 象所属的类(M_Undead 、M_Element 、M_Mechanic) 一般都具有相同的父类(Monster) 。 调 用者(这里指 main 函数)无须关心创建对象的细节。
也可以把MonsterFactory 类中的 createMonster 实现为静态成员函数,具体如下:
public:
static Monster*createMonster(string strmontype)
{}
这样在 main 函数中就不必创建 facobj 对象,直接采用诸如
MonsterFactory::createMonster("udd");
的调用方式创建怪物即可,此时的简单工厂模式又可以称为静态 工厂方法(Static Factory Method)模式。
针对前面的代码范例绘制一下简单工厂模式的UML 图,如图3.1所示。 在图3.1中,可以看到:
-
(1)类与类之间以实线箭头表示父子关系,子类(M_Undead、M_Element、M_Mechanic)
与父类(Monster) 之间有一条带箭头的实线,箭头的方向指向父类。 MonsterFactory 类与 M_Undead、M_Element、M_Mechanic 类之间的虚线箭头表示箭头连接的两个类之间存在 着依赖关系(一个类引用另一个类),换句话说,虚线箭头表示一个类(MonsterFactory) 实例 化另外一个类(M_Undead、M_Element、M_Mechanic) 的对象,箭头指向被实例化对象 的 类 。 -
(2)因为创建怪物只需要与 MonsterFactory 类打交道,所以创建怪物的代码(调用createMonster 成员函数的代码)是稳定的,但增加新类型怪物需要修改 MonsterFactory 类 的 createMonster 成员函数代码,所以 createMonster 成员函数是变化的。
-
(3)如果 MonsterFactory 类由第三方开发商开发,该开发商并不希望将 M_Undead 、 M_Element 、M_Mechanic 这些类的名字等信息暴露给开发者,那么通过为开发者提供 createMonster 成员函数(接口)来创建出不同类型的怪物就可以实现具体怪物类的隐藏效 果,同时也实现了创建怪物对象的代码与具体的怪物类(M_Undead 、M_Element、
M Mechanic) 解耦合(对任意一个模块的更改都不会影响另外一个模块)的效果。
3.1.2 工厂方法模式
有些书籍资料会把简单工厂模式看成工厂方法(Factory Method)模式的特例(笔者认 为这种看法不太合适,读者学习完本模式后可自行体会),所以并不会单独讲解简单工厂模 式。工厂方法模式是使用频率最高的工厂模式,而人们通常所说的工厂模式也常常指的就 是工厂方法模式,换句话说,工厂方法模式可以简称为工厂模式或多态工厂模式,这种模式 的实现难度比简单工厂模式略高一些。
前面讲解简单工厂模式时,读者已经注意到了如果引入新的怪物类型,则必须要修改 MonsterFactory 类的createMonster 成员函数来增加新的 if 判断分支,如果怪物的种类非 常多,那么这个if 判断分支会很长,从而造成逻辑过于烦琐使代码变得难以维护,同时,前 面也介绍了createMonster 成员函数的设计违反了开闭原则(对扩展开放,对修改关闭)。 工厂方法模式的引入,很好地满足了面向对象程序设计的开闭原则。当增加新的怪物类型 时,工厂方法模式采用增加新的工厂类的方式支持新怪物类型(不影响已有的代码)。与简 单工厂模式相比,工厂方法模式的灵活性更强,实现也更加复杂(增加了理解难度),同时也 要引入更多的新类(主要是引入新的工厂类)。
本节以简单工厂模式中实现的代码为基础进行代码改造,将用简单工厂模式实现的代码修改为用工厂方法模式实现。
在工厂方法模式中,不是用一个工厂类MonsterFactory 来解决创建多种类型怪物的问 题,而是用多个工厂类来解决创建多种类型怪物的问题。而且,针对每种类型的怪物,都需 要创建一个对应的工厂类,例如,当前要创建3种类型的怪物 M_Undead 、M_Element 、 M_Mechanic, 那么,就需要创建3个工厂类,例如分别命名为 M_UndeadFactory、
M_ElementFactory 、M_MechanicFactory 。 而且这3个工厂类还会共同继承自同一个工厂 父类,例如将该工厂父类命名为 M_ParFactory (工厂抽象类)。
如果将来策划要求引入第四种类型的怪物,那么毫无疑问,需要为该种类型的怪物增加 对应的一个新工厂类,当然该新工厂类依然继承自M_ParFactory 类。
从上面的描述,可以初步看出,工厂方法模式通过增加新的工厂类来符合面向对象程序 设计的开闭原则(对扩展开放,对修改关闭),但付出的代价是需要增加多个新的工厂类。
下面开始改造简单工厂模式中实现的代码。首先注释掉main 主函数中的所有代码, 然后将原有的怪物工厂类 MonsterFactory 也注释掉。接着,先来实现所有工厂类的父类 M_ParFactory (等价于将简单工厂模式中的工厂类MonsterFactory 进行抽象),代码如下:
class M_ParFactory {
public:
virtual Monster* createMonster() = 0;
virtual ~M_ParFactory();
};
M_ParFactory::~M_ParFactory()
{
}
然后,针对每个具体的怪物子类,都需要创建一个相关的工厂类,所以,针对 M_Undead 、M_Element 、M_Mechanic 类,创建3个工厂类M_UndeadFactory 、M_ElementFactory、M_MechanicFactory, 代码如下:
class M_UndeadFactory :public M_ParFactory {
public:
virtual Monster* createMonster() override;
};
class M_ElementFactory :public M_ParFactory {
public:
virtual Monster* createMonster() override;
};
class M_MechanicFactory :public M_ParFactory {
public:
virtual Monster* createMonster() override;
};
Monster* M_UndeadFactory::createMonster()
{
return new M_Undead(300, 50, 80);
}
Monster* M_ElementFactory::createMonster()
{
return new M_Element(200, 80, 100);
}
Monster* M_MechanicFactory::createMonster()
{
return new M_Mechanic(400, 0, 100);
}
有了这3个怪物工厂类之后,可以创建一个全局函数Gbl_CreateMonster 来处理怪物 对象的生成,代码如下:
class M_ParFactory;
Monster* Gbl_CreateMonster(M_ParFactory* factory);
Monster* Gbl_CreateMonster(M_ParFactory* factory) {
return factory->createMonster();
}
从现在的代码可以看到,Gbl_CreateMonster作为创建怪物对象的核心函数,并不依赖于具体的M_Undead、M_Element、M_Mechanic怪物类,只依赖于Monster类(Gbl_CreateMonster的返回类型)和M_ParFactory类(Gbl_CreateMonster的形参类型),变化的部分被隔离到调用Gbl CreateMonster函数的地方去了。
在main主函数中,注释掉原有代码,增加如下代码来通过各自的工厂生产各自的产品:
M_ParFactory* p_ud_fy = new M_UndeadFactory();
Monster* pM1 = Gbl_CreateMonster(p_ud_fy);
M_ParFactory* p_elm_fy = new M_UndeadFactory();
Monster* pM2 = Gbl_CreateMonster(p_elm_fy);
M_ParFactory* p_mec_fy = new M_MechanicFactory();
Monster* pM3 = Gbl_CreateMonster(p_mec_fy);
delete p_ud_fy;
delete p_elm_fy;
delete p_mec_fy;
delete pM1;
delete pM2;
delete pM3;
从上述代码可以看到,创建怪物对象时,不需要记住具体怪物类的名称,但需要知道创建该类怪物的工厂的名称。
引入工厂方法设计模式的定义(实现意图):定义一个用于创建对象的接口(M_ParFactory类中的createMonster成员函数,这其实就是工厂方法,工厂方法模式的名字也是由此而来),但由子类(M_UndeadFactory、M_ElementFactory、M_MechanicFactory)决定要实例化的类是哪一个。该模式使得某个类(M_Undead、M_Element、M_Mechanic)的实例化延迟到子类(_UndeadFactory、M_ElementFactory、M_MechanicFactory)。
针对前面的代码范例绘制一下工厂方法模式的UML图,如图3.2所示。
在图3.2中,可以看到:
-
Gbl CreateMonster函数所依赖的Monster类和ParFactory类都属于稳定部分(不需要改动的类)。
-
M_UndeadFactory、M_ElementFactory、M_MechanicFactory类以及M_Undead、_Element、M_Mechanic类都属于变化部分。Gbl CreateMonster函数并不依赖于这些变化部分。
-
当出现一个新的怪物类型[例如M Beast(野兽类怪物)]时,既不需要更改Gbl_CreateMonster函数,也不需要像简单工厂模式那样修改MonsterFactory类中的createMonster成员函数来增加新的if分支,除了要添加继承自Monster的类MBeast之外,只需要为新的怪物类型M_Beast增加一个新的继承自M_ParFactory的工厂类M BeastFactory即可。这正好符合面向对象程序设计的开闭原则一对扩展开放,对修改关闭(封闭)。所以,一般可以认为,将简单工厂模式的代码通过把工厂类进行抽象改造成符合开闭原则后的代码,就变成了工厂方法模式的代码。
-
如果M ParFactory工厂类以及各个工厂子类由第三方开发商开发,那么利用工厂方法模式可以很好地隐藏 M_Undead 、M_Element 、M_Mechanic 类,使其不暴露给 开发者。
-
可以根据实际需要扩充M ParFactory 中的接口(虚函数),例如增加游戏中对其他 内容[例如 NPC(非玩家角色,如商人、路人等)]的创建支持,或者不实现成抽象类 而为 createMonster 提供一些默认实现,等等,这方面读者可以发挥自身的想象力和 创造力。
-
增加新的工厂类是工厂方法设计模式必须付出的代价。
关于使用工厂模式的好处,再次阐明一下笔者的观点。从宏观的角度来讲,所有的工厂模 式(简单工厂模式、工厂方法模式、抽象工厂模式)都致力于将new 创建对象这件事集中到某个 或者某些工厂类的成员函数(createMonster) 中去做
这样做有几个非常明显的好处。
- (1)在讲解简单工厂模式时已经说过,就是希望封装变化,想将依赖具体怪物类的范围 尽量缩小,试想如果将来new 相关的代码行需要修改,例如原来是如下代码行:
prtnobj = new M_Element(200,80,100);
现在需要增加一个参数或者修改一个已有的参数:
prtnobj = new M_Element(200,80,80,100);
那么利用了工厂模式的代码,只需要修改工厂类的成员函数(createMonster) 即可;
如果不 采用工厂模式,则代码中凡是涉及new M Element(200,80,100);
的代码段可能都需要 修改,这是一个极其繁重又枯燥的工作。
当然,如果不怕暴露各种怪物类的类名,又不想写这么多的工厂子类,单纯地只是想封 装变化,也就是想把创建怪物对象的代码段限制在createMonster 成员函数中,那么通过创 建一个继承自M ParFactory 类的子类模板,也能达到同样的效果。参考如下代码段:
template<typename T>
class M_ChildFactory :public M_ParFactory {
public:
virtual Monster* createMonster() override {
return new T(0, 0, 0); //没有数值
}
virtual Monster* createMonster(int life,int magic,int attack) override {
return new T(life, magic, attack);
}
};
main 主函数中,可以像下面这样来使用M_ChildFactory 类模板:
void Test4() {
M_ChildFactory<M_Undead>p_ud_fy;
M_ChildFactory<M_Element>p_elm_fy;
M_ChildFactory<M_Mechanic>p_mec_fy;
Monster* pM1 = p_ud_fy.createMonster();
Monster* pM2 = p_elm_fy.createMonster();
Monster* pM3 = p_mec_fy.createMonster();
delete pM1;
delete pM2;
delete pM3;
}
对于工厂方法模式与简单工厂模式相比有什么明显不同或者说好处的问题,其实上面 已经解释得很清楚了,面向对象程序设计原则告诉人们:“修改现有的代码来实现一个新功 能不如通过增加新代码来实现该功能好。”为了遵循这个原则,人们将简单工厂模式通过将 工厂类进行抽象的方法进行改造升级成了工厂方法模式。如果从源码实现的角度看,也可 以这样解释:简单工厂模式把创建对象这件事放到了一个统一的地方来处理,弹性比较差, 而工厂方法模式相当于建立了一个程序实现框架,从而让工厂子类来决定对象如何创建。
另外,必须注意,工厂方法模式往往需要创建一个与产品等级结构(层次)相同的工厂等 级结构,这也增加了新类的层次结构和数目。
3.1.3 抽象工厂模式
1. 战斗场景分类范例
继续前面开发的单机闯关打斗类游戏,随着游戏内容越来越丰富,游戏中战斗场景(关 卡)数量和类型不断增加,从原来的在城镇中战斗逐步进入在沼泽地战斗、在山脉地区战斗 等。于是,策划把怪物种类进一步按照场景进行了分类,怪物目前仍旧保持3类:亡灵类、 元素类和机械类。战斗场景也分为3类:沼泽地区、山脉地区和城镇。这样来划分的话,整 个游戏中目前就有9类怪物:沼泽地区的亡灵类、元素类、机械类怪物;山脉地区的亡灵类、 元素类、机械类怪物;城镇中的亡灵类、元素类、机械类怪物。策划规定每个区域的同类型 怪物能力上差别很大,例如,沼泽地中的亡灵类怪物攻击力比城镇中的亡灵类怪物高很多, 山脉地区的机械类怪物会比沼泽地区的机械类怪物生命值高许多。
这样看起来,从怪物父类Monster 继承而来的怪物子类就会由原来的3种M_Undead、
M_Element 、M_Mechanic 变为9种,按照这样的怪物分类方式,使用工厂方法模式创建怪 物对象则需要创建多达9个工厂子类,但如果一个工厂子类能够生产不止一种具有相同规 则的怪物对象,那么就可以有效地减少所创建的工厂子类数量,这就是抽象工厂(AbstractFactory)模式的核心思想。
有两个概念在抽象工厂模式中经常被提及,分别是“产品等级结构”和“产品族”。绘制 一个坐标轴,把前述的9种怪物放入其中,如图3.3所示。
在图3.3中,相同的形状代表种类相同但场景不同的怪物,横着按行来观察,发现每个 怪物的种类不同,但所有怪物都位于相同的场景中,例如都位于沼泽中(产品的产地相同), 每一行产品就是一个产品族(3行代表着3个产品族)。接着,竖着按列来观察,发现每个怪 物的种类相同,但每个怪物都位于不同的场景中,那么每一列怪物就是一个产品等级结构(3 列代表着3个产品等级结构)。不难想象,如果用一个工厂子类生产1个产品族(1行),那 么因为有3个产品族(3行),所以只需要3个工厂就可以生产9个产品(9种怪物对象)。所以在图中,所需的3个工厂分别是沼泽地区的工厂、山脉地区的工厂以及城镇的工厂。请记 住,抽象工厂模式是按照产品族来生产产品——一个地点有一个工厂,该工厂负责生产本产 地的所有产品。
现在,程序要根据策划的需求重新规划怪物对象的创建问题。保留 Monster 怪物父 类,删除原有的 M_Undead 、M_Element 、M_Mechanic 怪物子类,重新引入一共9个怪物 类。代码如下,注意代码中的注释部分:
class M_Undead_Swap :public Monster {
public:
M_Undead_Swap(int life, int magic, int attack);
};
class M_Element_Swap :public Monster {
public:
M_Element_Swap(int life, int magic, int attack);
};
class M_Mechanic_Swap :public Monster {
public:
M_Mechanic_Swap(int life, int magic, int attack);
};
class M_Undead_Mountain :public Monster {
public:
M_Undead_Mountain(int life, int magic, int attack);
};
class M_Element_Mountain :public Monster {
public:
M_Element_Mountain(int life, int magic, int attack);
};
class M_Mechanic_Mountain :public Monster {
public:
M_Mechanic_Mountain(int life, int magic, int attack);
};
class M_Undead_Town :public Monster {
public:
M_Undead_Town(int life, int magic, int attack);
};
class M_Element_Town :public Monster {
public:
M_Element_Town(int life, int magic, int attack);
};
class M_Mechanic_Town :public Monster {
public:
M_Mechanic_Town(int life, int magic, int attack);
};
M_Undead_Swap::M_Undead_Swap(int life, int magic, int attack)
: Monster(life, magic, attack)
{
std::cout << "一只沼泽的亡灵怪物来到了这个世界" << std::endl;
}
M_Element_Swap::M_Element_Swap(int life, int magic, int attack)
: Monster(life, magic, attack)
{
std::cout << "一只沼泽的元素怪物来到了这个世界" << std::endl;
}
M_Mechanic_Swap::M_Mechanic_Swap(int life, int magic, int attack)
: Monster(life, magic, attack)
{
std::cout << "一只沼泽的机械怪物来到了这个世界" << std::endl;
}
M_Undead_Mountain::M_Undead_Mountain(int life, int magic, int attack)
: Monster(life, magic, attack)
{
std::cout << "一只山脉的亡灵怪物来到了这个世界" << std::endl;
}
M_Element_Mountain::M_Element_Mountain(int life, int magic, int attack)
: Monster(life, magic, attack)
{
std::cout << "一只山脉的元素怪物来到了这个世界" << std::endl;
}
M_Mechanic_Mountain::M_Mechanic_Mountain(int life, int magic, int attack)
: Monster(life, magic, attack)
{
std::cout << "一只山脉的机械怪物来到了这个世界" << std::endl;
}
M_Undead_Town::M_Undead_Town(int life, int magic, int attack)
: Monster(life, magic, attack)
{
std::cout << "一只城镇的亡灵怪物来到了这个世界" << std::endl;
}
M_Element_Town::M_Element_Town(int life, int magic, int attack)
: Monster(life, magic, attack)
{
std::cout << "一只城镇的元素怪物来到了这个世界" << std::endl;
}
M_Mechanic_Town::M_Mechanic_Town(int life, int magic, int attack)
: Monster(life, magic, attack)
{
std::cout << "一只城镇的机械怪物来到了这个世界" << std::endl;
}
因为工厂是针对一个产品族进行生产的,所以总共需要创建1个工厂父类和3个工厂 子类。先看 一看工厂父类的写法:
class M_ParFactory {
public:
virtual Monster* createMonster_Undead() = 0;
virtual Monster* createMonster_Element() = 0;
virtual Monster* createMonster_Mechanic() = 0;
virtual ~M_ParFactory();
};
3个工厂子类代码如下:
lass M_Factory_Swap :public M_ParFactory {
public:
virtual Monster* createMonster_Undead() override;
virtual Monster* createMonster_Element() override;
virtual Monster* createMonster_Mechanic() override;
};
class M_Factory_Mountain :public M_ParFactory {
public:
virtual Monster* createMonster_Undead() override;
virtual Monster* createMonster_Element() override;
virtual Monster* createMonster_Mechanic() override;
};
class M_Factory_Town :public M_ParFactory {
public:
virtual Monster* createMonster_Undead() override;
virtual Monster* createMonster_Element() override;
virtual Monster* createMonster_Mechanic() override;
};
M_ParFactory::~M_ParFactory()
{
}
在 main 主函数中,注释掉原有代码,增加如下代码:
M_ParFactory* p_swap_fy = new M_Factory_Swap();
M_ParFactory* p_mou_fy = new M_Factory_Mountain();
M_ParFactory* p_twn_fy = new M_Factory_Town();
Monster* pM1 = p_swap_fy->createMonster_Undead();
Monster* pM2 = p_swap_fy->createMonster_Element();
Monster* pM3 = p_swap_fy->createMonster_Mechanic();
Monster* pM4 = p_mou_fy->createMonster_Undead();
Monster* pM5 = p_mou_fy->createMonster_Element();
Monster* pM6 = p_mou_fy->createMonster_Mechanic();
Monster* pM7 = p_twn_fy->createMonster_Undead();
Monster* pM8 = p_twn_fy->createMonster_Element();
Monster* pM9 = p_twn_fy->createMonster_Mechanic();
delete p_swap_fy;
delete p_mou_fy;
delete p_twn_fy;
delete pM1;
delete pM2;
delete pM3;
delete pM4;
delete pM5;
delete pM6;
delete pM7;
delete pM8;
delete pM9;
看一看抽象工厂模式的优缺点:
-
(1)如果游戏中的战斗场景新增加一个森林类型的场景而怪物种类不变(依旧是亡灵 类怪物、元素类怪物和机械类怪物),则只需要增加一个新的子工厂类,例如 M_Factory_ Forest 并 继 承 自 M_ParFactory, 而 后 在 M_Factory_Forest 类中实现 createMonster_ Undead 、createMonster_Element 、createMonster_Mechanic 虚函数(接口)即可。这种代码 实现方式符合开闭原则,也就是通过增加新代码而不是修改原有代码来为游戏增加新功能 (对森林类型场景中怪物的创建支持)。
-
(2)如果游戏中新增加了一个新的怪物种类(例如龙类怪物),则此时不但要新增3个 继承自Monster 的子类来分别支持沼泽龙类怪物、山脉龙类怪物、城镇龙类怪物,还必须修改工厂父类 M_ParFactory 来增加新的虚函数(例如 createMonsterDragon) 以支持创建龙 类怪物,各个工厂子类也需要增加对 createMonsterDragon 的支持。这种在工厂类中通过 修改已有代码来扩充游戏功能的方式显然不符合开闭原则。所以此种情况下不适合使用抽 象工厂模式。
-
(3)抽象工厂模式具备工厂方法模式的优点,从图3.3来看,如果只是增加新的产品族 (新增1行),则只需要增加新的子工厂类,符合开闭原则,这是抽象工厂模式的优点。但如 果增加新的产品等级结构(新增1列),那么就需要修改抽象层的代码,这是抽象工厂模式的 缺点,所以应该避免在产品等级结构不稳定的情况下使用该模式,也就是说,如果游戏中怪 物种类(亡灵类、元素类、机械类)比较固定的情况下,更适合使用抽象工厂模式。
针对前面的代码范例绘制工厂方法模式的UML吗,如下图所示
- 不同厂商生产不同部件范例
再举一个范例增加读者对抽象工厂模式的理解。
芭比娃娃受到很多人的喜爱,它主要由3个部件组成:身体(包括头、颈、躯干、四肢)、 衣服、鞋子。现在,中国、日本、美国的厂商都可以制造芭比娃娃的身体、衣服、鞋子部件。现 在要求制作两个芭比娃娃,其中一个芭比娃娃的身体、衣服、鞋子全部采用中国厂商制造的 部件,另一个芭比娃娃的身体部件采用中国厂商,衣服部件采用日本厂商,鞋子部件采用美 国 厂 商 。
这个题目就可以采用抽象工厂来实现,理一理类的设计思路: · 将身体、衣服、鞋子这3个部件实现为抽象类。
- 实现一个抽象工厂,分别用来生产身体、衣服、鞋子这3个部件。
- 针对不同厂商的每个部件实现具体的类以及每个厂商所代表的具体工厂。 身体、衣服、鞋子3个部件的抽象类实现代码如下:
lass Body {
public:
virtual void getName() = 0;
virtual ~Body();
};
class Clothes
{
public:
virtual void getName() = 0;
virtual ~Clothes();
};
class Shoes {
public:
virtual void getName() = 0;
virtual ~Shoes();
};
class AbstractFactory {
public:
virtual Body* createBody() = 0;
virtual Clothes* createClothes() = 0;
virtual Shoes* createShoes() = 0;
virtual ~AbstractFactory();
};
抽象类和抽象工厂都具备的情况下,可以写一个芭比娃娃类如下:
class BarbieDoll {
public:
BarbieDoll(Body* m_body,Clothes* m_cloth,Shoes* m_shoes);
void Assemble();
private:
Body* body;
Clothes* cloth;
Shoes* shoes;
};
接着,就是针对每个厂商、针对每个部件实现具体部件类和具体工厂类。
class China_Body : public Body {
public:
virtual void getName();
};
class China_Clothes : public Clothes {
public:
virtual void getName();
};
class China_Shoes : public Shoes {
public:
virtual void getName();
};
class Japan_Body : public Body {
public:
virtual void getName();
};
class Japan_Clothes : public Clothes {
public:
virtual void getName();
};
class Japan_Shoes : public Shoes {
public:
virtual void getName();
};
class America_Body : public Body {
public:
virtual void getName();
};
class America_Clothes : public Clothes {
public:
virtual void getName();
};
class America_Shoes : public Shoes {
public:
virtual void getName();
};
现在,在main 主函数中,就可以生产第一个芭比娃娃了(身体、衣服、鞋子全部采用中 国厂商制造的部件),代码如下:
AbstractFactory* pChinaFactory = new ChinaFactory();
Body* pChinaBody = pChinaFactory->createBody();
Clothes* pChinaClothes = pChinaFactory->createClothes();
Shoes* pChinaShoes = pChinaFactory->createShoes();
BarbieDoll* pbdlobj = new BarbieDoll(pChinaBody, pChinaClothes, pChinaShoes);
pbdlobj->Assemble();
接着,生产第二个芭比娃娃(身体采用中国厂商,衣服采用日本厂商,鞋子采用美国厂商),代码如下:
AbstractFactory* pChinaFactory = new ChinaFactory();
AbstractFactory* pJapanFactory = new JapanFactory();
AbstractFactory* pAmericaFactory = new AmericaFactory();
Body*pChinaBody2 =pChinaFactory->createBody();
Clothes* pJapanClothes = pJapanFactory->createClothes();
Shoes* pAmericaShoes = pAmericaFactory->createShoes();
BarbieDoll* pbd2obj = new BarbieDoll(pChinaBody2, pJapanClothes, pAmericaShoes);
pbd2obj->Assemble();
针对前面的代码范例绘制工厂方法模式的UML 图,如图3.5所示。
从图3.5中可以看到,如果新增一个法国工厂也同样生产身体、衣服、鞋子部件,那么编 写代码并不复杂,并且代码也符合开闭原则。从整体看,抽象工厂整个确实比较复杂,无论 是产品还是工厂都进行了抽象。
抽象工厂 AbstractFactory 定 义 了 一 组 虚 函 数 (createBody 、createClothes 、createShoes), 而在工厂子类中这一组虚函数中的每一个都负责 创建一个具体的产品,例如China Body、Japan Clothes等。
相应地,绘制模式示意图,如图3.6所示。
在本范例中,能够成功运用抽象工厂模式的前提是所创建的部件应该保持稳定,始终是 身体、衣服、鞋子这3个部件,如果部件是不稳定的,例如将来会增加新的部件,那么采用抽 象工厂模式编程,代码的改动就会非常大并且违反开闭原则,这时可以考虑使用单独的工厂 方法模式,也许会更灵活一些。
下面再分析一下工厂方法模式与抽象工厂模式的区别:工厂方法模式适用于一个工厂 生产一个产品的需求,抽象工厂模式适用于一个工厂生产多个产品(一个产品族)的需求(笔 者认为抽象工厂模式改名为“产品族工厂方法模式”似乎更合适)。另外,无论是产品族数量 较多还是产品等级结构数量较多,抽象工厂的优势都将更加明显。
引入“抽象工厂”设计模式的定义(实现意图):提供一个接口(AbstractFactory), 让该 接口负责创建一系列相关或者相互依赖的对象(Body 、Clothes 、Shoes), 而无须指定它们具 体的类。
到这里,简单工厂模式、工厂方法模式、抽象工厂模式就都讲解完了,下面对这3种工厂 模式做 一个总结:
-
从代码实现复杂度上,简单工厂模式最简单,工厂方法模式次之,抽象工厂模式最复 杂。把简单工厂模式中的代码修改得符合开闭原则,就变成了工厂方法模式,修改 工厂方法模式的代码使一个工厂支持对多个具体产品的生产,就变成了抽象工厂 模 式 。
-
从需要的工厂数量上,简单工厂模式需要的工厂数量最少,工厂方法模式需要的工 厂数量最多,抽象工厂模式能够有效地减少工厂方法模式所需要的工厂数量(可以 将工厂方法模式看作抽象工厂模式的一种特例———抽象工厂模式中的工厂若只创 建一种对象就是工厂方法模式)。
-
从实际应用上,当项目中的产品数量比较少时考虑使用简单工厂模式,如果项目稍大一点或者为了满足开闭原则,则可以使用工厂方法模式,而对于大型项目中有众 多厂商并且每个厂商都生产一系列产品时应考虑使用抽象工厂模式。