自动注册的反射工厂模式

3 阅读7分钟

先来看一段代码

class DlnaPlayerFactory{
public:
    static DlnaPlayerFactory& getInstance(){
        static DlnaPlayerFactory _factory;
        return _factory;
    }

    DlnaPlayerFactory& registe(std::string name, std::function<DlnaPlayer*(std::map<std::string, std::any>)> constructor){
        auto lock = std::lock_guard(_mutex);
        constructors[name] = constructor;
        return *this;
    }

    std::shared_ptr<DlnaPlayer> create(std::string name, std::map<std::string, std::any> init_args = {}) const {
        auto lock = std::lock_guard(_mutex);
        try{
            auto constructor = constructors.at(name);
            return std::shared_ptr<DlnaPlayer>{constructor(init_args)};
        }
        catch(const std::exception& e){
            qErrnoWarning(e.what());
            return nullptr;
        }
    }

    struct Register{
        Register(std::string name, std::function<DlnaPlayer*(std::map<std::string,std::any>)> constructor){
            getInstance().registe(name, constructor);
        }
    };

protected:
    DlnaPlayerFactory(){}
private:
    mutable std::mutex _mutex;
    std::map<std::string, std::function<DlnaPlayer*(std::map<std::string,std::any>)>> constructors;
};

这种写法在 C++ 中被称为**“自动注册的反射工厂模式”**(Auto-registering Reflection Factory)。

别被这个名字吓到,其实它的原理就像**“点菜”**:

  1. 工厂是“菜单”。
  2. 具体的播放器类(比如 VlcPlayer)在程序启动还没开始时,就偷偷把自己的名字写到了“菜单”上。
  3. (用户)只需要对着菜单喊一声“我要 VlcPlayer”,工厂就给你端上来。

我来手把手教你如何写一个具体的播放器类,让它能被这个工厂接纳。我们假设你要写一个基于 FFmpeg 的播放器,名字叫 FFmpegPlayer


第一步:创建头文件 (FFmpegPlayer.h)

首先,你得创建一个类,继承自 DlnaPlayer,并把那些纯虚函数(= 0 的函数)都实现了。

#ifndef FFPEGPLAYER_H
#define FFPEGPLAYER_H

#include "DlnaPlayer.h" // 包含你刚才发的那个头文件
#include <iostream>

// 1. 继承 DlnaPlayer
class FFmpegPlayer : public DlnaPlayer {
public:
    // 2. 定义一个构造函数,必须符合工厂要求的格式
    // 工厂要求:接收 std::map<std::string, std::any>
    FFmpegPlayer(std::map<std::string, std::any> args);

    ~FFmpegPlayer() override;

    // 3. 实现所有纯虚函数 (这里只写几个做演示,实际都要写)
    void hide() override { std::cout << "FFmpeg隐藏窗口" << std::endl; }
    void show() override { std::cout << "FFmpeg显示窗口" << std::endl; }
    void setMediaSource(std::string url) override { m_url = url; }
    void play() override { std::cout << "FFmpeg开始播放: " << m_url << std::endl; }
    
    // ... 其他 stop, pause, seek 等必须实现,否则编译报错 ...
    void stop() override {}
    void pause() override {}
    void seek(size_t time_ms) override {}
    void next() override {}
    void previous() override {}
    const std::string getCurrectStatus() const override { return "Playing"; }
    const std::string getCurrectMediaSource() const override { return m_url; }
    const size_t getCurrectPlayTimestamp_ms() const override { return 0; }
    void onStatusChanged(std::function<void(std::string)>) override {}
    void onTimestampUpdate(std::function<void(size_t)>) override {}

private:
    std::string m_url;
};

#endif // FFPEGPLAYER_H

第二步:核心步骤 —— 实现与注册 (FFmpegPlayer.cpp)

这是最关键的一步!不要在 main 函数里注册,要在 .cpp 文件里利用全局变量自动注册。

#include "FFmpegPlayer.h"
#include <QDebug>

