C++设计模式实战避坑指南:单例、工厂、观察者、策略、装饰器
2026/7/21 4:54:06 网站建设 项目流程

1. 项目概述:为什么资深C++架构师要谈设计模式避坑?

干了十多年C++,从桌面客户端到高并发后台服务,从嵌入式设备到大型游戏引擎,代码写了上百万行,架构也画了无数张。回头看看,最让我感慨的不是用了多少酷炫的新特性,而是那些看似基础、却总在关键时刻“坑”人的设计模式。网上关于设计模式的教程和“23种”大全铺天盖地,但很多文章要么是照搬《设计模式》那本书的UML图,要么就是用Java/Python写个“玩具”示例,放到C++的真实生产环境里,水土不服是常态,性能陷阱、内存泄漏、过度设计等问题层出不穷。

这个笔记,就是我结合过去十年在大型C++项目中踩过的坑、填过的雷,总结出的5种最常用,但也最容易用错的设计模式实战避坑指南。它不追求大而全,只聚焦于单例、工厂、观察者、策略、装饰器这五种在C++项目中出场率超过80%的模式。我的目标很明确:告诉你这些模式在C++里怎么用才高效、安全,以及更重要的是,什么情况下应该避免使用。无论是正在啃《Effective C++》的进阶新手,还是负责核心模块设计的资深工程师,希望这些从真实项目血泪史中提炼出的经验,能帮你少走弯路,写出更健壮、更易维护的C++代码。

2. 核心思路:C++设计模式应用的三大原则

在深入具体模式之前,我们必须先统一思想。C++是一门拥有强大控制力但同时也充满“陷阱”的语言,直接套用其他语言的设计模式思想,往往会带来灾难。我的核心思路建立在三大原则上,这是所有后续讨论的基石。

2.1 原则一:资源管理优先于对象结构

