行业资讯
C++设计模式实战:单例、工厂、观察者与策略模式详解
1. 项目概述为什么C开发者绕不开设计模式如果你用C写过一些项目尤其是规模稍大、需要团队协作或者需要长期维护的代码大概率会遇到这样的场景代码越写越乱新加一个功能要改好几个地方牵一发而动全身或者想复用一段逻辑却发现它和当前业务耦合得太紧根本抽不出来。这时候你需要的可能不仅仅是多写几个类或者函数而是一套组织代码的“兵法”——这就是设计模式。设计模式不是具体的代码而是针对软件设计中反复出现的各种问题所提出的通用、可复用的解决方案模板。它描述的是“在什么场景下用什么样的结构去组织你的类和对象能更优雅地解决问题”。对于C这种兼具高性能和灵活性的语言来说理解并应用设计模式尤为重要。C没有像Java或C#那样庞大的标准库和框架提供现成的模式实现很多高级特性如多态、模板又给了开发者极大的自由用得好是利器用不好就是灾难。设计模式恰恰能帮你用好这些特性构建出松耦合、高内聚、易扩展的代码结构。我见过不少C项目初期追求极致的性能大量使用原始指针和宏忽视了结构设计。结果到了中期代码成了一团乱麻没人敢动性能优化也无从下手最终项目陷入僵局。学习设计模式就是学习如何“有预谋”地写代码避免这种技术债的过早积累。接下来我会结合C的特性带你深入几个最常用、最核心的模式看看它们是如何在具体场景中发挥威力的。2. 核心模式解析从原理到C实现设计模式通常被分为创建型、结构型和行为型三大类。对于C开发者我们不必一开始就追求23种模式全部精通而是应该优先掌握那些与C特性结合紧密、能解决实际工程痛点的模式。下面我挑选几个最具代表性的拆解其核心思想、C实现要点以及典型误用场景。2.1 单例模式全局访问点的利与弊单例模式可能是被滥用得最严重的模式没有之一。它的意图很简单保证一个类只有一个实例并提供一个全局访问点。在C中这常用于管理全局资源如配置管理器、日志系统、线程池等。C实现的经典挑战与演进早期的C单例实现很多人会写一个全局静态变量。但这有缺陷静态变量的初始化顺序在C标准中是未定义的如果其他全局对象的构造函数依赖这个单例就可能访问到一个未初始化的实例导致崩溃。// 线程安全的懒汉式单例C11后 class Logger { public: static Logger getInstance() { static Logger instance; // C11保证此处的静态局部变量初始化是线程安全的 return instance; } void log(const std::string message) { // 日志实现 std::cout [LOG] message std::endl; } // 删除拷贝构造和赋值操作符确保唯一性 Logger(const Logger) delete; Logger operator(const Logger) delete; private: Logger() default; // 构造函数私有化 ~Logger() default; };实现要点与避坑指南线程安全在C11之前实现线程安全的懒汉单例非常繁琐需要双检锁等技巧。C11之后函数内的静态局部变量初始化是线程安全的这是最简单、最推荐的方式。防止拷贝必须将拷贝构造函数和赋值运算符声明为delete或私有否则通过拷贝可能创建出“第二个”实例违背单例初衷。析构顺序单例对象在程序结束时析构。如果其他全局或静态对象在析构时还调用了单例可能会访问已析构的对象。这是一个棘手的问题通常需要仔细设计依赖关系或者让单例不持有需要复杂清理的资源。注意单例模式本质上是披着面向对象外衣的全局变量。它引入了隐式的耦合让单元测试变得困难因为你很难替换这个全局实例。在现代C设计中更推荐通过依赖注入的方式将“单例”作为服务显式地传递给需要它的模块而不是让它随处可访问。2.2 工厂模式与抽象工厂解耦对象创建当你的代码中充斥着new ConcreteClass()时创建逻辑就和具体类名紧耦合了。工厂模式的核心思想是将对象的创建过程封装起来让客户端代码不依赖于具体的类。简单工厂模式一个工厂类根据传入的参数创建不同的产品对象。这其实不是一个标准的设计模式更像一种编程习惯。class Button { /* ... */ }; class WindowsButton : public Button { /* ... */ }; class MacButton : public Button { /* ... */ }; class ButtonFactory { public: static std::unique_ptrButton createButton(const std::string osType) { if (osType Windows) { return std::make_uniqueWindowsButton(); } else if (osType Mac) { return std::make_uniqueMacButton(); } return nullptr; } }; // 使用 auto btn ButtonFactory::createButton(Windows);工厂方法模式定义一个创建对象的接口但让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类。class Dialog { public: virtual ~Dialog() default; virtual std::unique_ptrButton createButton() 0; // 工厂方法 void render() { auto button createButton(); // 调用工厂方法 button-onClick(); button-render(); } }; class WindowsDialog : public Dialog { public: std::unique_ptrButton createButton() override { return std::make_uniqueWindowsButton(); } };抽象工厂模式提供一个接口用于创建一系列相关或依赖的对象而无需指定它们具体的类。比如要创建一套跨平台的UI组件按钮、复选框、文本框。// 抽象工厂 class GUIFactory { public: virtual ~GUIFactory() default; virtual std::unique_ptrButton createButton() 0; virtual std::unique_ptrCheckbox createCheckbox() 0; }; // 具体工厂 class WindowsFactory : public GUIFactory { public: std::unique_ptrButton createButton() override { return std::make_uniqueWindowsButton(); } std::unique_ptrCheckbox createCheckbox() override { return std::make_uniqueWindowsCheckbox(); } }; class Application { std::unique_ptrGUIFactory factory_; std::unique_ptrButton button_; public: Application(std::unique_ptrGUIFactory factory) : factory_(std::move(factory)) { button_ factory_-createButton(); } void paint() { button_-render(); } };C实现心得智能指针管理所有权工厂模式返回的对象其生命周期管理是关键。使用std::unique_ptr明确所有权转移使用std::shared_ptr表示共享所有权能有效避免内存泄漏。何时选择抽象工厂如果你的系统需要创建的是产品族多个互有关联的产品并且有切换整个产品族的需求如从Windows风格切换到Mac风格抽象工厂是首选。如果只是创建单一产品工厂方法更简洁。与模板的结合在C中可以利用模板实现编译期多态的“工厂”这有时被称为“策略模式”或“特性类”Traits性能更高但灵活性稍差。2.3 观察者模式实现松耦合的事件通知观察者模式定义了对象间的一种一对多的依赖关系当一个对象主题的状态发生改变时所有依赖于它的对象观察者都会得到通知并自动更新。这在GUI事件处理、消息总线、数据监控等场景中无处不在。经典的C实现与陷阱一个朴素的实现是主题持有观察者的指针列表在状态变化时遍历列表调用观察者的更新方法。这里最大的陷阱是生命周期管理。class Observer { public: virtual ~Observer() default; virtual void update(const std::string message) 0; }; class Subject { std::vectorObserver* observers_; // 原始指针危险 public: void attach(Observer* obs) { observers_.push_back(obs); } void detach(Observer* obs) { observers_.erase(std::remove(observers_.begin(), observers_.end(), obs), observers_.end()); } void notify(const std::string msg) { for (auto obs : observers_) { obs-update(msg); // 如果obs已被销毁这里就是未定义行为 } } };改进方案使用弱指针打破循环引用如果观察者也需要引用主题直接用shared_ptr会造成循环引用内存无法释放。std::weak_ptr是解决这个问题的标准工具。class Subject : public std::enable_shared_from_thisSubject { std::vectorstd::weak_ptrObserver observers_; std::mutex mtx_; // 多线程环境下需要锁 public: void attach(std::weak_ptrObserver obs) { std::lock_guardstd::mutex lock(mtx_); observers_.push_back(obs); } void notify(const std::string msg) { std::lock_guardstd::mutex lock(mtx_); auto it observers_.begin(); while (it ! observers_.end()) { if (auto sp it-lock()) { // 尝试提升为shared_ptr sp-update(msg); it; } else { // 观察者对象已不存在移除无效的弱引用 it observers_.erase(it); } } } };实操技巧线程安全在attach、detach和notify方法中加锁是必须的因为观察者列表可能被多个线程同时修改。性能考量如果通知非常频繁且观察者数量众多遍历列表和虚函数调用可能成为瓶颈。可以考虑使用无锁队列、将事件打包批量处理或者对于性能极度敏感的场景使用基于函数对象std::function的回调列表减少虚函数开销。现代C的替代方案signals2库Boost或独立的实现提供了类型安全、线程安全的信号/槽机制是观察者模式的一个强大实现可以直接用于生产环境。2.4 策略模式用组合替代继承策略模式定义了一系列算法将每个算法封装起来并使它们可以互相替换。策略模式让算法的变化独立于使用算法的客户。这完美体现了“组合优于继承”的原则。一个简单的例子排序策略假设我们有一个数据处理器它需要对数据进行排序但排序算法可能根据数据特征动态选择快速排序、归并排序、冒泡排序。// 策略接口 class SortStrategy { public: virtual ~SortStrategy() default; virtual void sort(std::vectorint data) 0; }; // 具体策略 class QuickSort : public SortStrategy { public: void sort(std::vectorint data) override { std::sort(data.begin(), data.end()); // 实际应实现快排 std::cout Using QuickSort\n; } }; class BubbleSort : public SortStrategy { public: void sort(std::vectorint data) override { // 实现冒泡排序 std::cout Using BubbleSort (for small data)\n; } }; // 上下文 class DataProcessor { std::unique_ptrSortStrategy strategy_; public: void setStrategy(std::unique_ptrSortStrategy strategy) { strategy_ std::move(strategy); } void processData(std::vectorint data) { if (strategy_) { strategy_-sort(data); } // ... 其他处理 } }; // 使用 DataProcessor processor; processor.setStrategy(std::make_uniqueQuickSort()); std::vectorint myData {5, 2, 8, 1}; processor.processData(myData);C中的高级玩法使用std::function和模板在C中策略模式不一定需要继承体系。利用std::function任何可调用对象函数、lambda表达式、函数对象都可以作为策略更加灵活。class DataProcessor { using SortStrategy std::functionvoid(std::vectorint); SortStrategy strategy_ [](std::vectorint data) { std::sort(data.begin(), data.end()); }; // 默认策略 public: void setStrategy(SortStrategy strategy) { strategy_ std::move(strategy); } void processData(std::vectorint data) { strategy_(data); } }; // 使用lambda自定义策略 processor.setStrategy([](std::vectorint data) { std::sort(data.begin(), data.end(), std::greater()); std::cout Sorted in descending order.\n; });经验之谈策略模式是消除冗长的if-else或switch语句的利器。当你发现代码中有一组条件分支每个分支只是执行不同的算法或行为时就应该考虑策略模式。它让代码更清晰新增一种策略只需添加一个新类或新函数符合开闭原则。3. 设计模式在C项目中的典型应用场景理解了模式的原理和实现我们来看看它们如何在真实的C项目中大显身手。模式不是生搬硬套的而是为了解决特定问题自然浮现的解决方案。3.1 游戏开发状态模式与组件模式游戏是设计模式的天然试验场。以角色状态管理为例一个游戏角色可能有 idle待机、walk行走、run奔跑、jump跳跃、attack攻击等状态。用一堆布尔标志和复杂的条件判断来管理这些状态代码很快就会变得难以维护。状态模式的应用状态模式允许一个对象在其内部状态改变时改变它的行为对象看起来好像修改了它的类。我们将每个状态封装成一个类。class Character; class State { public: virtual ~State() default; virtual void enter(Character* owner) 0; virtual void execute(Character* owner, float deltaTime) 0; virtual void exit(Character* owner) 0; }; class IdleState : public State { /* 实现待机逻辑 */ }; class WalkState : public State { /* 实现行走逻辑 */ }; class Character { std::unique_ptrState currentState_; public: void changeState(std::unique_ptrState newState) { if (currentState_) currentState_-exit(this); currentState_ std::move(newState); if (currentState_) currentState_-enter(this); } void update(float deltaTime) { if (currentState_) currentState_-execute(this, deltaTime); } };这样每个状态的逻辑被隔离在自己的类中新增一个状态比如CrouchState蹲下非常简单不会影响到其他状态的代码。状态之间的转换规则也可以清晰地管理。组件模式组合模式的一种应用现代游戏引擎如Unity、Unreal广泛使用组件模式Entity-Component-System, ECS的雏形。一个游戏对象Entity不是通过复杂的继承树来定义而是由一系列组件Component组合而成。class Component { public: virtual ~Component() default; virtual void update(float deltaTime) {} // ... 其他通用接口 }; class TransformComponent : public Component { /* 处理位置、旋转、缩放 */ }; class RenderComponent : public Component { /* 处理渲染 */ }; class PhysicsComponent : public Component { /* 处理物理 */ }; class GameObject { std::vectorstd::unique_ptrComponent components_; public: templatetypename T, typename... Args T* addComponent(Args... args) { auto comp std::make_uniqueT(std::forwardArgs(args)...); T* rawPtr comp.get(); components_.push_back(std::move(comp)); return rawPtr; } templatetypename T T* getComponent() { for (auto comp : components_) { if (auto ptr dynamic_castT*(comp.get())) { return ptr; } } return nullptr; } void updateAll(float deltaTime) { for (auto comp : components_) { comp-update(deltaTime); } } };这种设计提供了极大的灵活性。你可以通过组合不同的组件来创建任意复杂的游戏对象而无需修改已有的类结构。这也是“组合优于继承”的极致体现。3.2 基础设施与中间件代理模式与适配器模式在开发网络库、数据库驱动等基础设施时代理模式和适配器模式非常常见。代理模式控制访问增强功能代理模式为另一个对象提供一个替身或占位符以控制对这个对象的访问。远程代理、虚拟代理、保护代理、智能引用代理等都是其变体。智能指针作为代理std::unique_ptr和std::shared_ptr本身就是代理模式的体现。它们代理了原始指针自动管理生命周期shared_ptr还提供了引用计数。懒加载代理虚拟代理如果一个对象创建开销很大可以用代理来延迟其创建。class ExpensiveObject { /* 创建很耗时 */ }; class ExpensiveObjectProxy { std::unique_ptrExpensiveObject realObject_; public: void operation() { if (!realObject_) { realObject_ std::make_uniqueExpensiveObject(); // 第一次访问时才创建 } realObject_-doSomething(); } };保护代理控制对真实对象的访问权限。可以在代理的接口中进行权限校验。适配器模式让不兼容的接口协同工作当你需要使用一个已有的类但其接口不符合你的需求时适配器模式就派上用场了。在C中这通常有两种实现方式类适配器通过多重继承和对象适配器通过组合。// 假设我们有一个旧的、接口不符合我们需求的日志库 class LegacyLogger { public: void writeToLog(const std::string msg, int priority) { // 旧的写入方式 } }; // 我们期望的新日志接口 class NewLoggerInterface { public: virtual ~NewLoggerInterface() default; virtual void logInfo(const std::string msg) 0; virtual void logError(const std::string msg) 0; }; // 对象适配器通过组合LegacyLogger来实现NewLoggerInterface class LoggerAdapter : public NewLoggerInterface { LegacyLogger legacyLogger_; public: void logInfo(const std::string msg) override { legacyLogger_.writeToLog(msg, 1); // 将新接口映射到旧接口 } void logError(const std::string msg) override { legacyLogger_.writeToLog(msg, 3); } };在集成第三方库、升级系统部分模块时适配器模式能极大地减少代码修改量保持系统的稳定性。3.3 高性能计算与模板元编程策略模式与策略模式的编译期变体在高性能计算领域我们经常需要为不同的硬件架构CPU/GPU、不同的数据类型float/double或不同的算法选择最优的实现。策略模式在这里同样适用并且可以借助C的模板将策略的选择从运行时提前到编译期实现零开销抽象。编译期策略模式基于模板// 策略作为模板参数 templatetypename SortingStrategy class Sorter { public: void sort(std::vectorint data) { SortingStrategy::sort(data); } }; // 具体策略作为静态类或包含静态函数的类 struct QuickSortPolicy { static void sort(std::vectorint data) { // 快速排序实现可以是高度优化的、针对特定CPU指令集的代码 } }; struct BubbleSortPolicy { static void sort(std::vectorint data) { // 冒泡排序 } }; // 使用 SorterQuickSortPolicy fastSorter; fastSorter.sort(myData); // 编译期就确定了使用快速排序无运行时多态开销标签分发与特性Traits这是策略模式的另一种高级应用常用于标准库和Boost库中。通过定义类型的“特性”在编译期分派到不同的实现。// 定义一个特性类用于判断类型是否有trivial析构函数 templatetypename T struct has_trivial_destructor { static constexpr bool value std::is_trivially_destructible_vT; }; // 根据特性选择不同的销毁算法 templatetypename T void destroy_impl(T* ptr, std::true_type /* has trivial destructor */) { // 对于平凡析构类型什么也不做 } templatetypename T void destroy_impl(T* ptr, std::false_type /* has non-trivial destructor */) { ptr-~T(); // 需要显式调用析构函数 } templatetypename T void my_destroy(T* ptr) { destroy_impl(ptr, std::integral_constantbool, has_trivial_destructorT::value{}); }这种方式将策略的选择完全交给了编译器生成的代码是高度特化的性能最优。这是C区别于其他语言、能够实现“零成本抽象”的关键技术之一。4. C实现设计模式的常见陷阱与最佳实践知道了怎么用更要知道怎么用好、用对。在C的语境下实现设计模式有一些特有的坑需要避开。4.1 内存管理与所有权语义这是C模式实现中最容易出错的地方。错误的内存管理会导致内存泄漏、悬空指针、双重释放等问题。明确所有权在设计模式中搞清楚“谁创建、谁持有、谁销毁”至关重要。对于工厂模式创建的对象使用std::unique_ptr明确表达“工厂转移所有权给调用者”。对于观察者模式中的观察者列表使用std::weak_ptr避免循环引用。Rule of Three/Five/Zero如果一个类需要自定义析构函数、拷贝构造函数或拷贝赋值运算符那么它很可能也需要定义其他几个Rule of Three。在C11后还要考虑移动构造函数和移动赋值运算符Rule of Five。最好的实践是遵循Rule of Zero让类本身不直接管理资源如原始指针而是依赖具有值语义的成员如std::vector,std::unique_ptr让编译器生成正确的默认行为。示例一个违反Rule of Zero的糟糕单例class BadSingleton { static BadSingleton* instance; int* someResource; // 原始指针 BadSingleton() { someResource new int(42); } public: static BadSingleton* getInstance() { if (!instance) instance new BadSingleton(); return instance; } ~BadSingleton() { delete someResource; } // 只有析构没有处理拷贝和赋值 // 缺失拷贝构造和拷贝赋值如果被拷贝会导致双重delete };改进后class GoodSingleton { std::unique_ptrint someResource; // 使用智能指针 GoodSingleton() : someResource(std::make_uniqueint(42)) {} public: static GoodSingleton getInstance() { static GoodSingleton instance; return instance; } // 不需要显式定义析构、拷贝构造等编译器生成的默认行为就是正确的删除拷贝 GoodSingleton(const GoodSingleton) delete; GoodSingleton operator(const GoodSingleton) delete; };4.2 多线程环境下的安全性很多设计模式的原型描述是单线程的。但在现代C多线程程序中必须考虑线程安全。单例的线程安全如前所述C11后使用局部静态变量是最简单的线程安全实现。观察者模式的通知线程安全notify方法遍历观察者列表时其他线程可能正在调用attach或detach必须加锁。同时要注意死锁和性能。一种常见优化是在notify内部复制一份观察者列表的副本然后在副本上执行通知这样可以缩短锁的持有时间。void Subject::notify(const std::string msg) { std::vectorstd::weak_ptrObserver observersCopy; { std::lock_guardstd::mutex lock(mtx_); observersCopy observers_; // 复制 } // 锁在这里释放 for (auto weakObs : observersCopy) { if (auto obs weakObs.lock()) { obs-update(msg); // 在锁外调用更新避免在观察者update方法中反向调用Subject造成死锁 } } }双重检查锁定模式DCLP的陷阱在C11之前为了实现懒汉式单例的线程安全DCLP很流行但由于内存读写乱序指令重排它是不安全的。C11的std::atomic和内存序memory order解决了这个问题但实现复杂。因此强烈推荐使用局部静态变量方案它既简单又正确。4.3 过度设计与模式滥用这是学习设计模式后容易走入的另一个极端为了用模式而用模式把简单问题复杂化。YAGNI原则You Ain‘t Gonna Need It不要为未来可能需要的、但当前并不需要的功能添加抽象层。如果当前只有一个具体的产品类就不要急着上抽象工厂。KISS原则Keep It Simple, Stupid能用一个简单if语句清晰表达的逻辑就不要引入策略模式。模式是工具不是目的。识别真正的变化点应用设计模式的关键在于识别代码中哪些部分在未来是可能变化的将这些变化点封装起来。如果某个地方根本不会变强行套用模式只会增加不必要的复杂度。一个反例为一个简单的、永远不会改变的配置文件读取器设计一个复杂的“配置读取策略”模式支持XML、JSON、YAML等格式但实际上项目只使用JSON。这就是过度设计。4.4 C现代特性对传统模式的改良C11/14/17/20引入的新特性让许多模式的实现变得更简洁、更安全、更高效。std::function和 Lambda 表达式极大地简化了命令模式、策略模式、观察者模式回调的实现无需再定义一堆小类。移动语义和完美转发在工厂模式中可以高效地构造和返回对象。在构建者模式Builder Pattern中可以链式调用并最终移动构造目标对象。类型推导auto减少了代码中对具体类型的依赖让基于接口的编程依赖倒置写起来更舒服。变参模板可以创建接受任意数量、任意类型参数的工厂方法或构建者非常灵活。constexpr和if constexpr结合模板可以在编译期完成更多的逻辑判断和策略选择将运行时开销降到零。例如一个现代C的通用对象工厂可能长这样templatetypename Base, typename... Args class GenericFactory { using CreatorFunc std::unique_ptrBase(*)(Args...); std::unordered_mapstd::string, CreatorFunc creators_; public: templatetypename Derived bool registerClass(const std::string name) { creators_[name] [](Args... args) - std::unique_ptrBase { return std::make_uniqueDerived(std::forwardArgs(args)...); }; return true; } std::unique_ptrBase create(const std::string name, Args... args) { auto it creators_.find(name); if (it ! creators_.end()) { return it-second(std::forwardArgs(args)...); } return nullptr; } };这个工厂利用变参模板和完美转发可以注册和创建任何构造参数匹配的派生类对象代码通用且类型安全。5. 从理论到实践一个迷你项目的模式应用剖析让我们通过一个具体的、简化但完整的小项目来串联几种模式。假设我们要开发一个轻量级的数据报表生成器它可以从不同来源数据库、CSV文件获取数据用不同的格式HTML、纯文本渲染并支持将报告通过不同方式邮件、文件输出。需求分析数据源可变Database, CSV。渲染格式可变HTML, PlainText。输出方式可变Email, File。希望这些部分能独立变化和组合。设计选择这明显是一个需要创建产品族数据获取器、渲染器、输出器的场景且它们之间存在关联比如HTML渲染器可能和邮件输出器搭配更好。抽象工厂模式是首选。UML思路用文字描述ReportFactory抽象工厂声明创建一系列产品DataSource,Renderer,Exporter的接口。StandardReportFactory和FancyReportFactory具体工厂分别创建一套“标准”产品如CSV源、文本渲染、文件输出和一套“ fancy”产品如DB源、HTML渲染、邮件输出。各个产品有自己的抽象接口和具体实现。C核心代码实现// 1. 抽象产品接口 class DataSource { public: virtual ~DataSource() default; virtual std::vectorstd::string fetchData() 0; }; class Renderer { public: virtual ~Renderer() default; virtual std::string render(const std::vectorstd::string data) 0; }; class Exporter { public: virtual ~Exporter() default; virtual void exportReport(const std::string content) 0; }; // 2. 抽象工厂 class ReportFactory { public: virtual ~ReportFactory() default; virtual std::unique_ptrDataSource createDataSource() 0; virtual std::unique_ptrRenderer createRenderer() 0; virtual std::unique_ptrExporter createExporter() 0; }; // 3. 具体产品 - 标准系列 class CsvDataSource : public DataSource { std::string filePath_; public: explicit CsvDataSource(const std::string path) : filePath_(path) {} std::vectorstd::string fetchData() override { // 模拟读取CSV return {Alice,100, Bob,85, Charlie,95}; } }; class TextRenderer : public Renderer { public: std::string render(const std::vectorstd::string data) override { std::string result Report:\n; for (const auto line : data) { result - line \n; } return result; } }; class FileExporter : public Exporter { std::string filePath_; public: explicit FileExporter(const std::string path) : filePath_(path) {} void exportReport(const std::string content) override { std::ofstream file(filePath_); file content; std::cout Report saved to file: filePath_ std::endl; } }; // 4. 具体工厂 - 标准工厂 class StandardReportFactory : public ReportFactory { std::string csvPath_; std::string outputPath_; public: StandardReportFactory(std::string csvPath, std::string outputPath) : csvPath_(std::move(csvPath)), outputPath_(std::move(outputPath)) {} std::unique_ptrDataSource createDataSource() override { return std::make_uniqueCsvDataSource(csvPath_); } std::unique_ptrRenderer createRenderer() override { return std::make_uniqueTextRenderer(); } std::unique_ptrExporter createExporter() override { return std::make_uniqueFileExporter(outputPath_); } }; // 5. 客户端代码 - 报表生成器 class ReportGenerator { std::unique_ptrReportFactory factory_; public: explicit ReportGenerator(std::unique_ptrReportFactory factory) : factory_(std::move(factory)) {} void generate() { auto dataSource factory_-createDataSource(); auto renderer factory_-createRenderer(); auto exporter factory_-createExporter(); auto data dataSource-fetchData(); auto reportContent renderer-render(data); exporter-exportReport(reportContent); } }; // 6. 使用 int main() { // 配置使用标准报表工厂 auto factory std::make_uniqueStandardReportFactory(data.csv, report.txt); ReportGenerator generator(std::move(factory)); generator.generate(); // 未来要切换为FancyReportFactory只需修改这里的一行代码。 // 客户端代码(ReportGenerator)完全不需要改动。 return 0; }模式组合与扩展在这个系统中如果我们需要动态切换渲染算法比如根据数据量选择快速渲染或高质量渲染可以在Renderer的具体实现内部使用策略模式。如果报表生成过程非常复杂涉及多个步骤验证数据、清洗数据、渲染、添加水印、导出可以考虑使用建造者模式Builder Pattern来分步构造最终的报表对象。如果我们需要监听报表生成的生命周期事件开始、完成、出错可以引入观察者模式。项目总结与反思这个迷你项目展示了抽象工厂如何将一系列相关的对象创建封装起来让客户端代码只依赖抽象接口。最大的好处是符合开闭原则当我们需要增加一套新的报表风格比如Markdown渲染控制台输出只需要新增一组具体产品类和一个具体工厂类现有的ReportGenerator和ReportFactory接口都无需修改。然而抽象工厂的缺点也很明显增加新的产品等级结构比如新增一个ChartGenerator图表生成器会非常困难需要修改抽象工厂和所有具体工厂的接口。因此在项目初期需要仔细评估哪些维度是真正稳定、哪些是容易变化的。如果产品族非常稳定而产品等级结构可能增加那么抽象工厂可能不是最优选或许工厂方法模式或依赖注入框架更适合。在实际工程中没有银弹。设计模式为我们提供了经过验证的解决方案工具箱但最终选择哪把工具需要你根据项目的具体需求、团队的技术栈、以及未来的可维护性进行综合权衡。记住模式是仆人不是主人。写出清晰、可维护、高效的代码才是我们最终的目的。
郑州网站建设
网页设计
企业官网