// 1. 实现构造函数
FFmpegPlayer::FFmpegPlayer(std::map<std::string, std::any> args) {
    // 这里的 args 就是你创建时传进来的参数
    // 演示:尝试获取一个叫 "volume" 的参数
    if (args.count("volume")) {
        try {
            // std::any 需要用 any_cast 转回原来的类型
            int vol = std::any_cast<int>(args["volume"]);
            qDebug() << "FFmpegPlayer 初始化,音量设置为:" << vol;
        } catch (...) {
            qDebug() << "参数类型转换失败";
        }
    }
}

FFmpegPlayer::~FFmpegPlayer() {
    qDebug() << "FFmpegPlayer 被销毁";
}

// ==========================================
// 重点来了!这就是“自动注册”的魔法
// ==========================================

// 2. 定义一个静态的创建函数,用来 new 出这个对象
// 为什么要这个?因为工厂存的是函数指针,需要通过这个函数来创建对象
DlnaPlayer* createFFmpegPlayer(std::map<std::string, std::any> args) {
    return new FFmpegPlayer(args);
}

// 3. 利用工厂里的内部类 Register 进行注册
// 这行代码会在 main 函数执行之前就运行!
// 参数1: "ffmpeg" -> 这是以后你创建它时用的名字 ID
// 参数2: createFFmpegPlayer -> 告诉工厂怎么创建它
static DlnaPlayerFactory::Register _register("ffmpeg", createFFmpegPlayer);

// 写法二(更高级的 Lambda 写法,省去上面第2步,直接写在一起):
/*
static DlnaPlayerFactory::Register _register_lambda("ffmpeg_v2", 
    [](std::map<std::string, std::any> args) -> DlnaPlayer* {
        return new FFmpegPlayer(args);
    }
);
*/

第三步:如何使用 (main.cpp)

现在,你的 FFmpegPlayer 已经准备好了。在主程序里,你完全不需要 #include "FFmpegPlayer.h",只需要知道它的名字 "ffmpeg" 就能用!

这就是工厂模式的解耦优势。

#include "DlnaPlayer.h"
#include <iostream>
#include <any>

int main() {
    // 准备初始化参数 (std::any 可以存任何东西)
    std::map<std::string, std::any> params;
    params["volume"] = 80;      // 存个 int
    params["debug_mode"] = true; // 存个 bool

    // 1. 找工厂要对象
    // 注意:这里不需要 include 具体播放器的头文件,只要名字 "ffmpeg"
    std::shared_ptr<DlnaPlayer> player = DlnaPlayerFactory::getInstance().create("ffmpeg", params);

    if (player) {
        // 2. 像操作普通指针一样操作它
        player->setMediaSource("http://movie.mp4");
        player->play();
        
        // player->show();
    } else {
        std::cout << "创建失败,工厂里没找到这个名字的播放器!" << std::endl;
    }

    return 0;
} 
// 程序结束,player 智能指针引用计数归零,自动调用 FFmpegPlayer 的析构函数

帮你复盘一下“魔法”发生的过程

  1. 程序启动:操作系统加载程序。

  2. 静态初始化:在 main() 函数执行之前,全局变量和静态变量开始初始化。

    • FFmpegPlayer.cpp 里的 static DlnaPlayerFactory::Register _register(...) 被执行。
    • Register 的构造函数调用了 DlnaPlayerFactory::getInstance().registe(...)
    • 工厂里的 map 此时就被插入了一条数据:{"ffmpeg", createFFmpegPlayer}
  3. 进入 main()

    • 你调用 factory.create("ffmpeg", ...)
    • 工厂去 map 里查 "ffmpeg",找到了对应的函数。
    • 执行函数,new FFmpegPlayer,返回指针。

这种写法的优点

假如你想增加一个 GStreamerPlayer,你只需要新建 .h.cpp,写好注册代码,完全不用去修改工厂类(DlnaPlayerFactory)的代码,也不用改 main.cpp。这在大型项目中非常有用,这叫“对扩展开放,对修改关闭”(开闭原则)。 刚才教你的这种写法,在软件工程里有一个非常响亮的名字,叫做:

带有“自动注册”功能的工厂模式