这是C++与其他托管语言(如Java、C#)应用设计模式时最根本的差异。在Java中,你思考的是对象间的引用关系;在C++中,你首先必须思考的是:内存谁分配、谁释放?对象如何传递?拷贝还是移动?一个设计模式即使结构再优美,如果引入了难以管理的资源生命周期问题,在C++中就是失败的。

例如,观察者模式中,观察者对象的生命周期可能短于被观察者。在Java里,你注册一个监听器,不用太担心它被GC回收后的问题(虽然有内存泄漏风险)。但在C++中,如果被观察者持有一个已销毁观察者的裸指针,后续通知就会导致未定义行为,通常是程序崩溃。因此,C++中应用任何涉及对象关联的模式,首要任务就是理清资源所有权(ownership)和生命周期

2.2 原则二:编译时多态优于运行时多态

C++提供了两种多态:基于虚函数和继承的运行时多态,以及基于模板的编译时多态。传统设计模式大多依赖于运行时多态,这带来了虚函数调用开销、对象切片(object slicing)风险以及更复杂的继承层次。

现代C++(C++11/14/17之后)鼓励我们,在能使用编译时多态解决问题时,优先考虑它。例如,策略模式完全可以用std::function和模板来实现,避免定义抽象的策略基类。这不仅能消除虚函数调用开销(可能被编译器内联),还能让接口更灵活,支持函数对象、lambda表达式等。我们的目标是:在保持模式灵活性的同时,尽可能将决策点从运行时提前到编译时,让编译器为我们做更多的检查和优化。

2.3 原则三:简洁直白优于过度抽象

设计模式是为了解决特定问题,而不是为了用模式而用模式。我见过太多代码,为了“符合设计模式”,引入了不必要的抽象层,导致代码跳转五六层才能找到实际逻辑,严重损害了可读性和可调试性。

在C++中,过度抽象的代价尤其高昂。每一个虚函数调用、每一次动态内存分配(new)、每一层额外的间接性,都可能成为性能瓶颈。因此,我的避坑指南始终贯穿着一个思想:先用最简单直白的方式实现功能,只有当代码中出现重复、变化或复杂度确实需要被管理时,再引入相应的设计模式进行重构。记住,最优雅的设计往往是看起来最不“设计”的。

3. 五大常用设计模式C++实战与避坑详解

接下来,我们进入正题,逐一拆解这五种模式。我会先给出一个典型的“坑”式实现,然后分析问题,最后给出经过实战检验的“避坑”实现方案。

3.1 单例模式(Singleton):从双重检查锁定到Meyers‘ Singleton

单例大概是争议最大、也最容易被滥用的模式。它的意图是确保一个类只有一个实例,并提供一个全局访问点。

经典坑点:线程不安全的懒汉式

class UnsafeSingleton { public: static UnsafeSingleton* getInstance() { if (instance_ == nullptr) { // 危险操作! instance_ = new UnsafeSingleton(); } return instance_; } // ... 其他成员函数 private: UnsafeSingleton() = default; static UnsafeSingleton* instance_; }; UnsafeSingleton* UnsafeSingleton::instance_ = nullptr;

这个实现在多线程环境下是灾难。两个线程可能同时通过if (instance_ == nullptr)检查,从而导致构造函数被调用两次,内存被重复分配,造成内存泄漏或更诡异的状态问题。

进阶坑点:双重检查锁定(DCLP)及其陷阱为了解决线程安全,很多人会搬出“双重检查锁定”:

class DCLPSingleton { public: static DCLPSingleton* getInstance() { if (instance_ == nullptr) { // 第一次检查 std::lock_guard<std::mutex> lock(mutex_); if (instance_ == nullptr) { // 第二次检查 instance_ = new DCLPSingleton(); } } return instance_; } private: static DCLPSingleton* instance_; static std::mutex mutex_; };

在C++11之前,这个实现仍然有问题!因为instance_ = new DCLPSingleton();这行代码不是原子的。它大致分为三步:1. 分配内存;2. 在内存上构造对象;3. 将地址赋值给instance_。编译器或CPU可能对步骤2和3进行重排序,导致另一个线程在第一次检查时看到instance_非空,但指向的对象尚未构造完成,从而访问到未初始化的内存。虽然C++11后的内存模型可以通过std::atomic和特定内存序来解决,但实现复杂,容易出错。

避坑指南:使用局部静态变量的Meyers‘ Singleton (C++11后)对于大多数场景,这是最简单、最安全、也最高效的单例实现。

class MeyerSingleton { public: static MeyerSingleton& getInstance() { static MeyerSingleton instance; // C++11保证此初始化是线程安全的 return instance; } // 删除拷贝构造和赋值,确保唯一性 MeyerSingleton(const MeyerSingleton&) = delete; MeyerSingleton& operator=(const MeyerSingleton&) = delete; private: MeyerSingleton() = default; ~MeyerSingleton() = default; };

为什么这是最佳实践?

  1. 线程安全:C++11标准明确规定,局部静态变量的初始化在并发执行时,只会有一个线程执行初始化,其他线程会阻塞等待初始化完成。这由编译器在底层保证。
  2. 懒加载:只有在第一次调用getInstance()时,对象才会被构造。
  3. 自动释放:程序结束时,静态对象会按照构造的逆序自动析构,无需手动delete,避免了内存泄漏。
  4. 返回引用:返回引用避免了返回指针可能为nullptr的歧义,语义更清晰。

实操心得:除非有非常特殊的生命周期管理需求(例如需要显式控制单例的创建和销毁顺序),否则在C++11及以上环境中,请无条件使用Meyers‘ Singleton。它几乎完美地解决了单例模式的所有经典问题。

3.2 工厂模式(Factory):避免继承地狱,拥抱返回智能指针

工厂模式用于封装对象的创建过程,使代码不依赖于具体的类。但在C++中,传统的工厂方法容易导致“继承地狱”和原始指针管理混乱。

经典坑点:返回原始指针的工厂

class Product { public: virtual ~Product() {} virtual void operate() = 0; }; class ConcreteProductA : public Product { /*...*/ }; class ConcreteProductB : public Product { /*...*/ }; class Creator { public: // 坑:返回原始指针,调用者负责删除,极易忘记导致内存泄漏 virtual Product* createProduct() = 0; }; class ConcreteCreatorA : public Creator { public: Product* createProduct() override { return new ConcreteProductA(); // 谁负责delete? } };

这个设计的致命伤在于所有权模糊。工厂返回了一个new出来的对象,调用者必须记住在合适的时候delete它。在复杂的调用链或异常发生时,这很难保证。

避坑指南:使用std::unique_ptr明确所有权,并考虑模板化现代C++工厂应该优先返回智能指针,明确传递所有权。

#include <memory> #include <string> class Product { public: virtual ~Product() = default; virtual void operate() = 0; }; class ConcreteProductA : public Product { /*...*/ }; class ConcreteProductB : public Product { /*...*/ }; // 方案1:简单工厂函数,返回unique_ptr std::unique_ptr<Product> createProduct(const std::string& type) { if (type == "A") { return std::make_unique<ConcreteProductA>(); } else if (type == "B") { return std::make_unique<ConcreteProductB>(); } throw std::invalid_argument("Unknown product type"); } // 方案2:模板工厂,编译时绑定,零开销 template <typename ProductType> std::unique_ptr<Product> createProduct() { return std::make_unique<ProductType>(); } // 使用:auto prod = createProduct<ConcreteProductA>();

更进一步:避免庞大的if-else/switch链当产品类型很多时,上面的if-else会变得臃肿。可以使用注册表模式(Registry Pattern)来动态注册创建函数。

class ProductFactory { public: using CreatorFunc = std::function<std::unique_ptr<Product>()>; static ProductFactory& instance() { static ProductFactory factory; return factory; } bool registerProduct(const std::string& name, CreatorFunc creator) { return creators_.emplace(name, std::move(creator)).second; } std::unique_ptr<Product> create(const std::string& name) { auto it = creators_.find(name); if (it != creators_.end()) { return it->second(); // 调用注册的创建函数 } return nullptr; } private: ProductFactory() = default; std::unordered_map<std::string, CreatorFunc> creators_; }; // 每个具体产品类在其CPP文件中自行注册 namespace { bool registeredA = ProductFactory::instance().registerProduct("A", []{ return std::make_unique<ConcreteProductA>(); }); }

注意事项:工厂模式的核心价值在于隔离变化点。如果产品的创建逻辑非常稳定,或者只有一两种类型,直接newmake_unique可能比引入工厂更简洁。不要为了模式而模式。

3.3 观察者模式(Observer):弱引用与生命周期管理的艺术

观察者模式定义了一种一对多的依赖关系,当一个对象状态改变时,所有依赖它的对象都会得到通知。在C++中,最大的坑是观察者生命周期管理不当导致的“悬空指针”问题。

经典坑点:被观察者持有观察者的原始指针

class Observer { public: virtual void update() = 0; }; class Subject { std::vector<Observer*> observers_; // 危险!持有裸指针 public: void attach(Observer* obs) { observers_.push_back(obs); } void detach(Observer* obs) { /* 从vector中删除obs... */ } void notify() { for (auto obs : observers_) { obs->update(); // 如果obs已销毁,这里就是未定义行为! } } };

如果某个Observer对象在detach之前就被销毁了,那么Subject持有的指针就变成了“悬空指针”。后续调用notify()时,对其解引用obs->update()会导致程序崩溃。

避坑指南:使用std::weak_ptr打破循环引用,或使用唯一标识符解决方案的核心是:被观察者不能拥有观察者的所有权,只能持有一种“不会延长其生命周期,但能安全检测其是否存活”的引用。

方案一:基于std::shared_ptrstd::weak_ptr(推荐)

#include <memory> #include <vector> class Observer : public std::enable_shared_from_this<Observer> { public: virtual void update() = 0; virtual ~Observer() = default; }; class Subject { std::vector<std::weak_ptr<Observer>> observers_; // 存储弱引用 public: void attach(std::shared_ptr<Observer> obs) { observers_.push_back(obs); // shared_ptr 自动转换为 weak_ptr } void notify() { auto it = observers_.begin(); while (it != observers_.end()) { if (auto sp = it->lock()) { // 尝试提升为shared_ptr sp->update(); // 对象还活着,安全调用 ++it; } else { // 对象已销毁,移除无效的弱引用 it = observers_.erase(it); } } } };

关键点解析

  1. std::weak_ptr:不增加引用计数,不会阻止所指向的对象被销毁。
  2. lock()方法:尝试将weak_ptr提升为shared_ptr。如果对象还存在,则提升成功,返回一个有效的shared_ptr;如果对象已被销毁,则返回空的shared_ptr。这是一个线程安全的检查。
  3. 自动清理:在notify时,我们顺便清理了那些已经失效的观察者引用,避免了容器膨胀。

方案二:基于唯一标识符和显式注销(适用于不使用智能指针的场景)如果项目禁用或不便使用智能指针,可以采用“令牌(Token)”模式。

class Observer { public: virtual void update() = 0; virtual ~Observer() { // 析构时,最好能自动通知Subject注销自己,但这需要Subject的引用 } }; class Subject { std::unordered_map<int, Observer*> observers_; // 用ID映射 int nextId_{0}; public: int attach(Observer* obs) { // 返回一个令牌ID int id = ++nextId_; observers_[id] = obs; return id; } void detach(int token) { observers_.erase(token); } void notify() { // 仍然有风险,但比直接遍历裸指针向量稍好 for (auto& [id, obs] : observers_) { if (obs) { // 无法判断obs是否悬空! obs->update(); } } } }; // 观察者需要保存这个token,并在析构前调用detach(token)

常见问题:方案二仍然不完美,因为Observer析构时必须记得调用detach,否则Subject仍持有悬空指针。这依赖于程序员的自律,容易出错。因此,在C++11及以上环境中,方案一(weak_ptr)是更通用、更安全的选择。它虽然引入了智能指针的复杂度,但彻底解决了生命周期管理的核心难题。

3.4 策略模式(Strategy):用std::function和模板替代继承

策略模式定义了一系列算法,并将每个算法封装起来,使它们可以互相替换。传统实现依赖于继承和虚函数,但在C++中,这常常不是最优解。

经典坑点:厚重的继承层次

class SortStrategy { public: virtual void sort(std::vector<int>& data) = 0; virtual ~SortStrategy() = default; }; class BubbleSort : public SortStrategy { /*...*/ }; class QuickSort : public SortStrategy { /*...*/ }; class MergeSort : public SortStrategy { /*...*/ }; class Context { SortStrategy* strategy_; public: void setStrategy(SortStrategy* strategy) { strategy_ = strategy; } void execute(std::vector<int>& data) { if (strategy_) strategy_->sort(data); } };

每增加一种排序算法,就需要新增一个类。如果算法很简单(比如只是一个比较函数),这种开销显得很不划算。

避坑指南:使用std::function实现轻量级策略std::function可以包装任何可调用对象(函数、函数指针、lambda表达式、bind表达式、函数对象)。这让我们可以摆脱继承的束缚。

#include <functional> #include <vector> using SortStrategy = std::function<void(std::vector<int>&)>; void bubbleSort(std::vector<int>& data) { /*...*/ } class QuickSorter { public: void operator()(std::vector<int>& data) const { /*...*/ } }; class Context { SortStrategy strategy_; public: void setStrategy(SortStrategy strategy) { strategy_ = std::move(strategy); // 可调用对象也是可移动的 } void execute(std::vector<int>& data) { if (strategy_) { strategy_(data); } } }; // 使用方式极其灵活 Context ctx; ctx.setStrategy(bubbleSort); // 设置普通函数 ctx.setStrategy(QuickSorter{}); // 设置函数对象 ctx.setStrategy([](std::vector<int>& data) { // 设置lambda表达式 std::sort(data.begin(), data.end()); });

优势分析

  1. 零继承:无需为每个策略创建单独的类,减少代码量。
  2. 极致的灵活性:策略可以是任何可调用实体,包括捕获了状态的lambda。
  3. 性能可能更优:对于小型的可调用对象(如无捕获的lambda),std::function可能进行小对象优化,避免堆分配。编译器也可能对内联简单策略有更好的优化机会。

进阶:编译时策略与模板如果策略在编译时就能确定,并且对性能有极致要求,可以使用模板,完全消除运行时开销。

template <typename Strategy> class ContextT { Strategy strategy_; public: void execute(std::vector<int>& data) { strategy_(data); } }; // 使用 ContextT<QuickSorter> ctx; ctx.execute(data); // 或者直接用lambda的类型(C++20起更方便) auto myStrategy = [](std::vector<int>& data) { /*...*/ }; ContextT<decltype(myStrategy)> ctx2{myStrategy};

实操心得:现代C++中,策略模式的首选实现是std::function。它提供了运行时多态的灵活性,又避免了继承体系的笨重。只有当策略类型在编译期固定且性能敏感时,才考虑模板化。同时,将策略定义为std::function也使得依赖注入单元测试变得更加容易,你可以轻松地注入一个模拟(Mock)策略。

3.5 装饰器模式(Decorator):小心组合爆炸与切片问题

装饰器模式动态地给一个对象添加额外的职责。在C++中实现,需要特别注意对象切片和多重装饰导致的类型膨胀问题。

经典坑点:值语义导致的“对象切片”

class Component { public: virtual void operation() = 0; virtual ~Component() = default; }; class ConcreteComponent : public Component { /*...*/ }; class Decorator : public Component { protected: Component* wrapped_; // 使用指针,正确 }; class ConcreteDecoratorA : public Decorator { public: void operation() override { // 添加额外功能 wrapped_->operation(); // 添加额外功能 } };

这个基础结构是对的。但一个常见的错误是,在装饰器链的构建或传递过程中,不小心使用了值传递(by value),导致派生类对象被“切片”为基类对象,丢失了装饰器特有的状态和行为。

void badFunction(Component comp) { // 按值传递,发生切片! comp.operation(); } ConcreteDecoratorA decorator; badFunction(decorator); // 传入的是Decorator,函数内收到的是被切片的Component

避坑指南一:始终使用指针或引用(最好是智能指针)传递多态对象装饰器模式中,所有涉及Component的地方,都应该使用指针(Component*)或引用(Component&),或者更好的,使用std::unique_ptr<Component>来明确所有权。

std::unique_ptr<Component> component = std::make_unique<ConcreteComponent>(); component = std::make_unique<ConcreteDecoratorA>(std::move(component)); // 现在component指向一个被DecoratorA装饰过的对象

装饰器构造函数的正确写法

class ConcreteDecoratorA : public Decorator { public: // 接受一个unique_ptr,接管其所有权 explicit ConcreteDecoratorA(std::unique_ptr<Component> comp) : wrapped_(std::move(comp)) {} // ... operation 实现 };

避坑指南二:警惕装饰器链过长带来的性能与调试复杂度装饰器模式的优点是灵活,可以动态组合功能。但缺点是,如果装饰层数过多,会导致:

  1. 调用栈深:一个操作调用会经过层层转发,影响性能(尤其是虚函数调用开销)和调试体验。
  2. 对象结构复杂:内存中是一个长长的对象链,理解其当前状态比较困难。
  3. 组合爆炸:如果有N种装饰器,理论上可以组合出非常多的形态,但很多组合可能没有实际意义,反而增加了系统的复杂度。

注意事项:在实际项目中,要谨慎评估是否真的需要动态装饰。有时,使用简单的组合(即一个类直接持有多个功能类的指针)或者模板混合(Template Mixin)在编译期组合功能,可能是更清晰、更高效的选择。装饰器模式更适合那些职责单一、可叠加、且需要在运行时动态增删的场景,比如数据流处理、中间件、GUI组件边框装饰等。

4. 设计模式在C++项目中的综合应用与抉择

掌握了单个模式的避坑技巧,我们还需要从更高的视角看如何在项目中综合运用和抉择。模式不是孤立的,它们经常协同工作,但错误的选择会让系统变得复杂难懂。

4.1 何时该用,何时不该用?

这是一个价值百万的问题。我的经验法则是“三次法则”和“变化轴分析”。

三次法则(Rule of Three):不要第一次发现需求变化时就引入设计模式。当你第三次写类似的、为了适应变化的冗余代码时,再考虑引入模式进行重构。这能有效避免过度设计。

变化轴分析:识别系统中哪些部分会因何种原因发生变化。设计模式应该封装那些真正可能变化的部分。例如:

  • 工厂模式封装的是“对象创建方式”的变化。
  • 策略模式封装的是“算法或策略”的变化。
  • 装饰器模式封装的是“对象职责”的动态增减。 如果某个部分在可预见的未来根本不会变,为其套用模式就是画蛇添足。

4.2 模式组合的典型案例与陷阱

案例:配置解析器(工厂+策略)假设我们需要一个配置解析器,支持JSON、XML、YAML等多种格式,并且每种格式的解析细节(如日期处理、数字精度)可以有不同策略。

  • 工厂:负责根据文件后缀名或配置内容,创建对应的解析器对象(JsonParser,XmlParser)。这里可以用我们之前提到的注册表工厂。
  • 策略:在每个解析器内部,对于“日期解析”这个行为,可以定义一个DateParsingStrategy,并用std::function来实现,允许运行时切换不同的日期格式处理逻辑。

陷阱:循环依赖与过度抽象当模式组合时,最容易出现类图复杂、模块间循环依赖的问题。例如,观察者模式中的SubjectObserver如果相互引用太深,就容易形成耦合。此时,可以考虑引入中介者模式(Mediator)或事件总线(Event Bus)来解耦,但这又增加了新的抽象层。务必权衡,确保新引入的复杂度带来的收益(可维护性、灵活性)大于其成本。

4.3 性能考量:零成本抽象不是免费的

C++哲学强调“零成本抽象”,但设计模式的抽象层通常会带来一些成本:

  1. 虚函数调用开销:每次虚函数调用都有一次间接寻址(vptr查找vtable),可能影响CPU缓存局部性。在极端性能敏感的循环中,需要评估。
  2. 动态内存分配:工厂模式创建对象、观察者模式存储观察者列表等,都可能涉及堆内存分配(new/make_unique),这比栈分配慢。
  3. 间接性:通过指针或引用访问对象,比直接访问多一次解引用。

应对策略

  • 测量,而不是猜测:使用性能分析工具(如perf, VTune)找到真正的热点,不要过早优化。
  • 编译时多态:如前所述,用模板实现策略、工厂方法,可以将决策提前到编译期。
  • 对象池:对于需要频繁创建销毁的对象(如某些工厂产品),可以考虑使用对象池复用内存,减少分配开销。
  • 扁平化设计:在性能关键路径上,有时“简单粗暴”的if-elseswitch比精致的模式更高效。

5. 从理论到实践:一个微型日志库的设计案例

让我们用一个具体的例子来串联几种模式。假设我们要设计一个轻量级的日志库,需求是:支持输出到控制台和文件,支持不同的日志格式(如纯文本、JSON),并且可以动态添加日志过滤器(如只输出ERROR级别以上的日志)。

5.1 核心组件与模式映射

  • Logger(主体):采用观察者模式的核心。它是一个Subject,维护一个Sink(日志槽,即观察者)列表。当有日志需要记录时,它notify所有Sink
  • Sink(抽象观察者):定义日志输出的接口。具体的ConsoleSinkFileSink实现它。
  • Formatter(策略):作为Sink的一个成员,采用策略模式。每个Sink可以配置不同的Formatter(如PlainTextFormatterJsonFormatter)来决定日志的最终格式。
  • Filter(装饰器?):这里有两种选择。一是作为装饰器,装饰在SinkFormatter外,在日志被处理前进行过滤。二是作为策略,集成到LoggerSink的逻辑中。考虑到过滤器可能多个且需要灵活组合,采用装饰器模式更合适,可以动态地包裹Sink

5.2 关键实现片段

// Formatter 策略 class Formatter { public: virtual std::string format(const LogMessage& msg) = 0; virtual ~Formatter() = default; }; class JsonFormatter : public Formatter { /*...*/ }; class PlainTextFormatter : public Formatter { /*...*/ }; // Sink 观察者 class Sink : public std::enable_shared_from_this<Sink> { std::unique_ptr<Formatter> formatter_; public: virtual void write(const std::string& formattedMessage) = 0; void setFormatter(std::unique_ptr<Formatter> fmt) { formatter_ = std::move(fmt); } std::string formatMessage(const LogMessage& msg) { return formatter_ ? formatter_->format(msg) : msg.toString(); } virtual ~Sink() = default; }; // Filter 装饰器基类 class FilterSink : public Sink { protected: std::unique_ptr<Sink> sink_; public: explicit FilterSink(std::unique_ptr<Sink> sink) : sink_(std::move(sink)) {} void write(const std::string& msg) override { // 由子类决定是否/如何转发 sink_->write(msg); } }; // 具体装饰器:级别过滤器 class LevelFilterSink : public FilterSink { LogLevel minLevel_; public: LevelFilterSink(std::unique_ptr<Sink> sink, LogLevel lvl) : FilterSink(std::move(sink)), minLevel_(lvl) {} void write(const std::string& msg) override { if (/* 判断消息级别 >= minLevel_ */) { FilterSink::write(msg); // 调用被装饰sink的write } // 否则丢弃 } }; // Logger (Subject) class Logger { std::vector<std::weak_ptr<Sink>> sinks_; std::mutex mutex_; // 考虑多线程 public: void addSink(std::shared_ptr<Sink> sink) { std::lock_guard lock(mutex_); sinks_.push_back(sink); } void log(const LogMessage& msg) { std::lock_guard lock(mutex_); for (auto it = sinks_.begin(); it != sinks_.end();) { if (auto sp = it->lock()) { sp->write(sp->formatMessage(msg)); ++it; } else { it = sinks_.erase(it); } } } }; // 使用示例 auto main() -> int { auto consoleSink = std::make_shared<ConsoleSink>(); consoleSink->setFormatter(std::make_unique<PlainTextFormatter>()); auto fileSink = std::make_shared<FileSink>("app.log"); fileSink->setFormatter(std::make_unique<JsonFormatter>()); // 给文件Sink添加一个过滤器,只记录WARNING及以上级别 auto filteredFileSink = std::make_unique<LevelFilterSink>(std::move(fileSink), LogLevel::WARNING); Logger logger; logger.addSink(consoleSink); logger.addSink(std::move(filteredFileSink)); // 添加被装饰过的sink logger.log(LogMessage{LogLevel::INFO, "This is an info message"}); // 只会输出到控制台 logger.log(LogMessage{LogLevel::ERROR, "Something bad happened"}); // 输出到控制台和文件 }

这个案例展示了如何将观察者、策略、装饰器模式有机结合起来,构建一个灵活、可扩展的日志库。每个模式都负责封装一个明确的变化点:观察者模式处理输出目标的增减,策略模式处理格式的变化,装饰器模式处理输出前的过滤行为。

5.3 案例反思与优化点

  1. 性能:每次日志调用都涉及虚函数调用、可能的动态内存分配(格式化字符串)、锁竞争。在高性能场景下,可能需要引入异步日志、双缓冲队列等技术。
  2. 异常安全Sink::write操作(如写文件)可能抛出异常。需要决定是让异常传播出去,还是在Logger::log内部捕获并处理,避免一个Sink的失败影响其他Sink。
  3. 配置化:如何方便地从配置文件初始化这个复杂的对象树?这又可以结合工厂模式和建造者模式(Builder)来解决。

设计模式是强大的工具,但绝不是银弹。在C++这片充满细节与陷阱的土地上,理解每种模式的意图、权衡其带来的开销、并熟练运用现代C++特性(智能指针、std::function、模板等)来规避经典实现中的坑,才是将其价值最大化的关键。记住,最好的代码往往是简单的、清晰的、直指问题核心的。模式应该服务于这个目标,而不是相反。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询