设计模式不是装饰——是经过验证的问题解决方案。
这篇文章聚焦三种在 C++ 工程中最常见、最容易被误用的模式:RAII、观察者、工厂。每种模式都从一个具体的工程痛点出发,给出现代 C++ 的实现,并结合真实场景说明为什么这么做。
RAII:资源获取即初始化
问题场景
你在写 HID 设备管理,需要打开设备句柄,做一些操作,最后关闭。最初的写法大概是这样:
// ❌ 危险写法:多个出口,每个都要记得 CloseHandle
HANDLE hDevice = CreateFile(devicePath, GENERIC_READ | GENERIC_WRITE, ...);
if (hDevice == INVALID_HANDLE_VALUE) return false;
if (!HidD_GetAttributes(hDevice, &attributes)) {
CloseHandle(hDevice); // 记得关
return false;
}
// 如果这里抛异常呢?句柄泄漏!
processDevice(hDevice);
CloseHandle(hDevice); // 正常路径才会执行到
return true;
函数有 3 个出口,每个都要手写 CloseHandle。一旦中间抛了异常,或者新同事加了一个 early return,句柄就泄漏了。
RAII 包装器实现
解决方案是把资源的生命周期绑定到对象的生命周期——对象析构时资源自动释放:
#include <windows.h>
#include <functional>
#include <stdexcept>
// 通用 RAII 包装器:接受任意清理函数
class ScopeGuard {
public:
explicit ScopeGuard(std::function<void()> cleanup)
: cleanup_(std::move(cleanup)), active_(true) {}
~ScopeGuard() {
if (active_) cleanup_();
}
// 提前解除(不再清理,所有权转移时使用)
void dismiss() { active_ = false; }
// 禁止拷贝
ScopeGuard(const ScopeGuard&) = delete;
ScopeGuard& operator=(const ScopeGuard&) = delete;
private:
std::function<void()> cleanup_;
bool active_;
};
// 专用 HID 设备句柄包装器
class HidDeviceHandle {
public:
explicit HidDeviceHandle(const wchar_t* devicePath) {
handle_ = CreateFileW(
devicePath,
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
nullptr,
OPEN_EXISTING,
FILE_FLAG_OVERLAPPED,
nullptr
);
if (handle_ == INVALID_HANDLE_VALUE) {
throw std::runtime_error("Failed to open HID device");
}
}
~HidDeviceHandle() {
if (handle_ != INVALID_HANDLE_VALUE) {
CloseHandle(handle_);
}
}
// 移动语义:所有权转移
HidDeviceHandle(HidDeviceHandle&& other) noexcept
: handle_(other.handle_) {
other.handle_ = INVALID_HANDLE_VALUE;
}
HidDeviceHandle& operator=(HidDeviceHandle&& other) noexcept {
if (this != &other) {
if (handle_ != INVALID_HANDLE_VALUE) CloseHandle(handle_);
handle_ = other.handle_;
other.handle_ = INVALID_HANDLE_VALUE;
}
return *this;
}
// 禁止拷贝(句柄不能共享所有权)
HidDeviceHandle(const HidDeviceHandle&) = delete;
HidDeviceHandle& operator=(const HidDeviceHandle&) = delete;
HANDLE get() const { return handle_; }
bool valid() const { return handle_ != INVALID_HANDLE_VALUE; }
private:
HANDLE handle_ = INVALID_HANDLE_VALUE;
};
裸指针 vs unique_ptr vs 自定义 RAII
// ❌ 裸指针:手动管理,容易泄漏
HANDLE h = OpenDevice();
// ... 中间任何异常或 return 都会泄漏
CloseHandle(h);
// ✅ unique_ptr + 自定义 deleter:适合有 delete-like 清理函数的资源
auto deleter = [](HANDLE* p) { if (*p != INVALID_HANDLE_VALUE) CloseHandle(*p); };
std::unique_ptr<HANDLE, decltype(deleter)> hGuard(&h, deleter);
// ✅ 自定义 RAII 类:推荐方案,语义最清晰
{
HidDeviceHandle device(L"\\\\.\\HID#...#...#{...}");
// 任意操作,任意路径退出,句柄都会被 ~HidDeviceHandle() 关闭
HIDD_ATTRIBUTES attrs;
HidD_GetAttributes(device.get(), &attrs);
} // 这里自动 CloseHandle
工程案例:多资源同时管理
bool initializeDevice(const wchar_t* path) {
try {
HidDeviceHandle device(path);
// 互斥锁也可以 RAII:std::lock_guard
std::mutex mtx;
std::lock_guard<std::mutex> lock(mtx);
// 临时缓冲区
auto buffer = std::make_unique<uint8_t[]>(1024);
// 所有资源在 try 块结束时自动释放
// 无需任何手动清理代码
return HidD_SetFeature(device.get(), buffer.get(), 1024);
} catch (const std::exception& e) {
// device 析构已经关闭句柄,这里只需处理错误逻辑
logError(e.what());
return false;
}
}
RAII 的本质:用 C++ 对象的确定性析构(deterministic destruction)替代手动资源管理。任何需要”成对操作”(open/close、lock/unlock、malloc/free)的资源,都适合用 RAII 包装。
观察者模式:类型安全事件系统
问题场景
设备管理器需要在设备插拔时通知多个组件:UI 要刷新列表,日志模块要记录,业务层要更新状态。最简单(也最糟糕)的做法是在设备管理器里直接调用这些组件:
// ❌ 紧耦合:设备管理器知道太多东西
class DeviceManager {
void onDeviceArrival() {
ui_.refreshList(); // 依赖 UI
logger_.logEvent(...); // 依赖 Logger
bizLayer_.update(...); // 依赖业务层
}
};
添加新订阅者需要修改 DeviceManager,违反开闭原则,而且单元测试极其困难。
基于 std::function 的类型安全事件系统
#include <functional>
#include <unordered_map>
#include <vector>
#include <string>
#include <typeindex>
#include <any>
// 事件基类(用于类型擦除)
struct EventBase {
virtual ~EventBase() = default;
};
// 具体事件类型
struct DeviceArrivalEvent : EventBase {
std::string devicePath;
uint16_t vendorId;
uint16_t productId;
};
struct DeviceRemovalEvent : EventBase {
std::string devicePath;
};
struct DeviceDataEvent : EventBase {
std::string devicePath;
std::vector<uint8_t> data;
};
// 类型安全事件总线
class EventBus {
public:
// 订阅:返回 token,用于取消订阅
template<typename EventT>
int subscribe(std::function<void(const EventT&)> handler) {
auto key = std::type_index(typeid(EventT));
int token = nextToken_++;
handlers_[key].push_back({
token,
[handler](const EventBase& e) {
handler(static_cast<const EventT&>(e));
}
});
return token;
}
// 取消订阅
template<typename EventT>
void unsubscribe(int token) {
auto key = std::type_index(typeid(EventT));
auto it = handlers_.find(key);
if (it == handlers_.end()) return;
auto& vec = it->second;
vec.erase(
std::remove_if(vec.begin(), vec.end(),
[token](const auto& h) { return h.token == token; }),
vec.end()
);
}
// 发布事件
template<typename EventT>
void publish(const EventT& event) {
auto key = std::type_index(typeid(EventT));
auto it = handlers_.find(key);
if (it == handlers_.end()) return;
// 拷贝一份,防止回调中修改 handlers_
auto handlers = it->second;
for (const auto& h : handlers) {
h.fn(event);
}
}
private:
struct HandlerEntry {
int token;
std::function<void(const EventBase&)> fn;
};
std::unordered_map<std::type_index, std::vector<HandlerEntry>> handlers_;
int nextToken_ = 0;
};
// 全局事件总线(实际项目中可用依赖注入)
inline EventBus& getEventBus() {
static EventBus bus;
return bus;
}
使用:设备插拔事件广播
// 设备管理器:只负责发布事件,不知道谁在监听
class DeviceManager {
public:
void onHotplugArrival(const std::string& path, uint16_t vid, uint16_t pid) {
// 发布事件,其他组件自行处理
getEventBus().publish(DeviceArrivalEvent{path, vid, pid});
}
void onHotplugRemoval(const std::string& path) {
getEventBus().publish(DeviceRemovalEvent{path});
}
};
// UI 组件:订阅感兴趣的事件
class DeviceListView {
public:
DeviceListView() {
arrivalToken_ = getEventBus().subscribe<DeviceArrivalEvent>(
[this](const DeviceArrivalEvent& e) {
addDeviceItem(e.devicePath, e.vendorId, e.productId);
}
);
removalToken_ = getEventBus().subscribe<DeviceRemovalEvent>(
[this](const DeviceRemovalEvent& e) {
removeDeviceItem(e.devicePath);
}
);
}
~DeviceListView() {
// 析构时取消订阅,防止悬空回调
getEventBus().unsubscribe<DeviceArrivalEvent>(arrivalToken_);
getEventBus().unsubscribe<DeviceRemovalEvent>(removalToken_);
}
private:
int arrivalToken_;
int removalToken_;
void addDeviceItem(const std::string&, uint16_t, uint16_t) { /* ... */ }
void removeDeviceItem(const std::string&) { /* ... */ }
};
// 日志模块:独立订阅,DeviceManager 完全不知道它的存在
class DeviceLogger {
public:
DeviceLogger() {
token_ = getEventBus().subscribe<DeviceArrivalEvent>(
[](const DeviceArrivalEvent& e) {
printf("[LOG] Device arrived: %s (VID=%04X PID=%04X)\n",
e.devicePath.c_str(), e.vendorId, e.productId);
}
);
}
~DeviceLogger() {
getEventBus().unsubscribe<DeviceArrivalEvent>(token_);
}
private:
int token_;
};
与 Qt Signal/Slot 对比
| 特性 | Qt Signal/Slot | 本文事件总线 |
|---|---|---|
| 类型安全 | ✅(编译期) | ✅(模板) |
| 跨线程 | ✅(内置队列连接) | ❌(需额外处理) |
| 依赖 | Qt MOC 预处理器 | 纯标准库 |
| 运行时开销 | 中等(元对象系统) | 低(std::function) |
| 动态订阅/取消 | ✅ | ✅ |
| 适用场景 | Qt 应用 | 无 Qt 依赖的 C++ 项目 |
观察者模式的核心价值:发布者和订阅者互不依赖,可以独立演化、独立测试。增加新的监听者无需修改任何现有代码。
工厂模式:注册表式插件架构
问题场景
你的应用需要处理多种协议(HID、USB Bulk、Serial、BLE),每种协议对应不同的处理器类。最直观的写法是 if-else:
// ❌ 每次新增协议都要修改这里
std::unique_ptr<ProtocolHandler> createHandler(const std::string& type) {
if (type == "hid") return std::make_unique<HidHandler>();
if (type == "serial") return std::make_unique<SerialHandler>();
if (type == "ble") return std::make_unique<BleHandler>();
return nullptr;
}
随着协议类型增多,这个函数会无限膨胀,而且每次改动都要触碰核心代码。
注册表式工厂(自注册)
让每个处理器类自己注册到工厂,工厂完全不知道具体类型:
#include <memory>
#include <string>
#include <unordered_map>
#include <functional>
#include <stdexcept>
// 抽象基类
class ProtocolHandler {
public:
virtual ~ProtocolHandler() = default;
virtual bool open(const std::string& path) = 0;
virtual void close() = 0;
virtual std::vector<uint8_t> read() = 0;
virtual bool write(const std::vector<uint8_t>& data) = 0;
virtual std::string type() const = 0;
};
// 工厂注册表
class HandlerFactory {
public:
using Creator = std::function<std::unique_ptr<ProtocolHandler>()>;
static HandlerFactory& instance() {
static HandlerFactory factory;
return factory;
}
// 注册创建函数
void registerHandler(const std::string& type, Creator creator) {
registry_[type] = std::move(creator);
}
// 创建实例
std::unique_ptr<ProtocolHandler> create(const std::string& type) const {
auto it = registry_.find(type);
if (it == registry_.end()) {
throw std::runtime_error("Unknown handler type: " + type);
}
return it->second();
}
// 查询已注册类型
std::vector<std::string> registeredTypes() const {
std::vector<std::string> types;
for (const auto& [key, _] : registry_) types.push_back(key);
return types;
}
private:
std::unordered_map<std::string, Creator> registry_;
};
// 自注册辅助类模板
template<typename T>
class AutoRegister {
public:
explicit AutoRegister(const std::string& type) {
HandlerFactory::instance().registerHandler(
type,
[]() -> std::unique_ptr<ProtocolHandler> {
return std::make_unique<T>();
}
);
}
};
// 自注册宏(简化样板代码)
#define REGISTER_HANDLER(ClassName, TypeName) \
static AutoRegister<ClassName> _autoReg_##ClassName{TypeName}
具体处理器实现(自注册)
// hid_handler.cpp
class HidHandler : public ProtocolHandler {
public:
bool open(const std::string& path) override {
handle_ = CreateFileA(path.c_str(), GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
nullptr, OPEN_EXISTING, 0, nullptr);
return handle_ != INVALID_HANDLE_VALUE;
}
void close() override {
if (handle_ != INVALID_HANDLE_VALUE) {
CloseHandle(handle_);
handle_ = INVALID_HANDLE_VALUE;
}
}
std::vector<uint8_t> read() override {
std::vector<uint8_t> buf(65);
DWORD bytesRead = 0;
ReadFile(handle_, buf.data(), 65, &bytesRead, nullptr);
buf.resize(bytesRead);
return buf;
}
bool write(const std::vector<uint8_t>& data) override {
DWORD written = 0;
return WriteFile(handle_, data.data(), data.size(), &written, nullptr);
}
std::string type() const override { return "hid"; }
private:
HANDLE handle_ = INVALID_HANDLE_VALUE;
};
// 在 .cpp 文件全局作用域自动注册(程序启动时执行)
REGISTER_HANDLER(HidHandler, "hid");
// serial_handler.cpp(同理,自包含,不修改工厂)
class SerialHandler : public ProtocolHandler {
// ... 实现
std::string type() const override { return "serial"; }
};
REGISTER_HANDLER(SerialHandler, "serial");
使用工厂
// 调用方完全不依赖具体类型
void connectToDevice(const std::string& type, const std::string& path) {
auto handler = HandlerFactory::instance().create(type);
if (!handler->open(path)) {
throw std::runtime_error("Failed to open device");
}
// handler 是 unique_ptr,RAII 保证 close
}
// 动态加载配置(从 JSON/命令行读取类型字符串)
void loadDevicesFromConfig(const std::vector<DeviceConfig>& configs) {
for (const auto& cfg : configs) {
try {
auto handler = HandlerFactory::instance().create(cfg.type);
handler->open(cfg.path);
// ...
} catch (const std::exception& e) {
fprintf(stderr, "Failed to create handler for %s: %s\n",
cfg.type.c_str(), e.what());
}
}
}
C++17 if constexpr 与 std::variant 在工厂中的应用
工厂模式结合 std::variant 可以在编译期消除运行时分支,适合类型集合固定的场景:
#include <variant>
// 已知类型集合(编译期固定)
using HandlerVariant = std::variant<HidHandler, SerialHandler, BleHandler>;
// 根据枚举创建变体(编译期分派)
enum class HandlerType { Hid, Serial, Ble };
HandlerVariant createHandlerVariant(HandlerType type) {
switch (type) {
case HandlerType::Hid: return HidHandler{};
case HandlerType::Serial: return SerialHandler{};
case HandlerType::Ble: return BleHandler{};
}
std::unreachable(); // C++23,C++17 用 __builtin_unreachable()
}
// std::visit + if constexpr:针对不同类型执行不同逻辑
void processHandler(HandlerVariant& handler) {
std::visit([](auto& h) {
using T = std::decay_t<decltype(h)>;
if constexpr (std::is_same_v<T, HidHandler>) {
// HID 特有操作:设置报告描述符
h.setReportDescriptor();
} else if constexpr (std::is_same_v<T, BleHandler>) {
// BLE 特有操作:配对
h.startPairing();
} else {
// 通用操作
h.open("/dev/ttyUSB0");
}
}, handler);
}
variant 工厂 vs 注册表工厂的选择:
| 场景 | 推荐方案 |
|---|---|
| 类型集合固定,编译期已知 | std::variant + if constexpr |
| 类型集合动态扩展(插件系统) | 注册表工厂 |
| 需要运行时字符串映射(配置文件) | 注册表工厂 |
| 性能极敏感,无虚函数开销 | std::variant |
三种模式的协同
这三种模式在实际项目中往往配合使用:
工厂模式
└─ 创建具体的 ProtocolHandler(RAII 包装底层句柄)
└─ 发布事件到 EventBus(观察者)
├─ UI 订阅 → 刷新设备列表
├─ Logger 订阅 → 记录日志
└─ Monitor 订阅 → 更新状态机
- RAII 确保资源不泄漏,不论执行路径如何
- 观察者 解耦生产者和消费者,两侧可独立变化
- 工厂 隔离创建逻辑,调用方不依赖具体类型
这不是理论,是在 HID 设备管理、协议层抽象、UI 事件系统中被反复验证过的工程实践。