C++设计模式实战:单例、工厂与适配器在真实项目中的应用

发布时间:2026/7/25 6:16:23

C++设计模式实战:单例、工厂与适配器在真实项目中的应用 1. 项目概述从“玩具”到“实战”的设计模式演练在C开发这条路上我们常常会接触到各种设计模式。很多教程里的例子比如一个Logger单例、一个Shape工厂虽然能让你明白模式的结构但总感觉和真实项目隔着一层纱。它们像实验室里的“玩具”干净、简单但缺乏真实世界的“泥泞感”——那些复杂的依赖、性能的考量、线程的陷阱以及如何优雅地融入现有架构。最近我正好在重构一个老旧的音视频处理模块其中就密集地用到了单例、工厂和适配器模式。我决定把这次实战中的思考结合几个更贴近真实场景的Demo分享出来。这些例子不再是孤立的代码片段而是模拟了你在开发一个中小型C应用比如一个简易的媒体播放器后台、一个游戏引擎的子系统或一个网络服务框架时可能遇到的典型场景。如果你已经理解了模式的基本概念但困惑于如何将它们用得“地道”和“安全”那么接下来的内容应该能给你一些直接的参考。2. 单例模式实战一个线程安全的配置管理器单例模式大概是争议最大也最容易被误用的模式。我们需要的不是一个简单的static变量而是一个在多线程环境下安全、可延迟初始化、且便于进行资源管理和测试的配置中心。2.1 核心需求与设计选型在一个分布式日志采集系统或游戏引擎中全局配置如数据库连接串、日志级别、资源路径需要被各个模块频繁读取。这个配置管理器必须满足全局唯一访问点确保所有代码获取的配置来源一致。线程安全初始化防止多线程同时调用getInstance()时创建多个实例。延迟加载系统启动时可能不需要所有配置应避免不必要的开销。可测试性便于在单元测试中替换或重置配置。C11之后利用局部静态变量的线程安全特性Magic Static是实现单例最简洁、高效的方式它完美解决了双重检查锁定DCLP在旧标准下的复杂性和潜在风险。2.2 贴近实战的ConfigManager实现下面是一个模拟从文件加载JSON配置的ConfigManager#include iostream #include fstream #include string #include unordered_map #include mutex #include memory #include nlohmann/json.hpp // 假设使用 nlohmann/json 库 using json nlohmann::json; class ConfigManager { public: // 删除拷贝构造和赋值操作符确保唯一性 ConfigManager(const ConfigManager) delete; ConfigManager operator(const ConfigManager) delete; // 获取单例实例的全局访问点 static ConfigManager getInstance() { static ConfigManager instance; // C11保证此初始化是线程安全的 return instance; } // 从文件加载配置 bool loadConfig(const std::string filePath) { std::lock_guardstd::mutex lock(configMutex_); // 加载配置时需要加锁 try { std::ifstream file(filePath); if (!file.is_open()) { std::cerr 无法打开配置文件: filePath std::endl; return false; } json j; file j; configData_ j; // 假设configData_是json类型或已转换为内部结构 file.close(); std::cout 配置文件加载成功: filePath std::endl; return true; } catch (const json::exception e) { std::cerr JSON解析错误: e.what() std::endl; return false; } catch (...) { std::cerr 未知错误加载配置文件。 std::endl; return false; } } // 获取配置值模板函数支持不同类型 templatetypename T T getValue(const std::string key, const T defaultValue T{}) const { // 注意这里读取也需要加锁因为json不是线程安全的。 // 但为了性能如果配置加载后只读可以考虑使用读写锁或原子操作。 std::lock_guardstd::mutex lock(configMutex_); try { return configData_.value(key, defaultValue); } catch (...) { return defaultValue; } } // 仅供演示打印所有配置 void printAll() const { std::lock_guardstd::mutex lock(configMutex_); std::cout 当前配置: configData_.dump(2) std::endl; } private: // 私有构造函数防止外部构造 ConfigManager() { std::cout ConfigManager 实例创建。 std::endl; // 可以在这里初始化默认配置 configData_ json::object(); } // 私有析构函数 ~ConfigManager() { std::cout ConfigManager 实例销毁。 std::endl; } mutable std::mutex configMutex_; // mutable允许在const成员函数中加锁 json configData_; };使用示例int main() { // 获取单例引用 auto config ConfigManager::getInstance(); // 加载配置 if (!config.loadConfig(app_config.json)) { std::cerr 启动失败缺少配置文件。 std::endl; return -1; } // 从不同“模块”获取配置 int threadPoolSize config.getValueint(performance.thread_pool_size, 4); std::string logLevel config.getValuestd::string(logging.level, INFO); std::string dbHost config.getValuestd::string(database.host, localhost); std::cout 线程池大小: threadPoolSize std::endl; std::cout 日志级别: logLevel std::endl; std::cout 数据库主机: dbHost std::endl; // 打印全部配置 config.printAll(); return 0; }2.3 关键细节与避坑指南线程安全是核心示例中getInstance()的线程安全由C11标准保证。但注意configData_的读写getValue,loadConfig也需要用mutex保护因为nlohmann::json库本身不是线程安全的。如果配置在初始化后是只读的那么getValue可以不加锁以提升性能但loadConfig写操作必须独占。延迟加载与静态局部变量static ConfigManager instance;这行代码只有在第一次调用getInstance()时才会执行构造。这是最优雅的延迟初始化。防止拷贝和移动通过 delete显式删除拷贝构造和赋值操作符这是现代C防止单例被意外复制的标准做法。关于析构函数单例的析构顺序在程序结束时可能是个问题特别是当其他全局/静态对象在析构时还试图访问单例。因此单例中应避免持有需要复杂清理的资源如网络连接或者确保其生命周期足够长。可测试性的考虑硬编码的单例会阻碍单元测试。一个改进方案是引入一个IConfigManager接口让ConfigManager实现它。在生产代码中使用单例而在测试中你可以通过依赖注入的方式提供一个MockConfigManager。这需要一些框架设计上的配合。实操心得在真实项目中我倾向于使用“单例接口依赖注入容器”的方式来替代传统的静态getInstance。这样既保持了全局访问的便利性又为测试和灵活性留了后路。但对于很多中小型项目或模块内部工具类上述基于Magic Static的单例实现已经足够稳健和高效。3. 简单工厂模式进阶可扩展的压缩算法工厂简单工厂模式并不属于GoF的23种设计模式但它非常实用常用于创建一系列具有共同接口但实现不同的对象。一个典型的“玩具”例子是创建不同的Shape。我们来做一个更贴近实际的一个支持多种压缩算法如ZIP GZIP 自定义LZ4的压缩器工厂。3.1 场景分析与设计思路假设我们在开发一个文件备份工具或网络传输模块需要根据文件类型、目标平台或用户选择来动态采用不同的压缩算法。每种算法都是一个独立的类但它们都遵循相同的压缩/解压接口。简单工厂的核心价值在于将对象的创建逻辑集中管理客户端无需关心具体的实现类。设计要点抽象产品ICompressor接口定义compress和decompress纯虚函数。具体产品ZipCompressorGzipCompressorLz4Compressor等。工厂类CompressorFactory根据传入的枚举类型或字符串标识符创建对应的压缩器实例。3.2 支持注册机制的动态工厂实现基础简单工厂使用switch-case或if-else但添加新算法需要修改工厂类违反了开闭原则。我们可以实现一个支持运行时注册的“准工厂”提高扩展性。#include iostream #include memory #include unordered_map #include string #include functional // 1. 抽象产品类 class ICompressor { public: virtual ~ICompressor() default; virtual std::string compress(const std::string data) 0; virtual std::string decompress(const std::string compressedData) 0; virtual std::string getName() const 0; }; // 2. 具体产品类 class ZipCompressor : public ICompressor { public: std::string compress(const std::string data) override { // 模拟ZIP压缩逻辑 std::cout [ZipCompressor] 正在压缩 data.size() 字节的数据... std::endl; return ZIP_COMPRESSED( data ); } std::string decompress(const std::string compressedData) override { std::cout [ZipCompressor] 正在解压数据... std::endl; // 模拟解压逻辑移除假想的前缀 if (compressedData.find(ZIP_COMPRESSED() 0) { return compressedData.substr(15, compressedData.size() - 16); // 去掉头尾 } return compressedData; } std::string getName() const override { return ZIP; } }; class GzipCompressor : public ICompressor { public: std::string compress(const std::string data) override { std::cout [GzipCompressor] 使用GZIP算法压缩... std::endl; return GZIP_COMPRESSED( data ); } std::string decompress(const std::string compressedData) override { std::cout [GzipCompressor] 使用GZIP算法解压... std::endl; if (compressedData.find(GZIP_COMPRESSED() 0) { return compressedData.substr(16, compressedData.size() - 17); } return compressedData; } std::string getName() const override { return GZIP; } }; // 3. 可注册的工厂类 class CompressorFactory { public: using CreatorFunc std::functionstd::unique_ptrICompressor(); // 获取工厂单例工厂本身也可以是单例 static CompressorFactory getInstance() { static CompressorFactory instance; return instance; } // 注册创建器 bool registerCompressor(const std::string compressorType, CreatorFunc creator) { if (creatorRegistry_.find(compressorType) ! creatorRegistry_.end()) { std::cerr 压缩器类型 compressorType 已注册 std::endl; return false; } creatorRegistry_[compressorType] std::move(creator); std::cout 成功注册压缩器: compressorType std::endl; return true; } // 创建压缩器 std::unique_ptrICompressor createCompressor(const std::string compressorType) { auto it creatorRegistry_.find(compressorType); if (it creatorRegistry_.end()) { std::cerr 未知的压缩器类型: compressorType std::endl; // 可以返回一个默认的压缩器或nullptr return nullptr; } return it-second(); // 调用注册的创建函数 } // 列出所有支持的压缩器 void listSupported() const { std::cout 支持的压缩器类型: ; for (const auto pair : creatorRegistry_) { std::cout pair.first ; } std::cout std::endl; } private: CompressorFactory() { // 在构造函数中注册内置的压缩器 registerCompressor(ZIP, []() - std::unique_ptrICompressor { return std::make_uniqueZipCompressor(); }); registerCompressor(GZIP, []() - std::unique_ptrICompressor { return std::make_uniqueGzipCompressor(); }); } ~CompressorFactory() default; std::unordered_mapstd::string, CreatorFunc creatorRegistry_; }; // 4. 一个第三方或后续新增的压缩器 class Lz4Compressor : public ICompressor { public: std::string compress(const std::string data) override { std::cout [Lz4Compressor] 高速LZ4压缩中... std::endl; return LZ4( data ); } std::string decompress(const std::string compressedData) override { std::cout [Lz4Compressor] 高速LZ4解压中... std::endl; if (compressedData.find(LZ4() 0) { return compressedData.substr(4, compressedData.size() - 5); } return compressedData; } std::string getName() const override { return LZ4; } };使用示例int main() { auto factory CompressorFactory::getInstance(); factory.listSupported(); // 动态创建压缩器 std::string data 这是一段需要被压缩的原始数据。; auto zipCompressor factory.createCompressor(ZIP); if (zipCompressor) { auto compressed zipCompressor-compress(data); auto decompressed zipCompressor-decompress(compressed); std::cout ZIP解压后数据: decompressed std::endl; } auto gzipCompressor factory.createCompressor(GZIP); if (gzipCompressor) { auto compressed gzipCompressor-compress(data); auto decompressed gzipCompressor-decompress(compressed); std::cout GZIP解压后数据: decompressed std::endl; } // 动态注册一个新的压缩器例如在插件加载时 bool registered factory.registerCompressor(LZ4, []() - std::unique_ptrICompressor { return std::make_uniqueLz4Compressor(); }); if (registered) { auto lz4Compressor factory.createCompressor(LZ4); if (lz4Compressor) { auto compressed lz4Compressor-compress(data); auto decompressed lz4Compressor-decompress(compressed); std::cout LZ4解压后数据: decompressed std::endl; } } factory.listSupported(); // 现在应该包含LZ4 return 0; }3.3 模式变体与经验总结从“简单”到“可扩展”基础的简单工厂模式在createCompressor函数里用switch判断compressorType。而上述“注册表”模式将创建逻辑分散到各个注册调用中新增算法时无需修改工厂类只需调用registerCompressor即可。这更符合开闭原则。工厂方法的结合当对象创建逻辑非常复杂或者我们希望将产品的创建延迟到子类时简单工厂会演变为工厂方法模式每个具体产品对应一个具体工厂。简单工厂更适合创建逻辑相对固定、类型已知且不多的场景。返回智能指针工厂返回std::unique_ptrICompressor明确表达了所有权转移避免了手动管理内存和内存泄漏的风险这是现代C的推荐做法。错误处理工厂方法应能处理未知类型请求。示例中返回了nullptr客户端代码必须检查。也可以选择抛出一个标准异常如std::invalid_argument或者返回一个默认的“空对象”Null Object。注意事项这个“可注册工厂”本身也使用了单例模式来管理全局的注册表。在动态库插件加载的场景下需要注意静态初始化顺序问题Static Initialization Order Fiasco。一个常见的解决方案是使用“显式初始化”函数在插件加载后主动调用注册函数而不是依赖静态变量的初始化。4. 适配器模式实战集成遗留的第三方SDK适配器模式Adapter常用于解决接口不兼容的问题。教科书例子是把一个方形钉子适配到圆孔里。现实中更常见的场景是你需要集成一个老旧的、接口设计不佳的第三方库或遗留代码到你的新系统中。4.1 真实场景统一日志接口假设你的新系统定义了一个简洁的日志接口IModernLogger但你必须使用一个陈旧的第三方日志库LegacyLogger它的函数名古怪、参数顺序反人类而且不支持你的日志级别枚举。目标接口你的系统标准// 现代日志接口 class IModernLogger { public: virtual ~IModernLogger() default; enum class Level { DEBUG, INFO, WARN, ERROR }; virtual void log(Level level, const std::string message) 0; };需要适配的遗留类你无法修改的第三方库// 陈旧的第三方日志库假设是C风格接口或一个设计很差的类 class LegacyLogger { public: // 函数名奇怪参数顺序是(消息 严重程度数值)且级别定义不同 void recordLog(const char* msg, int severityCode) { std::cout [LegacyLogger] Severity( severityCode ): msg std::endl; } // 可能还有其他不一致的方法... };4.2 对象适配器与类适配器的选择与实现适配器模式有两种主要形式对象适配器使用组合和类适配器使用继承在C中需要多重继承。在C中对象适配器更灵活、更常用因为它不依赖于继承被适配者可以适配一个类及其所有子类。对象适配器实现#include iostream #include string // 适配器类继承目标接口组合被适配者 class LegacyLoggerAdapter : public IModernLogger { public: // 构造函数注入被适配的遗留对象可以是引用或指针这里用指针更灵活 LegacyLoggerAdapter(LegacyLogger* legacyLogger) : legacyLogger_(legacyLogger) { if (!legacyLogger_) { throw std::invalid_argument(LegacyLogger pointer cannot be null); } } void log(Level level, const std::string message) override { // 适配逻辑将现代接口的参数转换为遗留接口所需的格式 int severityCode convertLevelToSeverity(level); legacyLogger_-recordLog(message.c_str(), severityCode); } private: LegacyLogger* legacyLogger_; // 持有被适配对象的指针 // 转换函数将现代枚举映射到遗留的严重程度代码 int convertLevelToSeverity(Level level) const { switch (level) { case Level::DEBUG: return 1; // 假设遗留库中1代表调试 case Level::INFO: return 2; // 2代表信息 case Level::WARN: return 3; // 3代表警告 case Level::ERROR: return 4; // 4代表错误 default: return 2; // 默认信息级别 } } };使用示例int main() { // 遗留系统的日志对象 LegacyLogger oldLogger; // 创建适配器将旧日志器适配到新接口 LegacyLoggerAdapter adapter(oldLogger); // 现在可以使用现代接口来记录日志了 IModernLogger* logger adapter; logger-log(IModernLogger::Level::INFO, 系统启动成功。); logger-log(IModernLogger::Level::WARN, 磁盘空间不足。); logger-log(IModernLogger::Level::ERROR, 数据库连接失败); // 你的系统其他部分只依赖于IModernLogger接口完全感知不到LegacyLogger的存在 return 0; }4.3 处理更复杂的适配场景现实中的适配可能更复杂接口粒度不匹配IModernLogger可能有一个logWithContext(Level, const string msg, const LogContext ctx)方法而LegacyLogger只有recordLog。适配器需要在logWithContext内部将ctx的信息拼接到msg字符串中或者调用多次recordLog。适配多个源你可能需要同时适配LegacyLogger和另一个AnotherOldLogger并提供一个统一的接口。这时适配器内部可能需要根据配置或消息类型决定使用哪一个被适配对象或者实现一个“多重适配器”。C风格函数适配如果遗留库是C风格的函数如void legacy_log(int code, const char* fmt, ...)适配器类需要处理可变参数列表将其转换为字符串后再调用。// 示例适配一个C风格的可变参数日志函数 extern C { void legacy_log(int severity, const char* format, ...); } class CVarargsLoggerAdapter : public IModernLogger { public: void log(Level level, const std::string message) override { int sev convertLevel(level); // 这里需要将std::string转换为C风格字符串并调用 // 注意legacy_log是可变参数的我们这里简单适配假设它内部处理了格式化 // 更复杂的适配可能需要自己解析格式字符串这里只是一个演示 legacy_log(sev, %s, message.c_str()); } private: int convertLevel(Level level) { /* ... */ } };4.4 适配器模式的边界与思考何时使用当你想使用一个已有的类但其接口与你系统的其他部分不兼容时或者当你需要创建一个可复用的类该类需要与多个不兼容的接口协作时。与外观模式Facade的区别外观模式是为复杂的子系统提供一个简化的统一接口它通常涉及多个类。而适配器模式主要是解决两个已有接口之间的不兼容问题通常只涉及一个被适配者。适配器是“事后补救”外观是“事前设计”。性能开销适配器层会引入一个微小的间接调用开销。在性能极其敏感的代码路径上需要评估。但对于日志、IO等操作这点开销通常可以忽略不计。保持适配器单一职责一个适配器最好只做一件事——转换接口。不要在其中添加业务逻辑。如果转换逻辑非常复杂考虑将其抽离到独立的“转换器”Converter类中。踩坑实录我曾经遇到一个第三方SDK它的初始化函数init()必须在主线程调用且调用后会产生一个全局回调。而我们的系统是事件驱动的。直接调用会阻塞事件循环。最终的解决方案是设计了一个SDKThreadAdapter它继承自我们的IEventEmitter接口内部封装了SDK对象和一个专用线程。适配器的init()方法将任务派发到专用线程执行并通过信号/槽或回调将SDK的异步事件转换并转发到主线程的事件系统中。这已经超出了简单的接口转换涉及到了线程边界的适配是适配器模式一个更高级的应用。5. 模式联合作战一个简易插件系统的设计草图在实际项目中这些模式很少孤立存在。让我们构思一个简单的插件系统看看它们如何协同工作单例模式用于管理全局的PluginManager它掌握所有已加载插件的信息。简单工厂模式PluginManager内部可能有一个工厂根据插件类型如“音频过滤器”、“视频解码器”创建统一的插件接口IPlugin的具体实例。这个工厂可以是可注册的以支持动态插件。适配器模式如果某个插件是用C语言写的或者接口与我们的IPlugin不匹配我们就需要为它编写一个适配器类。这个系统的大致框架如下// 目标插件接口 class IPlugin { public: virtual ~IPlugin() default; virtual std::string getName() const 0; virtual void initialize() 0; virtual void execute() 0; }; // 单例的插件管理器 class PluginManager { public: static PluginManager getInstance(); bool loadPlugin(const std::string path); std::unique_ptrIPlugin createPlugin(const std::string type); // ... 其他管理函数 private: std::unordered_mapstd::string, std::functionstd::unique_ptrIPlugin() pluginFactoryRegistry_; // ... }; // 假设有一个遗留的C语言插件 extern C { typedef struct legacy_plugin_t legacy_plugin_t; legacy_plugin_t* legacy_plugin_create(); void legacy_plugin_do_work(legacy_plugin_t*); void legacy_plugin_destroy(legacy_plugin_t*); } // 适配器将C插件适配到IPlugin接口 class LegacyPluginAdapter : public IPlugin { legacy_plugin_t* legacyPlugin_; public: LegacyPluginAdapter() : legacyPlugin_(legacy_plugin_create()) {} ~LegacyPluginAdapter() override { legacy_plugin_destroy(legacyPlugin_); } std::string getName() const override { return LegacyPlugin (Adapted); } void initialize() override { /* C插件可能不需要显式初始化 */ } void execute() override { legacy_plugin_do_work(legacyPlugin_); } }; // 在PluginManager的初始化中注册适配器 PluginManager::getInstance().registerPluginFactory(LEGACY, []() { return std::make_uniqueLegacyPluginAdapter(); });通过这样的组合系统核心部分只与稳定的IPlugin接口交互单例的PluginManager负责生命周期工厂负责创建适配器负责兼容。这使得系统核心非常稳定而将变化新增插件类型、集成老旧代码隔离在了边缘模块中。设计模式的价值不在于生搬硬套而在于理解其背后“封装变化”、“面向接口编程”、“松耦合”的思想。当你面对一个具体的设计难题时这些模式就像是工具箱里的标准件能帮你更快地构建出清晰、健壮、易于维护的代码结构。希望这三个贴近实战的Demo能让你下次在代码中应用单例、工厂和适配器时更加得心应手。

相关新闻