(Auto-registering Factory Pattern)

也有人称之为 “反射式工厂” (虽然 C++ 原生没有 Java 那样的反射,但我们通过 map 模拟实现了反射的效果)。

这不仅仅是“一种写法”,它其实是**插件化架构(Plugin Architecture)的雏形。为了让你明白它的含金量,我们来做一个“新手写法” vs “高手写法”**的对比。


1. 痛苦的“新手写法” (如果不这么写,你会怎么写?)

如果你不用刚才那个 Register 的技巧,通常你会怎么实现工厂模式?你会写出一种叫 “上帝类” (God Class) 的代码,它是所有开发者的噩梦。

假设你有 FFmpegPlayerVlcPlayerGstreamerPlayer 三种播放器。

修改前的工厂 (DlnaPlayerFactory.cpp):

// 你必须在工厂里引用所有子类的头文件!耦合度极高!
#include "FFmpegPlayer.h"
#include "VlcPlayer.h"
#include "GstreamerPlayer.h" 

std::shared_ptr<DlnaPlayer> create(std::string name, ...) {
    if (name == "ffmpeg") {
        return new FFmpegPlayer(...);
    } 
    else if (name == "vlc") { // 每次加新播放器,都要来改这行代码
        return new VlcPlayer(...);
    }
    else if (name == "gstreamer") { // 代码越来越长...
        return new GstreamerPlayer(...);
    }
    return nullptr;
}

“新手写法”的致命缺点:

  1. 违反“开闭原则” (Open/Closed Principle) :这是面向对象设计最重要的原则之一。原则要求:对扩展开放,对修改关闭

    • 上面这种写法,每次你新写一个播放器,你都得去修改工厂类的源代码(加一个 else if)。万一你手抖改错了,整个工厂都挂了,影响别的播放器。
  2. 依赖地狱:工厂类必须 #include 所有具体的播放器头文件。只要其中一个子类头文件编译不过,工厂就报错,整个项目都瘫痪。


2. 你刚才学会的“高手写法”

你现在的写法,彻底解决了上面所有问题。

这种写法的牛逼之处:

  1. 彻底解耦 (Decoupling)

    • DlnaPlayerFactory 根本不知道 FFmpegPlayer 的存在!它只知道它有一个 map,里面存着一堆函数指针。
    • FFmpegPlayer 自己悄悄把自己的名字写进那个 map 里。
    • 结果:两者完全没有 #include 关系,甚至可以在不同的 .dll.so 动态库里。
  2. 真正的“插件化”

    • 想象一下,你的同事小王想加一个 SonyPlayer
    • 他只需要写 SonyPlayer.cppSonyPlayer.h
    • 他完全不需要动你写好的 DlnaPlayerFactory 代码,甚至不需要看你的代码。
    • 代码编译链接在一起,他的播放器就能用了。
  3. 利用 C++ 特性

    • 利用 static 全局变量在 main 函数之前初始化的特性,完成了“自动报名”的过程。

3. 这种模式在哪见过?

这种模式在很多大型、复杂的软件系统中非常常见:

  1. 数据库驱动 (JDBC/ODBC)

    • 你的程序写的是 Connect("mysql") 或者 Connect("oracle")
    • 程序本身并不包含 MySQL 的具体代码,而是当你引入 MySQL 的驱动包(Plugin)时,驱动包会自动向管理器注册自己。
  2. 游戏引擎 (如 Unreal Engine)

    • 各种不同的物体、AI 行为,都是注册到系统里的,引擎核心不需要知道具体的怪兽是怎么写的。
  3. Visual Studio / VS Code 的插件

    • 所有的插件都是“注册”进主程序的,主程序不需要为了某个插件去修改源代码。

总结

你刚刚学会的,是 C++ 架构师 级别的技巧。

  • 语法上:它用了 staticstd::functionstd::maplambda
  • 思想上:它实现了 “依赖倒置”“控制反转” —— 不是工厂去找产品,而是产品自己向工厂报名。

恭喜你,你的 C++ 水平已经不仅仅是“基础语法”了,你已经开始接触架构设计了!