欢迎光临
我们一直在努力

【设计模式系列 (二) 】万字详解工厂模式

⭐️在这个怀疑的年代,我们依然需要信仰。

个人主页 :YYYing.

⭐️设计模式系列专栏:设计模式系列

系列下期内容:暂无


目录

前言:为什么需要工厂模式

从一行 new 说起

三者关系与演进路线

1. 简单工厂模式

1.1 概述

1.2 简单工厂模式的结构

1.3 简单工厂模式的实现

1.3.1 类图设计

1.3.2 代码实现

1.3.3 运行结果

1.4 简化版:把工厂方法搬进抽象产品

1.5 优缺点与适用环境

1.6 为什么说它违背开闭原则

2. 工厂方法模式

2.1 概述

2.2 结构

2.3 实现

2.4 引擎盖之下:仓库里的自注册反射机制

2.5 优缺点与适用环境

3. 抽象工厂模式

3.1 概述:产品等级结构与产品族

3.2 结构

3.3 实现

3.4 开闭原则的倾斜性

3.5 优缺点与适用环境

4. 三种工厂模式对比

4.1 核心对比表

4.2 一句话记忆法

4.3 选择决策树

4.4 开闭原则在三个模式中的表现

5. 面试专题

5.1 开场题:说说工厂模式吧

5.2 追问:工厂模式 vs 建造者模式

5.3 高频追问清单

5.4 答题模板

6. 工程实践与踩坑

6.1 裸指针与内存泄漏

6.2 线程安全

6.3 工厂膨胀与过度设计

附录 A:三模式速查表

结语

—⭐️封面自取⭐️—



前言:为什么需要工厂模式

从一行 new 说起

假设你在写一个游戏,道具商店里卖各种硬币:

// 最朴素的写法
if (type == 1)  coin = new CopperCoin();
else if (type == 2) coin = new GoldCoin();
else throw std::string("没有你要的硬币");

这段代码能跑,但问题也很明显:

问题说明
创建逻辑散落各处 商店要 new、背包要 new、掉落系统也要 new——一旦加个"银币",三处都得改
客户端被迫"知道得太多" 调用方必须记住 CopperCoin/GoldCoin 这些具体类名,以及 1/2 这些魔法数字的含义
创建与使用耦合 对象的"怎么造"和"怎么用"混在一起,改造成本高

工厂模式解决的就是这件事:把"对象的创建"从"对象的使用"里拆出来。

💬 人话版 工厂模式,说白了就是你不自己动手造东西,而是去点单。

  • 你自己 new → 相当于自己在家和面、擀皮、剁馅、包饺子;

  • 工厂模式 → 相当于去饺子馆点单,你只说"来份猪肉白菜的",至于它怎么和面、用哪家的面粉,跟你没关系。

好处是:馅料配方变了(加个新品种),你点单的话术不用变,你家里的厨房也不用改造。

更准确地说,工厂模式的核心价值是解耦"创建"与"使用",让变化被挡在工厂内部。


三者关系与演进路线

简单工厂 工厂方法 抽象工厂
┌────────┐ ┌────────┐ ┌────────┐
│ 一个 │ 职责太重 │ 工厂 │ 产品变"一整套" │ 一个 │
│ 工厂类 │ ────拆分────▶ │ 等级 │ ──────演进──────▶ │ 工厂造 │
│ 造所有 │ 不合开闭 │ 结构 │ 产品族概念 │ 一个族 │
└────────┘ └────────┘ └────────┘
│ │ │
switch 分支 多态分发 多态分发 + 多产品线

