【设计模式】建造者模式——工厂模式
三、建造者模式——工厂模式
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种工厂 模式做 一个总结:
-
从代码实现复杂度上,简单工厂模式最简单,工厂方法模式次之,抽象工厂模式最复 杂。把简单工厂模式中的代码修改得符合开闭原则,就变成了工厂方法模式,修改 工厂方法模式的代码使一个工厂支持对多个具体产品的生产,就变成了抽象工厂 模 式 。
-
从需要的工厂数量上,简单工厂模式需要的工厂数量最少,工厂方法模式需要的工 厂数量最多,抽象工厂模式能够有效地减少工厂方法模式所需要的工厂数量(可以 将工厂方法模式看作抽象工厂模式的一种特例———抽象工厂模式中的工厂若只创 建一种对象就是工厂方法模式)。
-
从实际应用上,当项目中的产品数量比较少时考虑使用简单工厂模式,如果项目稍大一点或者为了满足开闭原则,则可以使用工厂方法模式,而对于大型项目中有众 多厂商并且每个厂商都生产一系列产品时应考虑使用抽象工厂模式。
相关文章:
【设计模式】建造者模式——工厂模式
三、建造者模式——工厂模式 3.1 工厂模式 创建一个类对象的传统方式是使用关键字new, 因为用new 创建的类对象是一个堆对象,可以实现多态。工厂模式通过把创建对象的代码包装起来,实现创建对象的代码与具体 的业务逻辑代码相隔离的目的(将对象的创建和…...
Java基础:枚举类enum入门案例
1.基础枚举定义与使用: package com.zxy;public class Main {public static void main(String[] args) { // 获取枚举值cars car cars.BMW;switch (car){case BMW :System.out.println("BMW");break;case BENZ :System.out.println("BENZ&…...
蓝桥备赛(18)- 红黑树和 set 与 map(上)
对于二叉搜索树 , 平衡二叉树 , 以及红黑树 , 目前只需要了解背后的原理 , 不做代码实现的要求 , 重要的就是了解各种操作的时间复杂度即可 , 为set 与 map 做铺垫 一、二叉搜索树 1.1 基本概念 相较与于堆…...
Spring Boot集成EasyExcel
1. 初始化Spring Boot项目 首先,使用Spring Initializr(https://start.spring.io/)生成一个基本的Spring Boot项目。选择以下依赖项: Spring WebLombok (用于减少样板代码)SLF4J (用于日志记录) 2. 添加依赖 在你的pom.xml文件…...
obeaver 连接oracle 库 模式乱码
下载orai18n-12.1.0.2.0.jar 库--添加文件--把提前下载好的jar 随便放在一个文件夹下--添加文件选中,然后点击找到类, 选择类,确定即可正常 下载地址:https://download.csdn.net/download/weixin_42845364/88368302...
ChatGPT 使用教程:深度探索AI常用功能技巧
文章目录 前言一、ChatGPT介绍1.1 人工智能与自然语言处理的发展1.2 ChatGPT 的诞生与意义 二、ChatGPT 基础入门2.1 注册与登录2.2 对话界面介绍2.3 基本提问方式 三、常用功能详解3.1 文本生成3.2 问题回答3.3 语言翻译3.4 代码生成与调试 四、高级使用技巧4.1 指令优化4.2 多…...
無人機的應用程序有那些可以部署在linux server 系統
Dronecode Project:由 Linux Foundation 主導的開源項目,提供無人機航空操作系統和導航工具的開發框架,適合開發者使用。 DeepSeek-R1:這是一個人工智能模型,適用於無人機的數據處理和分析,支持在 Linux 系…...
[HUBUCTF 2022 新生赛]messy_traffic
下载附件 看到文件类型直接用wireshark打开,对MySQL协议进行追踪流,并没有什么发现,后面对NO.437发现有用信息,http追踪流 发现**system(‘cat passwd.txt’);**这里是在打开查看passwd.txt,密码是"SignUpForHUBU…...
铁人三项(第五赛区)_2018_rop题解
先启动靶机连接看看。 直接ls,就给我输出句话,看来不能直接拿flag。 那走下流程。 查下位数和其他信息: 可以看到是32位的包,开了NX,但没开其他保护。 用ida32打开looklook。 主函数就是个这,看到了弹出的…...
package.json 依赖包约束及快速删除node_modules
文章目录 一、package.json版本约束1、初始项目安装2. 已有 yarn.lock 文件的项目安装3. 特殊情况手动修改 package.json 版本:使用 yarn upgrade 命令: 二、快速删除node_modules三、depcheck 检测npm未使用的依赖 一、package.json版本约束 1、初始项…...
Compose 实践与探索六 —— 动画的流程控制与 Transition
1、Block 参数:监听每一帧 animateTo() 与 animateDecay() 中都有一个函数类型的 block 参数: suspend fun animateDecay(initialVelocity: T,animationSpec: DecayAnimationSpec<T>,block: (Animatable<T, V>.() -> Unit)? null): An…...
虚拟机Contos7为啥不能被本机电脑访问?
1.查看防火墙是否开启 systemctl status firewalld.service 2.如果防火墙关闭就可以直接被访问 3.如果防火墙打开了我们需要开放端口(下面为防火墙一系列指令) # 关闭防火墙 systemctl stop firewalld.service# 打开防火墙 systemctl start firewalld.service# 关闭开启自启…...
【21】单片机编程核心技巧:if语句逻辑与真假判断
【21】单片机编程核心技巧:if语句逻辑与真假判断 七律 条件分野 if语句判真假,括号条件定乾坤。 非零为真零为假,大括号内藏玄门。 省略虽简风险在,代码规范护本根。 单片逻辑由心控,条件分支自成文。 注释…...
Java 实现 Android ViewPager2 顶部导航:动态配置与高效加载指南
Java 实现:明确使用的编程语言。Android ViewPager2:技术栈和核心组件。顶部导航:功能点。动态配置与高效加载指南:突出动态配置的灵活性和性能优化的重点。 在 Android 中使用 Java 实现 ViewPager2 和 TabLayout 的顶部导航也是…...
Python :数据模型
一. 什么是数据模型? Python数据模型是Python对象系统的抽象,通过一组特殊方法(如__init__、__len__等)和协议(如迭代协议、上下文管理协议),定义了对象如何与语言的内置功能(如…...
idea超级AI插件,让 AI 为 Java 工程师
引言 用户可在界面中直接通过输入自然语言的形式描述接口的需求,系统通过输入的需求自动分析关键的功能点有哪些,并对不确定方案的需求提供多种选择,以及对需求上下文进行补充,用户修改确定需求后,系统会根据需求设…...
施磊老师c++笔记(五)
继承与多态-深入掌握oop语言最强大的机制 文章目录 继承与多态-深入掌握oop语言最强大的机制1.继承的基本意义2.派生类的构造过程3.重载,隐藏,覆盖4.虚函数, 静态绑定和动态绑定--面试重点5.虚析构函数--重点在于什么呢时候用6.再讨论虚函数和动态绑定7.理解多态到底是什么8.理…...
µCOS-III从入门到精通 第十四章(软件定时器)
参考教程:【正点原子】手把手教你学UCOS-III实时操作系统_哔哩哔哩_bilibili 一、软件定时器简介 1、定时器的概念与种类 (1)定时器的概念:从指定的时刻开始,经过一个指定时间,然后触发一个超时事件&…...
MySQL数据库复杂的增删改查操作
在前面的文章中,我们主要学习了数据库的基础知识以及基本的增删改查的操作。接下去将以一个比较实际的公司数据库为例子,进行讲解一些较为复杂且现时需求的例子。 基础知识: 一文清晰梳理Mysql 数据库基础知识_字段变动如何梳理清楚-CSDN博…...
KCD 北京站丨Volcano 邀您畅聊云原生智能调度技术与应用
AI与云原生技术正以前所未有的速度改变着我们的世界,而云原生技术则如同一座坚实的桥梁,连接着传统IT与现代化的数字世界。当AI与云原生相遇,它们相互赋能,相互促进,为开发者们打开了一个全新的技术宇宙。 3 月 15 日&…...
BLEU评估指标
一、介绍 用于评估模型生成的句子和实际句子差异的指标,取值在[0,1],匹配度高就距离1近,反之距离0近。这个指标计算代价小,容易理解,与语言无关,与人类评价结果高度相关。 BLEU主要基于n-gram匹配&#x…...
高效自动化测试:打造Python+Requests+Pytest+Allure+YAML的接口测试框架
一、背景 在快节奏的开发周期中,如何确保接口质量?自动化测试是关键。通过构建标准化、可复用的测试框架,能显著提升测试效率与准确性,为项目质量保驾护航[1][7]。 二、目标 ✅ 核心目标: ● 实现快速、高效的接口测试…...
如何修复 Tauri 发布后程序运行时显示 `asset not found: index.html` 的问题
如何修复 Tauri 发布后程序运行时显示 asset not found: index.html 的问题 在使用 Tauri 发布应用程序时,如果运行时出现 asset not found: index.html 的错误,通常是因为 Tauri 无法找到或正确加载前端资源文件(如 index.html)…...
BSides Vancouver: 2018 (Workshop)
BSides Vancouver: 2018 (Workshop) 来自 <https://www.vulnhub.com/entry/bsides-vancouver-2018-workshop,231/> 1,将两台虚拟机网络连接都改为NAT模式 2,攻击机上做namp局域网扫描发现靶机 nmap -sn 192.168.23.0/24 那么攻击机IP为192.168.23…...
rStar论文精读
论文简介 论文标题:《Mutual reasoning makes smaller LLMs stronger problem-solvers》 论文地址:https://arxiv.org/abs/2408.06195 录用会议:ICLR2025 背景与挑战 挑战1:在SLM中平衡exploration与exploitation。一些方法有很…...
【动态规划】对局匹配 (分组线性DP)
题目详情 问题描述: 小明喜欢在一个围棋网站上找别人在线对弈。这个网站上所有注册用户都有一个积分,代表他的围棋水平。 小明发现网站的自动对局系统在匹配对手时,只会将积分差恰好是K的两名用户匹配在一起。如果两人分差小于或大于K…...
python 提取视频中的音频
在Python中提取视频中的音频,你可以使用moviepy库,这是一个非常强大且易于使用的库,专门用于视频编辑。以下是如何使用moviepy来提取视频中的音频的步骤: 安装moviepy 首先,你需要安装moviepy。你可以通过pip安装它&a…...
self.cls_token在 Vision Transformer (ViT) 模型中的训练阶段和推理阶段的行为和作用的异同
self.cls_token 在 Vision Transformer (ViT) 模型中,在训练阶段和推理阶段的行为和作用是不同的,而且它的值在训练过程中会发生变化。 1. self.cls_token 的作用 在 ViT 中,self.cls_token 是一个特殊的、可学习的嵌入向量(emb…...
【量化科普】Leverage,杠杆
【量化科普】Leverage,杠杆 🚀量化软件开通 🚀量化实战教程 在量化投资领域,杠杆(Leverage)是一个核心概念,它允许投资者通过借入资金来增加投资规模,从而放大投资收益或亏损。简…...
247g 的工业级电调,如何让无人机飞得更 “聪明“?——STONE 200A-M 深度测评
一、轻量化设计背后的技术取舍 当拿到 STONE 200A-M 时,247g 的重量让人意外 —— 这个接近传统 200A 电调 70% 的重量,源自 1205624.5mm 的紧凑结构(0.1mm 公差控制)。实测装机显示,相比同规格产品,其体积…...
