先来看一段代码
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)。
别被这个名字吓到,其实它的原理就像**“点菜”**:
- 工厂是“菜单”。
- 具体的播放器类(比如
VlcPlayer)在程序启动还没开始时,就偷偷把自己的名字写到了“菜单”上。 - 你(用户)只需要对着菜单喊一声“我要 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 的析构函数
帮你复盘一下“魔法”发生的过程
-
程序启动:操作系统加载程序。
-
静态初始化:在
main()函数执行之前,全局变量和静态变量开始初始化。FFmpegPlayer.cpp里的static DlnaPlayerFactory::Register _register(...)被执行。Register的构造函数调用了DlnaPlayerFactory::getInstance().registe(...)。- 工厂里的
map此时就被插入了一条数据:{"ffmpeg", createFFmpegPlayer}。
-
进入 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) 的代码,它是所有开发者的噩梦。
假设你有 FFmpegPlayer、VlcPlayer、GstreamerPlayer 三种播放器。
修改前的工厂 (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;
}
“新手写法”的致命缺点:
-
违反“开闭原则” (Open/Closed Principle) :这是面向对象设计最重要的原则之一。原则要求:对扩展开放,对修改关闭。
- 上面这种写法,每次你新写一个播放器,你都得去修改工厂类的源代码(加一个
else if)。万一你手抖改错了,整个工厂都挂了,影响别的播放器。
- 上面这种写法,每次你新写一个播放器,你都得去修改工厂类的源代码(加一个
-
依赖地狱:工厂类必须
#include所有具体的播放器头文件。只要其中一个子类头文件编译不过,工厂就报错,整个项目都瘫痪。
2. 你刚才学会的“高手写法”
你现在的写法,彻底解决了上面所有问题。
这种写法的牛逼之处:
-
彻底解耦 (Decoupling) :
DlnaPlayerFactory根本不知道FFmpegPlayer的存在!它只知道它有一个map,里面存着一堆函数指针。FFmpegPlayer自己悄悄把自己的名字写进那个map里。- 结果:两者完全没有
#include关系,甚至可以在不同的.dll或.so动态库里。
-
真正的“插件化” :
- 想象一下,你的同事小王想加一个
SonyPlayer。 - 他只需要写
SonyPlayer.cpp和SonyPlayer.h。 - 他完全不需要动你写好的
DlnaPlayerFactory代码,甚至不需要看你的代码。 - 代码编译链接在一起,他的播放器就能用了。
- 想象一下,你的同事小王想加一个
-
利用 C++ 特性:
- 利用
static全局变量在main函数之前初始化的特性,完成了“自动报名”的过程。
- 利用
3. 这种模式在哪见过?
这种模式在很多大型、复杂的软件系统中非常常见:
-
数据库驱动 (JDBC/ODBC) :
- 你的程序写的是
Connect("mysql")或者Connect("oracle")。 - 程序本身并不包含 MySQL 的具体代码,而是当你引入 MySQL 的驱动包(Plugin)时,驱动包会自动向管理器注册自己。
- 你的程序写的是
-
游戏引擎 (如 Unreal Engine) :
- 各种不同的物体、AI 行为,都是注册到系统里的,引擎核心不需要知道具体的怪兽是怎么写的。
-
Visual Studio / VS Code 的插件:
- 所有的插件都是“注册”进主程序的,主程序不需要为了某个插件去修改源代码。
总结
你刚刚学会的,是 C++ 架构师 级别的技巧。
- 语法上:它用了
static、std::function、std::map、lambda。 - 思想上:它实现了 “依赖倒置” 和 “控制反转” —— 不是工厂去找产品,而是产品自己向工厂报名。
恭喜你,你的 C++ 水平已经不仅仅是“基础语法”了,你已经开始接触架构设计了!