演进的内在逻辑(这是理解本章的钥匙):

  • 简单工厂:工厂类把创建逻辑集中了,但加产品要改工厂(违背开闭原则);

  • 工厂方法:把"改工厂"变成"加工厂",加产品只需新增类(符合开闭原则),代价是类的数量成对增长;

  • 抽象工厂:当产品不止一条产品线、而是成组出现时(如"精灵王国需要国王+城堡+军队"),一个工厂统一负责一整族产品,保证配套关系不被破坏;代价是加产品等级结构很麻烦。

  • 💬 人话版

    • 简单工厂:一个万能师傅,什么都会做,但你要新款他就得重学;

    • 工厂方法:每个师傅只做一手活儿,来新款就再招一个师傅,老师傅完全不受影响;

    • 抽象工厂:从"招一个师傅"升级成"开一家分公司"——分公司里一整套人马都是配套的(精灵的国王配精灵的城堡配精灵的军队),不会出现"兽人国王住在精灵城堡里"的错配。


    1. 简单工厂模式

    1.1 概述

    简单工厂模式(Simple Factory Pattern) 又叫做静态工厂方法模式(Static Factory Method Pattern),并不属于 GoF 的 23 种设计模式之一,是学习其他工厂模式的基础。

    官方定义:

    Wikipedia says: Factory is an object for creating other objects – formally a factory is a function or method that returns objects of a varying prototype or class.

    工厂是一个用于创建其他对象的对象——从形式上讲,工厂是一个函数或方法,它返回不同原型或类型的对象。

    Providing a static method encapsulated in a class called the factory, to hide the implementation logic and make client code focus on usage rather than initializing new objects.

    提供封装在名为"工厂"的类中的静态方法,以隐藏实现逻辑,并使客户端代码专注于使用而不是初始化新对象。

    核心特征有两个,缺一不可:

  • 一个方法(通常是静态方法)负责创建对象;

  • 一个"参数" 决定创建哪个具体类型 —— 这个参数就是"点单"用的菜名。

  • 💬 人话版 定义里有两个关键词值得划出来:

    • "static method"(静态方法):不需要先 new 一个工厂对象,直接 CoinFactory::getCoin(1) 就能用。

    • "hide the implementation logic"(隐藏实现逻辑):客户端看到的是"给我个硬币",看不到 new CopperCoin() 这行代码,也就不会被具体类的改动波及。

    一句话:简单工厂 = 一个带 switch 的静态方法。 它的"工厂味"就这么多,但恰恰是后面两个模式的起点。

    1.2 简单工厂模式的结构

    在简单工厂模式结构图中包含如下几个角色:

    角色英文职责
    工厂角色 Factory 即工厂类,是简单工厂模式的核心,负责实现创建所有产品实例的内部逻辑;工厂类可以被外界直接调用,创建所需的产品对象;在工厂类中提供了静态的工厂方法 factoryMethod(),它的返回类型为抽象产品类型 Product
    抽象产品角色 Product 它是工厂类所创建的所有对象的父类,封装了各种产品对象的公有方法;它的引入将提高系统的灵活性,使得在工厂类中只需定义一个通用的工厂方法,因为所有创建的具体产品对象都是其子类对象
    具体产品角色 ConcreteProduct 它是简单工厂模式的创建目标,所有被创建的对象都充当这个角色的某个具体类的实例;每一个具体产品角色都继承了抽象产品角色,需要实现在抽象产品中声明的抽象方法

    结构图:

    三个关键关系:

    • 工厂 → 产品:依赖(工厂"创建"产品);

    • 具体产品 → 抽象产品:泛化/继承(is-a);

    • 客户端 → 工厂:依赖(客户端只认识工厂,不认识具体产品)。

    💬 人话版 注意看:客户端和 CopperCoin/GoldCoin 之间没有连线。 这正是简单工厂的价值所在——把原本"客户端 → 一堆具体产品"的多对多关系,收缩成"客户端 → 工厂 → 一堆具体产品"的单点接触。 以后产品的名字变了、构造函数参数变了,改的都是工厂那一处,客户端不受影响。

    1.3 简单工厂模式的实现

    下面我们用一个"硬币工厂"的示例来演示简单工厂模式的实现。

    1.3.1 类图设计
    • Coin → 抽象产品;

    • CopperCoin(铜币)、GoldCoin(金币)→ 具体产品;

    • CoinFactory → 工厂角色。

    1.3.2 代码实现

    本文中的配置和各种宏可以去拉我的仓库看看:

    https://github.com/YYYingk/DesignPattern/tree/main

    ① 抽象产品

    #pragma once
    #include <string>

    #ifndef _COIN_H_
    #define _COIN_H_
    using namespace std;
    namespace sfp
    {
    /**
    * 抽象硬币接口
    */
    class Coin
    {
    public:
    virtual std::string getDescription() = 0;
    //static Coin* getCoin(int type);
    };
    }

    #endif // !_COIN_H_

    ② 具体产品1

    // CopperCoin.h
    #pragma once
    #include "Coin.h"

    #ifndef _COPPERCOIN_H_
    #define _COPPERCOIN_H_

    namespace sfp
    {
    class CopperCoin : public Coin
    {
    public:
    std::string getDescription() override;
    };
    }

    #endif // !_COPPERCOIN_H_
    // CopperCoin.cpp
    #include "CopperCoin.h"

    std::string sfp::CopperCoin::getDescription()
    {
    return "你获得了一枚铜币";
    }

    ③ 具体产品2:

    // GoldCoin.h
    #pragma once

    #ifndef _GOLDCOIN_H_
    #define _GOLDCOIN_H_
    #include "Coin.h"

    namespace sfp
    {
    class GoldCoin : public Coin
    {
    public:
    std::string getDescription() override;
    };
    }

    #endif // !_GOLDCOIN_H_
    // GoldCoin.cpp
    #include "GoldCoin.h"

    std::string sfp::GoldCoin::getDescription()
    {
    return "你获得了一枚金币";
    }

    ④ 工厂角色

    // CoinFactory.h
    #pragma once
    #ifndef _COINFACTORY_H_
    #define _COINFACTORY_H_
    #include "Coin.h"

    namespace sfp
    {
    class CoinFactory
    {
    public:
    static Coin* getCoin(int type); // ← 静态工厂方法:不需要实例化工厂
    };
    }

    #endif // !_COINFACTORY_H_
    // CoinFactory.cpp
    #include "CoinFactory.h"
    #include "CopperCoin.h"
    #include "GoldCoin.h"

    sfp::Coin* sfp::CoinFactory::getCoin(int type)
    {
    switch (type)
    {
    case 1:
    return new CopperCoin();
    break;
    case 2:
    return new GoldCoin();
    break;
    default:
    throw std::string("没有你要的硬币");
    break;
    }
    }

    ⑤ 客户端

    #include "CoinFactory.h"
    #include <iostream>
    #include "../../util/Properties.h"
    //#include "Coin.h"
    using namespace sfp;

    int main()
    {
    try
    {
    // 使用配置的方式动态调整硬币的创建
    CREATE_PROPERTIES(cp, conf);
    int type = std::stoi(cp.getProperty("sfp"));
    // 通过硬币工厂创建硬币
    Coin* coin = CoinFactory::getCoin(type);
    //Coin* coin = Coin::getCoin(type);
    cout << coin->getDescription() << endl;
    delete coin;
    }
    catch (const std::string& msg)
    {
    cout << msg.c_str();
    }
    return 0;
    }

    我们在配置中让 sfp=1 就是简单工厂最大的"灵活性"来源。 把 1 改成 2,程序就从发铜币变成发金币,不用重新编译。

    💬 人话版:配置化的意义 讲义把"引入配置文件"列为简单工厂的主要优点之一,原文是: "通过引入配置文件,可以在不修改任何客户端代码的情况下更换和增加新的具体产品类,在一定程度上提高了系统的灵活性。"

    翻译成人话:你把 new 谁的选择权,从"写死在代码里"变成了"写在配置里"。 注意原文的措辞——"在一定程度上"。因为配置只能切换已经存在的产品;要"增加"一个新的具体产品类,仍然必须回头去改工厂的 switch。这个"一定程度"的边界,就是下一节要讲的它的死穴。

    1.3.3 运行结果

    把 conf.properties 中的 sfp 设为 1,程序输出:

    你获得了一枚铜币

    设为 2:

    你获得了一枚金币

    设为其他值(如 3):

    没有你要的硬币

    1.4 简化版:把工厂方法搬进抽象产品

    当产品类层次很小、工厂逻辑几乎不会复用时,可以省掉 CoinFactory 这个类,把工厂方法直接搬到抽象产品里。

    // Coin.h —— 取消注释
    class Coin
    {
    public:
       virtual std::string getDescription() = 0;
       static Coin* getCoin(int type);
    };

    // Coin.cpp
    sfp::Coin* sfp::Coin::getCoin(int type)
    {
       switch (type)
      {
       case 1:  return new CopperCoin();
       case 2:  return new GoldCoin();
       default: throw std::string("没有你要的硬币");
      }
    }

    // Client.cpp —— 调用方式随之改变
    // Coin* coin = CoinFactory::getCoin(type);
    Coin* coin = Coin::getCoin(type);

    两种写法的区别:

    独立工厂类 CoinFactory简化版 Coin::getCoin
    类数量 +1 不变
    职责划分 ✅ 创建职责独立,符合 SRP ❌ 抽象产品被迫承担"创建自己"的职责
    可替换性 ✅ 换工厂即可 ❌ 客户端直接绑定 Coin

    简单说:简化版是牺牲单一职责原则换取类的精简。 项目小、产品就两三种时完全够用;一旦产品增多、或创建逻辑变复杂(要读配置、要缓存、要做池化),就应该把工厂独立出来。

    1.5 优缺点与适用环境

    优点

  • 实现了对象创建和使用的分离。 工厂类包含必要的判断逻辑,决定何时创建哪个产品类的实例,客户端只"消费"产品。

  • 减少使用者的记忆量。 客户端无须知道具体产品类的类名,只需知道对应的参数。

  • 对比一下第 2 点的实际收益:

    // 没有工厂:要知道类的全名,还要知道构造函数怎么调
    Coin* c = new sfp::CopperCoin();

    // 有工厂:只需要知道"1 号是铜币"
    Coin* c = CoinFactory::getCoin(1);

    类名改了、挪了命名空间?改工厂一处就够了,调用方一行都不用动。

  • 配置化带来的灵活性。 引入配置文件后,可以在不修改客户端代码的情况下更换具体产品类。

  • 注意措辞——"在一定程度上"。配置只能切换已经存在的产品;要"增加"一个新的具体产品类,仍然必须回头改工厂的 switch。这个边界就是它的死穴。

    缺点

  • 工厂类职责过重。 所有产品的创建逻辑都集中在工厂里,一旦它不能正常工作,整个系统都受影响。

  • 增加了类的个数。 引入了新的工厂类,增加系统复杂度。

  • 系统扩展困难(违背开闭原则)。 添加新产品不得不修改工厂逻辑,产品类型较多时工厂逻辑会过于复杂。

  • 无法形成继承等级结构。 由于使用静态工厂方法,工厂角色无法形成基于继承的等级结构。

  • 第 4 点最容易被忽略,但它是通向工厂方法模式的唯一理由:

    static 方法不能被重写(override),也没有多态。这意味着你想给工厂加个"带缓存的版本"做不到,想在测试里把工厂替换成返回假对象的 Mock 工厂也做不到。 静态 = 放弃多态 = 放弃扩展。 工厂方法模式做的第一件事,恰恰就是把 static 换成 virtual。

    适用环境

  • 工厂类负责创建的对象比较少,不会导致工厂方法里的业务逻辑过于复杂;

  • 客户端只知道传入工厂类的参数,对如何创建对象并不关心。

  • 一句话判断标准:产品种类少、且基本不会再增加 → 用简单工厂。 一旦你发现"这个 switch 我又改了第三次",就该升级到工厂方法模式了。

    1.6 为什么说它违背开闭原则

    空口说"违背开闭原则"没有体感,直接走一遍:现在要加一款"银币(SilverCoin)"。

    步骤文件改动
    1 SilverCoin.h / .cpp 新增(这是符合开闭原则的部分)
    2 CoinFactory.cpp ❌ 修改:#include "SilverCoin.h",switch 里加 case 3:
    3 conf.properties 修改配置值
    4 客户端 ✅ 不用改

    sfp::Coin* sfp::CoinFactory::getCoin(int type)
    {
       switch (type)
      {
       case 1:  return new CopperCoin();
       case 2:  return new GoldCoin();
       case 3:  return new SilverCoin();   // ← 新加的
       default: throw std::string("没有你要的硬币");
      }
    }

    开闭原则的要求是"对扩展开放,对修改关闭"。上面第 2 步修改了已存在、且已经测试通过的代码——这就是"违背"。

    更糟的是风险性质变了:加 SilverCoin 本该是纯新增,风险可控;但因为要动 CoinFactory.cpp,你就得重新测试整个工厂——已经跑通的铜币、金币路径也被拖进了"重新验证"范围。

    你只改了 1 行,但要重新验证的可能是 100 行。工厂方法模式的全部动机,就是把这第 2 步从"修改"变成"新增"。


    2. 工厂方法模式

    2.1 概述

    工厂方法模式(Factory Method Pattern)继承了简单工厂模式的优点,同时做出修改以达到符合开闭原则的要求。它也被称为虚拟构造器模式或多态工厂模式。

    官方定义:

    Define an interface for creating an object, but let subclasses decide which class to instantiate. Factory Method lets a class defer instantiation to subclasses.

    定义一个用于创建对象的接口,让子类决定将哪一个类实例化。工厂方法模式让一个类的实例化延迟到其子类。

    不再提供一个统一的工厂类来创建所有产品,而是针对不同的产品提供不同的工厂——系统提供一个与产品等级结构对应的工厂等级结构。

    三句话拆解:

    定义原文落到代码上
    Define an interface for creating an object 抽象工厂里声明 virtual Weapon* makeWeapon() = 0;
    but let subclasses decide which class to instantiate OrcBlacksmith::makeWeapon() 里 new OrcWeapon()
    lets a class defer instantiation to subclasses 编译期父类不知道造什么,运行期由多态决定

    核心就一件事:把 switch 换成"多态"。

    简单工厂:客户 ──▶ CoinFactory ──switch──▶ 具体产品
    工厂方法:客户 ──▶ 抽象工厂 ──多态──▶ 具体工厂 ──▶ 具体产品

    于是"加产品"从"改 switch"变成了"加一个具体工厂类"。

    名字里的 "延迟(defer)" 是关键:父类 Blacksmith 只知道"我能打造武器",它不知道也不关心打造出来的是狼牙棒还是穿云箭。这个决定发生在运行期,由具体工厂完成。

    2.2 结构

    角色英文职责
    抽象产品 Product 产品对象的公共父类
    具体产品 ConcreteProduct 实现抽象产品接口,与具体工厂一一对应
    抽象工厂 Factory 声明工厂方法,返回一个产品。是工厂方法模式的核心
    具体工厂 ConcreteFactory 抽象工厂的子类,实现工厂方法,返回具体产品实例

    注意两条平行的等级结构:产品等级结构(Weapon → OrcWeapon / ElfWeapon)与工厂等级结构(Blacksmith → OrcBlacksmith / ElfBlacksmith)。两者一一对应,这就是"具体工厂和具体产品一一对应"的含义。

    记住这个"双等级结构"的图景,代价也就一目了然:加一个产品,要成对加一个工厂。这是一笔交易——用"类的数量变多"换"修改老代码的次数变少"。

    2.3 实现

    ① 抽象产品与抽象工厂

    // Weapon.h
    namespace fmp
    {
       class Weapon
      {
       public:
           virtual void showWeapon() = 0;
      };
    }

    // Blacksmith.h
    namespace fmp
    {
       class Blacksmith
      {
       public:
           virtual Weapon* makeWeapon() = 0;   // ← 工厂方法,模式的名字就来自它
      };
    }

    virtual Weapon* makeWeapon() = 0; 这一行是这个模式的灵魂,逐字体会:

    • virtual … = 0:纯虚,父类不实现 → 所以父类"不知道"造什么;

    • 返回 Weapon*:返回抽象产品,不暴露具体类型 → 所以调用方"不知道"造了什么;

    • makeWeapon:方法名要体现"创建"语义,面试手写时这点很关键。

    Blacksmith 类没有任何成员变量、没有构造函数、只有一个纯虚函数——它是一个纯粹的"接口"。这正是"依赖倒置"里说的依赖抽象。

    ② 具体产品

    // OrcWeapon.cpp —— 兽人武器
    void fmp::OrcWeapon::showWeapon()
    {
       cout << "看我的狼牙棒!!!!!!" << endl;
    }

    // ElfWeapon.cpp —— 精灵武器
    void fmp::ElfWeapon::showWeapon()
    {
       cout << "一支穿云箭,千军万马来相见" << endl;
    }

    ③ 具体工厂

    // OrcBlacksmith.h
    class OrcBlacksmith : public Blacksmith
    {
       DECLARE_CLASS(fmp::OrcBlacksmith);   // ← 自注册机制,见 2.4
    public:
       Weapon* makeWeapon() override;
    };

    // OrcBlacksmith.cpp
    IMPLEMENT_CLASS(fmp::OrcBlacksmith);

    fmp::Weapon* fmp::OrcBlacksmith::makeWeapon()
    {
       return new OrcWeapon();      // 兽人铁匠只造兽人武器
    }

    // ElfBlacksmith.cpp
    IMPLEMENT_CLASS(fmp::ElfBlacksmith);

    fmp::Weapon* fmp::ElfBlacksmith::makeWeapon()
    {
       return new ElfWeapon();      // 精灵铁匠只造精灵武器
    }

    请注意 makeWeapon() 的实现有多"简单"——就是一个 new。这正是工厂方法模式最反直觉的地方:它看起来好像什么也没干,只是把 new 挪了个地方。

    但它挪得有讲究:挪到了"子类"里。

    • 兽人铁匠的 new OrcWeapon() → 编译期就"焊死"在 OrcBlacksmith.cpp 里;

    • 精灵铁匠的 new ElfWeapon() → 编译期就"焊死"在 ElfBlacksmith.cpp 里;

    • 而客户端拿到的是 Blacksmith*,它连自己用的是哪个铁匠都不知道。

    于是"造什么"这个决策被彻底封装进具体工厂——客户端和具体产品之间,隔着一层多态。

    ④ 客户端

    int main()
    {
       CREATE_PROPERTIES(cp, conf);                                  // fmp=fmp::OrcBlacksmith
       GET_INSTANCE_BY_NAME(Blacksmith*, smith, cp.getProperty("fmp"));
       //Blacksmith* smith = new ElfBlacksmith();
       Weapon* weapon = smith->makeWeapon();
       weapon->showWeapon();
       delete weapon;
       delete smith;
       return 0;
    }

    客户端只做了四件事:读配置 → 拿到工厂 → 要产品 → 用产品。全程没有出现 OrcBlacksmith、OrcWeapon 任何一个具体类名。

    运行结果:配置 fmp=fmp::OrcBlacksmith 输出"看我的狼牙棒!!!!!!";改成 fmp=fmp::ElfBlacksmith 后不用重新编译,输出变为"一支穿云箭,千军万马来相见"。

    2.4 引擎盖之下:仓库里的自注册反射机制

    上面客户端里根本没有 new OrcBlacksmith(),它只写了一句 cp.getProperty("fmp") 读取字符串 "fmp::OrcBlacksmith",然后就拿到了一个能用的对象。这是怎么做到的?

    C++ 原生没有反射(不像 Java 的 Class.forName())。仓库用"静态对象自注册"这个经典技巧模拟了反射,核心代码在:

    typedef void* (*createFun)(void);   // 类创建函数指针

    class ClassFactory
    {
    private:
       ClassFactory();
       ~ClassFactory();
       map<string, createFun> prt_createFunMap;    // 类名 → 创建函数
       stack<void*> prt_dyinstaces;                // 动态实例(用于收尾清理)
    public:
       void* getClassInstanceByName(string name);
       void  registClass(string name, createFun method, void* dyinstance);
       static ClassFactory& getInstance();
    };

    // 动态类:构造即注册
    class DynamicClass
    {
    public:
       DynamicClass(string name, createFun method);
    };

    // 声明动态类以及定义类创建方法,写在 h 中
    #define DECLARE_CLASS(className) \\
    private: \\
    static DynamicClass* prt_dcInstance; \\
    public: \\
    static void* createFun() {return new className();}

    // 定义动态类实例,写在 cpp 中
    #define IMPLEMENT_CLASS(className) \\
    DynamicClass* className::prt_dcInstance = new DynamicClass(#className, &className::createFun)

    // 通过类名称创建类实例
    #define GET_INSTANCE_BY_NAME(typeName, varName, className) \\
    typeName varName = (typeName)ClassFactory::getInstance().getClassInstanceByName(className)

    实现:

    void* ClassFactory::getClassInstanceByName(string name)
    {
       auto iter = prt_createFunMap.find(name);
       if (iter != prt_createFunMap.end())
           return iter->second();       // 找到就调用创建函数
       return NULL;
    }

    void ClassFactory::registClass(string name, createFun method, void* dyinstance)
    {
       prt_createFunMap.insert(make_pair(name, method));
       prt_dyinstaces.push(dyinstance);
    }

    ClassFactory& ClassFactory::getInstance()
    {
       static ClassFactory instance;    // C++11 起,局部静态变量初始化线程安全
       return instance;
    }

    // DynamicClass 的构造函数就是"注册"这个动作本身
    DynamicClass::DynamicClass(string name, createFun method)
    {
       ClassFactory::getInstance().registClass(name, method, this);
    }

    运行链条(以 OrcBlacksmith 为例):

    ① 程序启动,静态初始化阶段
      IMPLEMENT_CLASS(fmp::OrcBlacksmith)
      └─▶ new DynamicClass("fmp::OrcBlacksmith", &OrcBlacksmith::createFun)
          └─▶ ClassFactory::getInstance().registClass(…)
                └─▶ prt_createFunMap["fmp::OrcBlacksmith"] = &OrcBlacksmith::createFun

    ② 客户端运行
      GET_INSTANCE_BY_NAME(Blacksmith*, smith, "fmp::OrcBlacksmith")
      └─▶ ClassFactory::getInstance().getClassInstanceByName("fmp::OrcBlacksmith")
          └─▶ 查表命中 → 调用 OrcBlacksmith::createFun()
                └─▶ return new fmp::OrcBlacksmith();     ★ 就是这一行在 new
                    └─▶ 强转为 Blacksmith*

    ③ 程序退出
      ClassFactory::~ClassFactory() 弹出 prt_dyinstaces 逐个 delete

    关键点:

    机制作用
    static DynamicClass* prt_dcInstance 静态成员,在 main 之前完成初始化 → 所以注册是自动的
    #className(字符串化) 把类名编译成字符串字面量,作为注册表的 key
    createFun() 返回 void* 用 void* 擦除类型,再由 GET_INSTANCE_BY_NAME 强转回基类指针
    ClassFactory::getInstance() 局部静态 单例模式,保证"注册"和"查找"看到同一张表

    精髓是:"注册"这件事不需要你手动调用,它发生在程序启动时。 IMPLEMENT_CLASS(…) 展开后是一个静态成员变量的定义,C++ 保证静态成员在 main() 之前完成初始化,于是 DynamicClass 的构造函数被自动执行,顺手把自己登记进了全局注册表。

    两个必须知道的工程风险:

  • 静态初始化顺序问题:不同编译单元里静态对象谁先初始化,跨单元是不确定的(C++ 只保证同一编译单元内按定义顺序)。这里的例子能工作,是因为所有注册都发生在 main 之前、且 getInstance() 用了局部静态规避了经典陷阱。项目变大后这是脆弱点。

  • void* 强转没有类型检查:如果把配置写成 fmp=afp::ElfKingdomFactory(写错命名空间),编译期不会报错,运行期会得到错的对象或直接崩溃。配置项没有编译期保护,这是用反射换灵活性的代价。

  • 与开闭原则的关系:加上它之后,加新产品真正做到"零修改老代码"——新增 SilverBlacksmith.h/.cpp 并 IMPLEMENT_CLASS 即可,客户端和配置之外不需要动任何已有文件。

    2.5 优缺点与适用环境

    优点

  • 隐藏实例化细节。 工厂方法向客户隐藏了"哪种具体产品类将被实例化"这一细节,客户端甚至无须知道具体产品类的类名。

  • 多态性是关键。 工厂角色和产品角色的多态性设计是这个模式的关键,它让工厂可以自主确定创建何种产品对象。之所以又叫多态工厂模式,正是因为所有的具体工厂类都具有同一抽象父类。

  • 完全符合开闭原则。 加入新产品时无须修改抽象工厂、抽象产品、客户端和其他具体工厂/产品,只要添加一个具体工厂和具体产品就可以了。

  • 第 2 点值得强调:因为 OrcBlacksmith 和 ElfBlacksmith 都继承自 Blacksmith,客户端才能用同一个类型 Blacksmith* 去持有它们,才能写出与具体产品完全无关的代码。没有共同的抽象父类,多态无从谈起。

    缺点

  • 类的个数成对增加。 添加新产品需要同时编写具体产品类和与之对应的具体工厂类,增加了系统复杂度,也带来额外的编译和运行开销。

  • 引入抽象层增加了理解难度。 客户端代码均使用抽象层定义,增加了抽象性和理解难度;实现时可能还需要用到配置文件、反射等技术。

  • 这两个代价不要轻描淡写。现实取舍是:

    • 产品种类固定且很少(三两个)时,工厂方法带来的那堆工厂类纯属过度设计,用简单工厂甚至直接 new 更合适;

    • 产品会持续扩张(插件系统、多数据库适配、多协议解析)时,这个代价花得非常值。

    判断依据始终是那一条:"什么会变?" 会变的地方才值得留扩展点。

    适用环境

  • 客户端不知道它所需要的对象的类。 只需知道对应的工厂即可,可将具体工厂类的类名存储在配置文件或数据库中。

  • 抽象工厂类通过其子类来指定创建哪个对象。 利用多态性和里氏替换原则,运行时子类对象覆盖父类对象,使系统更容易扩展。

  • 第 1 点在本仓库里体现得淋漓尽致——fmp=fmp::OrcBlacksmith 就写在 conf.properties 里。

    第 2 点是里氏替换原则(LSP)的直接应用:客户端持有 Blacksmith*,换成任何合法的子类工厂,程序行为都应当正常运行。换句话说,工厂方法模式"能不能用",取决于你的工厂子类"是否符合 LSP"。 如果某个具体工厂违反了父类约定(比如 ElfBlacksmith::makeWeapon() 返回了 nullptr),整个模式就不成立了。设计模式不是孤立的知识点,它们背后站着的都是设计原则。


    3. 抽象工厂模式

    3.1 概述:产品等级结构与产品族

    工厂方法模式中的每个工厂只生产一类产品,可能导致系统中存在大量工厂类。此时可以把一些相关的产品组成一个"产品族",由同一个工厂统一生产——这就是抽象工厂模式的基本思想。

    先弄清两个核心概念:

    概念一:产品等级结构 —— 即产品的继承结构。抽象类是手机,子类有三星、小米、华为,则抽象手机与具体品牌手机之间构成一个产品等级结构。

          (一个产品等级结构)
          ┌──────────────┐
          │   手机       │
          └───────┬──────┘
          ┌───────┼───────┐
      ┌────▼───┐ ┌─▼────┐ ┌▼─────┐
      │三星手机│ │小米手机│ │华为手机│
      └────────┘ └──────┘ └──────┘

    概念二:产品族 —— 由同一个工厂生产的、位于不同产品等级结构中的一组产品。小米手机、小米笔记本、小米路由分别位于三个产品等级结构中,这一系列产品构成一个产品族。

      产品族(小米)               产品族(华为)
      ┌─────────────┐             ┌─────────────┐
      │ 小米手机   │ ──┐   ┌── │ 华为手机   │
      │ 小米笔记本 │   │   │   │ 华为笔记本 │
      │ 小米路由   │ ──┘   └── │ 华为路由   │
      └─────────────┘             └─────────────┘
          ▲                             ▲
          └──── 跨产品等级结构 ───────────┘

    概念方向例子一句话
    产品等级结构 纵向(继承) 手机 → 三星/小米/华为手机 同一类东西的不同品牌/实现
    产品族 横向(跨等级结构) 小米手机 + 小米笔记本 + 小米路由 同一品牌的不同种类产品

    钥匙是看它们是"一个方向"还是"跨方向":

    • 产品等级结构 = 竖着看:都是"手机",只是品牌不同。它们是继承关系,共用同一个抽象父类。

    • 产品族 = 横着切一刀:都是"小米",但一个是手机、一个是笔记本、一个是路由器。它们之间没有继承关系,只是"同一工厂产出的、必须配套使用"。

    一个形象的类比:产品族就是"套装"。 买西装不能只要上衣不要裤子——"上衣 + 西裤 + 领带"是一套,由同一家店提供。换一家店,拿到的就是另一套风格一致的行头。这也解释了抽象工厂存在的根本理由:防止"搭配错误"。

    定义:

    当系统所提供的工厂生产的具体产品并不是一个简单的对象,而是多个位于不同产品等级结构、属于不同类型的具体产品时,就可以使用抽象工厂模式。

    Provide an interface for creating families of related or dependent objects without specifying their concrete classes.

    提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。

    定义里有两个词组值得划出来:

    • "families of related or dependent objects"(一系列相关或相互依赖的对象)→ 这就是产品族,强调对象之间有关系;

    • "without specifying their concrete classes" → 客户端只管要"一整套",不指名道姓。

    抽象工厂解决的从来不是"造一个对象",而是"造一组协调一致的对象"。 如果你的产品之间没有配套关系,那就不需要抽象工厂。

    考点:抽象工厂模式是所有形式的工厂模式中最为抽象和最具一般性的一种形式。当一个工厂等级结构可以创建出分属于不同产品等级结构的一个产品族中的所有对象时,抽象工厂模式比工厂方法模式更为简单、更有效率。

    3.2 结构

    角色英文职责
    抽象工厂 AbstractFactory 声明一组用于创建一组产品的方法,每一个方法对应一种产品
    具体工厂 ConcreteFactory 实现创建产品的方法,生成一组具体产品,构成一个产品族
    抽象产品 AbstractProduct 为每种产品声明接口
    具体产品 ConcreteProduct 定义具体工厂生产的具体产品对象

    这张图是本章最重要的一张。和工厂方法模式相比,最大的变化是:抽象工厂里的方法从"一个"变成了"一组"。

    把它当成一个二维表格来读:

    CastleKingArmy
    精灵族 ElfCastle ElfKing ElfArmy
    兽人族 OrcCastle OrcKing OrcArmy
    • 每一列(竖向)→ 一个产品等级结构(都是 Castle,品牌不同);

    • 每一行(横向)→ 一个产品族(都是精灵,种类不同);

    • 每一个具体工厂,负责造满一行。

    所以 ElfKingdomFactory 的三行代码必然都是 Elf 开头,OrcKingdomFactory 的三行必然都是 Orc 开头——这种"整行一致"的保证,就是抽象工厂模式的全部价值所在。

    3.3 实现

    要创建一个王国,我们需要具有共同组织的对象:精灵王国需要精灵国王、精灵城堡和精灵军队,兽人王国需要兽人国王、兽人城堡和兽人军队。王国中的对象之间存在依赖关系。

    注意最后那句"王国中的对象之间存在依赖关系"——这是抽象工厂的适用信号。 如果只是"想要一个国王",用工厂方法就够了。但问题是:精灵国王必须配精灵城堡、精灵军队。 如果允许自由组合,就会出现"兽人国王住在精灵城堡里、指挥着精灵军队"这种逻辑错误——编译器不管你,但业务上是错的。 抽象工厂把"三个对象必须来自同一族"这条约束,从"靠程序员自觉"变成了"由类型系统保证":一旦选了 ElfKingdomFactory,三个 createXxx() 吐出来的一定都是精灵的,想搭配错都搭配不了。

    ① 三个抽象产品

    // Castle.h
    namespace afp
    {
       class Castle
      {
       public:
           virtual string getDesc() = 0;
      };
    }

    // King.h
    namespace afp
    {
       class King
      {
       public:
           virtual std::string getDesc() = 0;
      };
    }

    // Army.h
    namespace afp
    {
       class Army
      {
       public:
           virtual std::string getDesc() = 0;
      };
    }

    ② 抽象工厂

    // KingdomFactory.h
    namespace afp
    {
       class KingdomFactory
      {
       public:
           virtual Castle* createCastle() = 0;
           virtual King*   creatKing()    = 0;    // ⚠️ 仓库里是 creatKing(拼写笔误)
           virtual Army*   createArmy()   = 0;
      };
    }

    ③ 具体产品(以精灵族为例)

    // ElfKindom.h
    namespace afp
    {
       class ElfCastle : public Castle { public: string getDesc() override; };
       class ElfKing   : public King   { public: std::string getDesc() override; };
       class ElfArmy   : public Army   { public: std::string getDesc() override; };
    }

    // ElfKindom.cpp
    string afp::ElfCastle::getDesc()     { return std::string("精灵城堡"); }
    std::string afp::ElfKing::getDesc()   { return std::string("精灵国王"); }
    std::string afp::ElfArmy::getDesc()   { return std::string("精灵军队"); }

    兽人族 OrcKingdom.h/.cpp 完全对称,只是把前缀换成 Orc、返回值换成"兽人城堡/兽人国王/兽人军队"。

    ④ 具体工厂

    // ElfKingdomFactory.h
    class ElfKingdomFactory : public KingdomFactory
    {
       DECLARE_CLASS(afp::ElfKingdomFactory);
    public:
       Castle* createCastle() override;
       King*   creatKing()    override;
       Army*   createArmy()   override;
    };

    // ElfKingdomFactory.cpp
    IMPLEMENT_CLASS(afp::ElfKingdomFactory);

    afp::Castle* afp::ElfKingdomFactory::createCastle() { return new ElfCastle(); }
    afp::King*   afp::ElfKingdomFactory::creatKing()   { return new ElfKing();   }
    afp::Army*   afp::ElfKingdomFactory::createArmy()   { return new ElfArmy();   }

    OrcKingdomFactory.cpp 与之结构完全一致:

    // Elf 版                         // Orc 版
    return new ElfCastle();            return new OrcCastle();
    return new ElfKing();              return new OrcKing();
    return new ElfArmy();              return new OrcArmy();

    三个方法,结构完全一样,只有前缀不同。 这就是"产品族"在代码上的样子——一份代码,整族替换。

    ⑤ 客户端

    int main()
    {
       CREATE_PROPERTIES(cp, conf);                                          // afp=afp::ElfKingdomFactory
       GET_INSTANCE_BY_NAME(afp::KingdomFactory*, factory, cp.getProperty("afp"));

       King*   king   = factory->creatKing();
       Castle* castle = factory->createCastle();
       Army*   army   = factory->createArmy();

       std::cout << "2020年,推选了一位" << king->getDesc()   << std::endl;
       std::cout << "2021年,组件了一支" << army->getDesc()   << std::endl;
       std::cout << "2022年,建造了一座" << castle->getDesc() << std::endl;

       delete king; delete castle; delete army; delete factory;
       return 0;
    }

    运行结果:

    2020年,推选了一位精灵国王
    2021年,组件了一支精灵军队
    2022年,建造了一座精灵城堡

    把配置改成 afp=afp::OrcKingdomFactory,不用重新编译,输出整体切换为兽人版本。

    客户端从头到尾只认识 KingdomFactory 这一个类型,它调用的三个方法返回的也都是抽象类型。于是:改一个配置字符串,整个王国——国王、军队、城堡——全部换血,而客户端一行代码都没动。

    这就是抽象工厂比工厂方法"更高一层"的地方:工厂方法切换的是一个产品,抽象工厂切换的是一整套。

    3.4 开闭原则的倾斜性

    在抽象工厂模式中,增加新的产品族很方便,但是增加新的产品等级结构很麻烦,这种性质被称为"开闭原则"的倾斜性。

    变更类型是否支持开闭原则说明
    增加产品族 ✅ 支持 只需增加具体产品并对应增加一个新的具体工厂,对已有代码无须做任何修改
    增加新的产品等级结构 ❌ 违背 需要修改所有的工厂角色,包括抽象工厂类

    用本仓库的例子具体走一遍。

    情况 A:新增一个"亡灵族"(增加产品族)——轻松

    步骤文件动作
    1 UndeadKingdom.h/.cpp 新增 UndeadCastle、UndeadKing、UndeadArmy
    2 UndeadKingdomFactory.h/.cpp 新增,实现三个 create 方法
    3 conf.properties 改成 afp=afp::UndeadKingdomFactory
    KingdomFactory.h / 已有具体工厂 / Client.cpp ✅ 都不用改

    情况 B:新增一个产品等级结构"将军(General)"——痛苦

    步骤文件动作
    1 General.h 新增抽象产品
    2 ElfKindom.h/.cpp 新增 ElfGeneral
    3 OrcKingdom.h/.cpp 新增 OrcGeneral
    4 KingdomFactory.h ❌ 修改抽象工厂,加 virtual General* createGeneral() = 0;
    5 ElfKingdomFactory.h/.cpp ❌ 修改,实现新方法
    6 OrcKingdomFactory.h/.cpp ❌ 修改,实现新方法

    第 4~6 步是致命伤:抽象工厂一改,所有具体工厂都被迫跟着改。更麻烦的是,如果这个抽象工厂被第三方/其他团队使用,那就是破坏性变更(breaking change)。

    "倾斜性"这个词很传神——抽象工厂的天平是斜的:横向扩展(加族)很爽,纵向扩展(加产品线)很痛。回到那张二维表格:

    • 加一行(新族)→ 只是"多一个具体工厂",已有行完全不受影响;

    • 加一列(新等级结构)→ 意味着每一行都要多补一个格子,也就是每一个具体工厂都必须改。

    "加列"必然惊动所有人,这就是它违背开闭原则的根源。 所以抽象工厂有一个隐含前提——"产品等级结构稳定",这是选择抽象工厂前必须确认的。

    3.5 优缺点与适用环境

    优点

  • 隔离了具体类的生成。 客户不需要知道什么被创建。因为所有具体工厂都实现了抽象工厂的公共接口,只需改变具体工厂的实例,就可以在某种程度上改变整个软件系统的行为。

  • 保证同族对象被一起使用。 当一个产品族中的多个对象被设计成一起工作时,它能保证客户端始终只使用同一个产品族中的对象。

  • 增加新的产品族很方便,无须修改已有系统,符合开闭原则。

  • 第 2 点是抽象工厂独有的优点,工厂方法做不到:工厂方法能保证"我给你的是我要的那种武器",但保证不了"这批对象能凑成一套"。抽象工厂的强约束,恰恰是它存在的理由。

    缺点

    增加新的产品等级结构麻烦,需要对原有系统进行较大修改,甚至需要修改抽象层代码,违背了开闭原则。

    适用环境

  • 一个系统不应当依赖于产品类实例如何被创建、组合和表达的细节。 用户无须关心对象的创建过程,将创建和使用解耦。

  • 系统中有多于一个的产品族,而每次只使用其中某一产品族。 可通过配置文件等方式让用户动态改变产品族,也可以很方便地增加新的产品族。

  • 属于同一个产品族的产品将在一起使用,这一约束必须在系统的设计中体现出来。 同一产品族中的产品可以是没有任何关系的对象,但它们具有一些共同的约束,如同一操作系统下的按钮和文本框。

  • 产品等级结构稳定,设计完成后不会增加或删除已有的产品等级结构。

  • 第 3 点的例子太经典,必须记住——同一操作系统下的按钮和文本框,这正是抽象工厂模式的原始经典案例(也是 GoF 书里的例子)。

    想一想:在写一个跨平台的 UI 库,支持 Windows 和 Mac 两种风格。

    • 产品等级结构:Button、TextBox —— 两条竖线;

    • 产品族:Windows 风格(WinButton + WinTextBox)、Mac 风格(MacButton + MacTextBox)—— 两行。

    为什么必须用抽象工厂?因为按钮和文本框本身没有任何关系,但在视觉风格上必须一致——你不能给用户一个 Windows 风格的按钮配一个 Mac 风格的文本框,那看起来就是个 bug。

    "共同的约束条件" = 操作系统的类型。 抽象工厂就是把这个"看不见的风格约束"变成了"由类型系统强制保证"的东西。

    判断标准很清晰:问自己"这些对象之间有没有必须一起使用的约束?" 有 → 抽象工厂;没有,只是"造一个对象" → 工厂方法就够了。

    面试高频题:抽象工厂模式是否符合开闭原则? 答:部分符合——"增产品族"符合,"增产品等级结构"违背。这就是它的"倾斜性"。 答的时候一定要分情况说,不要简单答"符合"或"不符合"。


    4. 三种工厂模式对比

    4.1 核心对比表

    维度简单工厂工厂方法抽象工厂
    是否 GoF 23 种 ❌ 否 ✅ 是 ✅ 是
    GoF 分类 创建型 · 类模式 创建型 · 对象模式
    别称 静态工厂方法模式 虚拟构造器 / 多态工厂模式
    工厂方法数量 1 个(带 type 参数) 1 个 一组(每种产品一个)
    选择产品的机制 switch / if 判断参数 多态(换工厂) 多态(换整族工厂)
    产品维度 单一产品等级结构 单一产品等级结构 多个产品等级结构(产品族)
    加新产品 ❌ 改工厂 switch ✅ 加产品类 + 加工厂类 ✅ 加产品类 + 加工厂类
    加新产品线 ❌ 改抽象工厂 + 所有具体工厂
    是否符合 OCP ❌ 违背 ✅ 符合 ⚠️ 倾斜(加族符合,加等级结构违背)
    类的增长速度 慢(+1 产品) 快(成对 +2) 快(+1 产品 × 现有族数)
    客户端知道什么 参数含义 工厂类名 工厂类名
    典型适用 对象种类少、不常变 产品会持续扩展 多族产品需配套使用

    4.2 一句话记忆法

    简单工厂:一个工厂,switch 决定造谁       → 「一个万能师傅」
    工厂方法:一个产品一个工厂,多态决定造谁   → 「一个师傅一门手艺」
    抽象工厂:一个族一个工厂,一次造一整套     → 「一家分公司包一整套人马」

    4.3 选择决策树

    你需要的对象是"一个"还是"一整套(有配套约束)"?

    ├─ 一整套(如:国王+城堡+军队、按钮+文本框)────────▶ 抽象工厂模式
    │                                                     (确认:产品等级结构稳定吗?)

    └─ 一个
      │
      ├─ 产品种类少(≤ 三五种)且几乎不增加 ────────────▶ 简单工厂模式
      │                                                   (可配合配置文件提高灵活性)
      │
      └─ 产品会持续扩展,希望符合开闭原则 ──────────────▶ 工厂方法模式
                                                          (可叠加自注册反射,实现零修改扩展)

    4.4 开闭原则在三个模式中的表现

      简单工厂                     工厂方法                   抽象工厂
      加产品:改 switch             加产品:加一对类             加族:加一组类
        ❌                           ✅                       ✅
             ─────────────────────▶ ─────────────────────▶
                把"改老代码"                    把"造一个"升级为
                变成"加新代码"                    "造一整套"

                              抽象工厂的代价:
                  加产品线(等级结构)→ ❌ 改所有工厂

    一句话总结整章的演进逻辑:三个模式都在干同一件事——"把创建对象这件事从使用者的代码里挪走"。它们越往后,挪得越彻底,代价是结构越复杂。

    而"加产品时要改哪里"这个问题,就是衡量它们的标尺:

    • 简单工厂 → 改一个文件(工厂的 switch);

    • 工厂方法 → 改零个已有文件,只加两个新文件;

    • 抽象工厂 → 加产品族改零个,加产品线要改全部具体工厂。


    5. 面试专题

    5.1 开场题:说说工厂模式吧

    这类问题最常见的开场白,参考答案的结构是:总起(三个成员)→ 逐个展开(概念 → 角色 → 适用环境)。

    ① 简单工厂

    项内容
    概念 将对象实例化过程封装到一个对象中,由这个对象负责它的实例化
    角色 Factory、Product、ConcreteProduct
    适用环境 1. 工厂类创建的对象种类比较少;2. 客户端只知道传入参数就能创建对象,且不关注创建过程

    ② 工厂方法

    项内容
    概念 定义了一个创建对象的抽象方法,由子类决定实例化哪一个类,把类的实例化推迟到了子类
    角色 Product、ConcreteProduct、Factory、ConcreteFactory
    适用环境 1. 客户端不需要知道所需对象类型时;2. 需要满足产品扩展开闭原则时;3. 简单工厂创建的类职责过于沉重时

    ③ 抽象工厂

    项内容
    概念 提供一个接口,客户端用它获取一组产品,不需要关注实际产出的具体产品是什么
    角色 AbstractFactory、ConcreteFactory、AbstractProduct、ConcreteProduct
    适用环境 1. 系统有多个产品族且每次只使用一个;2. 属于一个产品族的产品必须一起使用,且这一约束必须在设计时体现出来;3. 产品等级结构稳定

    这个"骨架"好在它有一个固定套路:

    • 概念回答"是什么"——一句话说清它做了什么;

    • 角色回答"有哪些参与者"——体现你知道模式的内部结构,不是背概念;

    • 适用环境回答"什么时候用"——最能体现工程经验的部分,也是面试官最关心的。

    三个模式都按这三段答,每段一两句话,总时长控制在 1.5~2 分钟,非常适合面试节奏。

    可扩展回答的两个方向:

    • 工厂模式的优缺点 —— 参考本文 1.5 / 2.5 / 3.5 节。答的时候要成对说:说了"符合开闭原则"就一定要接"类的个数成对增加",这才叫权衡。

    • 描述你在项目中如何使用工厂模式 —— 这是必须提前准备的一题,回答要有具体场景 + 具体收益。例如:

      "我们的数据导出模块支持 CSV/Excel/PDF 三种格式,一开始用 switch 分发,每加一种格式都要改分发函数、回归测试整个导出流程。后来重构成工厂方法:抽象出 Exporter 接口,每种格式一个 ExporterFactory,再配合配置文件和自注册把工厂类名写进配置。结果是加新格式从'改 2 个文件 + 全量回归'变成'加 2 个新文件 + 配置一行',老代码一行没动。"

      这个回答包含了具体场景、遇到的问题、用了什么模式、量化的收益——四要素齐全。

      不要答"我们框架里用了 Spring 的 @Bean"就完事——那只能说明你会用框架,不能说明你懂模式。

    完整答题示范(约 90 秒):

    "工厂模式是一个家族,包含简单工厂、工厂方法、抽象工厂三个成员,它们解决的核心问题是把对象的创建和使用解耦。

    简单工厂是把对象的实例化过程封装到一个工厂类里,用静态方法加一个 type 参数决定造哪个产品。它有三个角色:工厂、抽象产品、具体产品。适合产品种类少、且客户端只关心参数不关心创建过程的场景。它的缺点是工厂类职责过重,加产品必须改工厂的 switch,违背开闭原则,而且静态方法没法被继承扩展。

    工厂方法模式就是为解决这个问题:它为每个产品配一个工厂,抽象工厂里声明 createXxx() 接口,由具体子类决定实例化哪个产品。加新产品只需新增一个产品类和一个工厂类,老代码不用动,符合开闭原则。代价是类的数量成对增长,而且引入了抽象层,理解成本变高。它适合产品会持续扩展的场景。

    抽象工厂再往上抽象一层:当需要创建的是一整套有配套约束的产品时,用一个工厂统一负责一个产品族。比如经典案例——跨平台 UI 里 Windows 的按钮配 Windows 的文本框。加产品族很方便,但加新的产品等级结构要改所有具体工厂,所以有'开闭原则的倾斜性',它要求产品等级结构稳定。

    三者的选择就看两点:产品会不会持续增加,以及对象之间有没有必须配套使用的约束。"

    这个示范的得分点:开场一句就点明核心价值("把创建和使用解耦"),而不是干巴巴地背定义;每个模式都讲到了代价;出现了关键词(开闭原则、倾斜性、推迟到子类、多态);结尾做了一次收束,说明你是有体系地在理解。


    5.2 追问:工厂模式 vs 建造者模式

    一句话总结:建造者关注装配顺序,工厂模式关注如何使用对象。

    维度工厂模式建造者模式
    关注点 "造什么" —— 产出什么类型的对象 "怎么造" —— 一步步装配的过程与顺序
    返回结果 返回完整可用的对象(一步到位) 返回组装完成的复杂对象(分步构建)
    核心方法 createXxx()(一个方法搞定) buildPartA() / buildPartB() + build()(多步)
    是否关心步骤 ❌ 不关心 ✅ 核心就是步骤和顺序
    典型场景 换一种产品/换一个数据库 同一个对象有多种配置组合、装配顺序有讲究

    用生活化的例子理解:

    • 工厂模式:你去 4S 店买车。你说"我要一辆 SUV",店里直接给你一辆能开的完整车,你不关心它是怎么装配的。

    • 建造者模式:你定制一辆车。先选发动机 → 再选轮毂 → 再选内饰 → 最后"出厂",顺序和每一步的选型都由你控制。

    换句话说:工厂是"点菜",建造者是"按食谱做菜"。

    对应适用场景:建造者适用于需要生成的对象具有复杂的内部结构、内部属性本身相互依赖;工厂适用于需要隐藏创建细节、支持产品扩展。


    5.3 高频追问清单

    Q1. 简单工厂模式是 GoF 的 23 种设计模式之一吗?

    ❌ 不是。 它又叫"静态工厂方法模式",是学习其他工厂模式的基础,但不在 GoF 23 种之列。常见陷阱题:问"GoF 创建型模式有哪 5 个"——答案是单例、工厂方法、抽象工厂、原型、建造者,不含简单工厂。

    Q2. 工厂方法模式为什么又叫"多态工厂模式"?

    因为所有的具体工厂类都具有同一抽象父类。客户端通过 Factory* 指针调用 createXxx(),实际执行的是子类的实现——这就是多态。同理,它还叫"虚拟构造器模式",因为"选择构造哪个对象"这个决定被延迟到了运行期。

    Q3. 抽象工厂模式符合开闭原则吗?

    部分符合,这就是它的"倾斜性": 增加产品族 ✅ 符合(新增具体产品 + 新增具体工厂,老代码不动);增加产品等级结构 ❌ 违背(必须修改抽象工厂和所有具体工厂)。 必须分情况回答,不能简单说"符合"或"不符合"。

    Q4. 产品等级结构和产品族的区别?

    • 产品等级结构:纵向的继承结构。如"手机 → 三星手机/小米手机/华为手机",是同一类产品的不同实现,共用抽象父类。

    • 产品族:横向的、由同一个工厂生产的一组产品,位于不同产品等级结构中。如"小米手机 + 小米笔记本 + 小米路由",它们之间没有继承关系,只是必须配套使用。

    记忆锚点:等级结构是"竖着看",产品族是"横着切"。

    Q5. 工厂模式中 throw std::string(…) 有什么问题?

    std::string 不是 std::exception 的派生类,所以调用方无法用 catch (const std::exception&) 统一捕获。工程上应该定义继承自 std::runtime_error 的自定义异常。

    Q6. 工厂返回裸指针有什么风险?

    三个风险:1. 内存泄漏——异常路径下 delete 不执行;2. 所有权不明确——调用方不知道是否该由自己 delete;3. 抽象产品缺少虚析构函数时,通过基类指针 delete 是未定义行为。 现代 C++ 的答案是返回 std::unique_ptr<Base>——所有权明确、异常安全、自动调用正确的析构函数。

    Q7. 如果生产对象很昂贵(比如数据库连接),工厂该怎么优化?

    在工厂内部引入对象池/缓存,或结合享元模式共享无状态对象。注意线程安全问题(std::mutex 或 std::call_once)。

    Q8. 工厂类用 static 方法还是实例方法?

    • 简单工厂:用 static(不需要工厂实例),但代价是无法被继承和重写,失去多态扩展能力;

    • 工厂方法与抽象工厂:必须用实例方法(virtual),因为多态是它们的根基。

    这个对比本身就是理解工厂方法模式"存在的理由"的钥匙。

    Q9. 配置文件 + 反射的方案有什么隐患?

  • 静态初始化顺序问题——跨编译单元的静态对象初始化顺序不确定,可能"还没注册就被查找";

  • void* 强转没有类型检查——配置写错类名(如把 fmp:: 写成 afp::)编译期不报错,运行期才崩;

  • 调试困难——错误被推迟到运行期,且是"配置问题"而非"代码问题",定位成本高;

  • 不利于静态分析——IDE 找引用、死代码检测都可能失效。

  • 这些是用灵活性换来的代价,要如实说明。

    Q10. 什么时候"不该"用工厂模式?

    • 产品只有一种且基本不会变——直接 new 更简单清晰;

    • 只是为了"看起来设计得好"而引入工厂,导致类数量爆炸——典型的过度设计;

    • 创建逻辑极其简单(就是一行 new),且没有配置化/测试替身的需求。

    对应《面向对象设计原则》的提醒:"过度设计和设计不足,都是失败。"

    这些追问有一个共同的深层考察点:你是不是真的理解"权衡"。

    注意几乎所有答案里都有"代价"或"不推荐"的部分。面试官问"throw std::string 有什么问题"、"什么时候不该用",不是要否定这些技术,而是在看你会不会无脑吹。一个只讲优点的答案,是新手答案;一个能说清代价和边界的答案,才是工程师答案。


    5.4 答题模板

    模板 1:概念类题("说说 XX 模式")

    【总起】 工厂模式包含三个成员:简单工厂、工厂方法、抽象工厂,
          核心价值是把"对象的创建"和"对象的使用"解耦。

    【分述】 ① 简单工厂:概念 → 角色 → 适用环境 → 缺点(违背 OCP)
          ② 工厂方法:概念 → 角色 → 适用环境 → 优点(符合 OCP) / 缺点(类成对增加)
          ③ 抽象工厂:概念 → 角色 → 适用环境 → 倾斜性

    【收束】 选择依据:产品会不会持续扩展?对象之间有没有配套使用的约束?

    模板 2:项目经验题("你在项目中怎么用")

    【场景】 项目/模块是什么,具体业务是什么
    【痛点】 不用模式之前的痛苦(switch 膨胀?改一处测全部?)
    【方案】 用了哪个模式,怎么设计的(抽象出什么接口,几个实现类)
    【收益】 ✅ 一定要量化:从"改 N 个文件"变成"加 M 个文件 + 配置一行"
    【反思】 这个方案的局限是什么,后来又做了什么优化

    模板 2 的"反思"部分,是最容易被忽略、但最能加分的一环。

    说"我们用了工厂模式"的人很多;能接着说"但它也有代价,产品线增加时很痛,所以我们后来把 XX 拆出去了"的人很少。面试官招的不是"模式背诵机",而是"能在真实约束下做技术决策的人"。承认方案的边界,恰恰证明你做过真实的决策。


    6. 工程实践与踩坑

    6.1 裸指针与内存泄漏

    这是仓库代码中值得修改的一步。所有工厂方法都返回裸指针,客户端手工 delete:

    Coin* coin = CoinFactory::getCoin(type);
    cout << coin->getDescription() << endl;
    delete coin;                 // ← 如果上一行抛异常,这行就不会执行

    风险说明
    异常路径泄漏 getDescription() 若抛异常,delete 被跳过,对象泄漏
    所有权不明确 调用方无法从签名看出"该不该由我释放",极易双删或漏删
    缺少虚析构函数 抽象产品(Coin、Weapon、Castle…)均没有虚析构函数,通过基类指针 delete 派生类对象在标准上属于未定义行为

    改进写法:

    // 方案 A:抽象产品加虚析构函数
    class Coin
    {
    public:
       virtual ~Coin() = default;
       virtual std::string getDescription() = 0;
    };

    // 方案 B(推荐):工厂返回智能指针,所有权清晰
    std::unique_ptr<Coin> getCoin(int type);

    // 客户端:无需 delete,异常安全
    auto coin = CoinFactory::getCoin(type);
    cout << coin->getDescription() << endl;

    用 unique_ptr 之后有两个直接好处:不用管释放(出了作用域自动析构,异常路径也安全,即 RAII);所有权明确(从签名就能看出该由谁负责)。

    面试手写代码时,返回 std::unique_ptr 是稳赚不赔的加分项。

    6.2 线程安全

    仓库的工厂方法本身没有共享可变状态(每次调用都是新的 new),所以是线程安全的。但有几个地方需要注意:

    位置风险说明
    ClassFactory::getInstance() ✅ 安全(C++11 起) 局部静态变量的初始化由标准保证线程安全(magic static)
    ClassFactory::registClass() ⚠️ 只在启动期调用 注册发生在 main() 之前的单线程静态初始化阶段,不涉及竞争;但若改成运行时动态注册,必须加锁
    若引入对象池/缓存 ❌ 需自行同步 一旦工厂内部有共享可变状态,必须用 std::mutex 保护

    6.3 工厂膨胀与过度设计

    症状清单——出现任一条,就该反思是否滥用工厂了:

    • 抽象工厂里的创建方法只有一个 → 应该降级为工厂方法;

    • 一个工厂类里塞了十几个 createXxx(),且彼此毫无关联 → 职责过重,该拆;

    • 工厂只是原样转发 new,没有任何判断、缓存、配置逻辑 → 这层抽象没有产生价值;

    • 项目里 30 个类,其中 20 个是工厂类 → 类数量失衡。

    最后一句话总结分寸感:工厂模式的价值在于"隔离变化",如果那个地方本来就不会变,隔离它就是纯粹的成本。 判断方法还是那一条——问自己"这里会变吗?" 会变 → 值得留扩展点;不会变 → 保持简单。


    附录 A:三模式速查表

    简单工厂工厂方法抽象工厂
    GoF 23 种
    别称 静态工厂方法模式 虚拟构造器 / 多态工厂模式
    意图一句话 一个工厂用参数决定造哪个产品 让子类决定实例化哪个产品 创建一系列相关对象,不指定具体类
    英文定义关键词 static method / hide implementation logic define an interface / let subclasses decide / defer to subclasses families of related or dependent objects
    角色 Factory、Product、ConcreteProduct Product、ConcreteProduct、Factory、ConcreteFactory AbstractFactory、ConcreteFactory、AbstractProduct、ConcreteProduct
    产品维度 1 条产品等级结构 1 条产品等级结构 N 条产品等级结构(产品族)
    工厂方法数 1 个(带参数) 1 个 一组
    选择机制 switch / if 判断参数 多态(换工厂) 多态(换整族工厂)
    OCP ❌ 违背(改 switch) ✅ 符合(加类) ⚠️ 倾斜(加族 ✅ / 加等级结构 ❌)
    主要优点 创建与使用分离;减少记忆量;可配置化 隐藏实例化细节;多态设计;完全符合 OCP 隔离具体类;保证同族对象配套使用;加族方便
    主要缺点 工厂职责过重;类增多;扩展难;静态方法无法继承 类成对增加;引入抽象层增加理解难度 加产品等级结构麻烦,需改所有工厂(违背 OCP)
    适用环境 种类少;客户端只关心参数 客户端不知道所需对象的类;需符合 OCP;简单工厂职责过重时 多于一个产品族且每次只用一个;同族对象必须一起使用;产品等级结构稳定
    仓库示例 factory/sfp(硬币) factory/fmp(铁匠铺) factory/afp(王国)

    结语

            那么工厂模式我们就说完了,期待我们下一次单例模式的讲解。

    我是YYYing,后面还有更精彩的内容,希望各位能多多关注支持一下主包。

    无限进步,我们下次再见!


    —⭐️封面自取⭐️—

    赞(0)
    未经允许不得转载:171主机测评 » 【设计模式系列 (二) 】万字详解工厂模式
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址