1. 项目概述从“会用”到“懂用”的设计模式最近在社区里看到不少朋友在讨论C设计模式特别是工厂模式和观察者模式感觉大家普遍卡在一个点上代码能照着书上的例子敲出来但一到自己的项目里就不知道怎么用或者用起来总觉得别扭像是给代码穿了件不合身的外套。这其实挺正常的我刚开始接触设计模式那会儿也这样总觉得这些“模式”有点玄乎像是武林秘籍里的招式背是背下来了但真打起来还是王八拳。实际上设计模式从来都不是死记硬背的“套路”。它更像是一群顶尖程序员在经历了无数个项目、踩过无数个坑之后总结出来的“最佳实践”或“惯用解决方案”。它的价值不在于代码本身而在于背后的设计思想和解决问题的思路。今天我们就以C特别是现代C17/20的语境下为舞台深入聊聊工厂模式和观察者模式。我们不只讲“怎么写”更要讲“为什么这么写”、“什么时候用”以及“用了之后会带来什么好处和代价”。目标很明确让你下次在项目里遇到类似场景时能自信地判断——“嗯这里用个工厂模式很合适”然后行云流水地把它实现出来而不是对着书发呆。2. 设计思想先行模式是思想的载体在跳进具体的代码之前我们必须先统一思想。很多人学设计模式一上来就找UML图、看类结构、背代码这是本末倒置。设计模式首先是设计思想的体现其次才是具体的代码实现。2.1 核心设计原则的体现无论是工厂模式还是观察者模式其诞生都深深植根于几个经典的面向对象设计原则。理解这些原则你就拿到了理解所有设计模式的万能钥匙。1. 开闭原则 (Open-Closed Principle, OCP)这是工厂模式的核心驱动力之一。原则要求“软件实体类、模块、函数等应该对扩展开放对修改关闭。” 听起来有点绕直白点说就是当需要增加新功能时应该通过添加新代码来实现而不是修改已有的、运行良好的旧代码。工厂模式如何体现想象一个图形绘制程序最初只支持画圆形和方形。如果不用工厂每增加一个三角形你可能就需要在一个巨大的if-else或switch语句里修改创建逻辑。而用了工厂模式特别是抽象工厂你只需要新增一个Triangle类和对应的TriangleFactory然后注册到工厂里。原有的创建圆形、方形的代码一行都不用动。系统对“增加新的图形类型”这个变化是“开放”的但对“修改原有创建逻辑”是“封闭”的。观察者模式如何体现主题被观察者的状态发生变化时需要通知所有观察者。如果不用观察者模式主题类里可能就需要维护一个观察者列表并且直接调用每个观察者的更新方法。当需要新增一种观察者比如日志观察者时你就得去修改主题类的通知代码。用了观察者模式后主题只依赖一个抽象的Observer接口。新增任何具体的观察者都只需要实现这个接口并注册即可主题类的核心通知逻辑无需任何修改。2. 依赖倒置原则 (Dependency Inversion Principle, DIP)高层模块不应该依赖低层模块二者都应该依赖其抽象。抽象不应该依赖细节细节应该依赖抽象。工厂模式如何体现客户端代码高层模块不应该直接new一个具体的产品对象低层模块比如Car* car new BMWCar();。这会导致客户端代码严重依赖BMWCar这个具体类。工厂模式通过引入一个抽象的CarFactory或一个静态的创建方法让客户端依赖这个抽象接口来获取产品从而将客户端与具体产品类解耦。客户端只知道它要一辆“车”而不知道也不关心这辆车是宝马还是奔驰。观察者模式如何体现主题不直接依赖具体的观察者如EmailNotifier,SMSNotifier而是依赖抽象的Observer接口。这样主题可以通知任何实现了该接口的观察者而不需要了解它们的具体实现细节。3. 单一职责原则 (Single Responsibility Principle, SRP)一个类应该只有一个引起它变化的原因。工厂模式如何体现将对象的创建过程从业务逻辑中剥离出来。原来一个类既要负责业务计算又要负责根据复杂条件创建对象职责过重。工厂模式专门用一个或多个工厂类来承担对象创建的职责使得业务类的职责更单一、更清晰。观察者模式如何体现将对象间的联动关系一个对象状态改变需要通知其他对象从对象自身的核心业务逻辑中解耦出来。主题的核心职责是管理自身的状态而通知观察者这个职责被委托给了观察者模式机制。观察者的核心职责是定义接收到通知后的反应行为。我的踩坑心得早年我做游戏开发有一个GameObject管理类里面密密麻麻全是switch (type)来创建怪物、道具、技能。后来要加一个新Boss我改这个管理类的时候手一抖把创建普通怪物的逻辑给改坏了导致线上出Bug。这就是典型的违反开闭原则和单一职责原则。如果早用上工厂模式新加Boss只需要新增代码根本不会碰到旧逻辑。理解了这些思想我们再去看工厂和观察者的代码就不会觉得它们是一堆莫名其妙的类在绕圈子而会看到每一种继承、每一个接口、每一次委托背后清晰的设计意图。3. 工厂模式深度解析不只是new的替代品工厂模式的核心诉求是封装对象的创建过程使客户端代码与具体产品类解耦。根据封装力度和场景复杂度主要分为三种简单工厂、工厂方法、抽象工厂。很多人容易混淆我们逐一拆解。3.1 简单工厂模式快速上手的创建门面这不是GoF 23种设计模式之一但它是理解工厂思想的绝佳起点在实际中小项目中应用极广。场景有一个Parser类需要根据文件后缀.json,.xml,.yaml创建不同的解析器对象。传统做法紧耦合// 客户端代码 std::unique_ptrParser parser; if (extension .json) { parser std::make_uniqueJsonParser(); } else if (extension .xml) { parser std::make_uniqueXmlParser(); } else if (extension .yaml) { parser std::make_uniqueYamlParser(); } else { throw std::runtime_error(Unsupported format); } // 使用parser...问题创建逻辑散落在客户端各处。如果新增一种.csv解析器你需要找到所有创建Parser的地方进行修改极易遗漏。简单工厂实现// 产品接口 class Parser { public: virtual ~Parser() default; virtual void parse(const std::string content) 0; }; // 具体产品 class JsonParser : public Parser { /*...*/ }; class XmlParser : public Parser { /*...*/ }; class YamlParser : public Parser { /*...*/ }; // 简单工厂类 class ParserFactory { public: static std::unique_ptrParser createParser(const std::string extension) { if (extension .json) return std::make_uniqueJsonParser(); if (extension .xml) return std::make_uniqueXmlParser(); if (extension .yaml) return std::make_uniqueYamlParser(); return nullptr; // 或抛异常 } }; // 客户端代码 auto parser ParserFactory::createParser(.json); if (parser) { parser-parse(someContent); }优点客户端解耦客户端不再依赖JsonParser等具体类只依赖Parser接口和ParserFactory。职责分离创建逻辑被集中到工厂类中符合单一职责原则。修改集中新增CsvParser时只需修改ParserFactory::createParser这一个方法。缺点与注意事项违反开闭原则这是简单工厂最核心的批评点。每次新增产品类型都必须修改工厂类的createParser方法通常是增加一个if分支。虽然修改点集中了但毕竟还是在修改。工厂类职责过重如果产品类型极多比如几十种createParser方法会变成一个庞大的if-else或switch怪兽难以维护。适用场景产品类型相对固定变化不频繁的场景。或者作为向更复杂工厂模式如工厂方法过渡的临时方案。3.2 工厂方法模式将创建权下放工厂方法模式定义了一个用于创建对象的接口但让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类。场景一个跨平台的UI库每个平台Windows, Mac, Linux都有自己的一套按钮(Button)、文本框(TextBox)等控件。我们希望在运行时根据当前平台创建对应的控件。UML核心有一个抽象的Creator工厂类它声明了抽象的factoryMethod()。ConcreteCreatorA和ConcreteCreatorB继承它并实现factoryMethod()来返回具体的ProductA和ProductB。现代C实现使用智能指针和现代特性// 产品接口 class Button { public: virtual ~Button() default; virtual void render() const 0; virtual void onClick() 0; }; // 具体产品 class WindowsButton : public Button { public: void render() const override { std::cout Rendering a Windows-style button.\n; } void onClick() override { std::cout Windows button clicked!\n; } }; class MacButton : public Button { /* 类似渲染Mac风格按钮 */ }; // 抽象创建者工厂接口 class Dialog { public: virtual ~Dialog() default; // 这就是工厂方法注意它返回的是基类指针。 virtual std::unique_ptrButton createButton() const 0; // 一个业务方法它会调用工厂方法来创建产品 void renderDialog() const { // 调用工厂方法创建者不知道具体按钮类型 auto button createButton(); button-render(); // ... 渲染其他对话框元素 } }; // 具体创建者 class WindowsDialog : public Dialog { public: std::unique_ptrButton createButton() const override { return std::make_uniqueWindowsButton(); } }; class MacDialog : public Dialog { public: std::unique_ptrButton createButton() const override { return std::make_uniqueMacButton(); } }; // 客户端代码 void clientCode(const Dialog dialog) { // 客户端只与抽象的Dialog和Button交互完全不知道具体平台 dialog.renderDialog(); } // 使用 int main() { // 根据配置或运行时环境决定使用哪个具体工厂 std::unique_ptrDialog dialog; #ifdef _WIN32 dialog std::make_uniqueWindowsDialog(); #elif __APPLE__ dialog std::make_uniqueMacDialog(); #endif if (dialog) { clientCode(*dialog); } }设计思想解读真正的开闭原则现在要支持一个新的LinuxButton我们只需要创建LinuxButton类和LinuxDialog类实现createButton。原有的WindowsDialog、MacDialog以及clientCode函数完全不需要修改。系统对扩展是开放的。依赖倒置的完美体现clientCode函数高层业务逻辑只依赖于抽象的Dialog和Button。具体的创建过程被“倒置”给了WindowsDialog和MacDialog这些低层模块但它们也依赖于抽象Button。单一职责每个具体工厂只负责创建自己系列的一个产品职责清晰。工厂方法 vs. 简单工厂结构简单工厂只有一个具体的工厂类通过参数决定产品。工厂方法有一个抽象工厂和多个具体工厂每个具体工厂只生产一种产品。开闭原则工厂方法符合开闭原则简单工厂不符合。复杂度工厂方法需要更多的类结构更复杂。实操心得工厂方法模式特别适合框架或库的设计。框架定义好抽象工厂和抽象产品Dialog,Button框架的使用者应用程序员通过继承这些抽象类提供具体的实现WindowsDialog,WindowsButton从而将框架扩展到新的场景。MFC、Qt等UI框架中大量使用了这种思想。3.3 抽象工厂模式创建产品家族抽象工厂模式提供一个接口用于创建相关的或依赖对象的家族而不需要明确指定具体类。简单说工厂方法针对的是一个产品等级结构如Button而抽象工厂针对的是多个产品等级结构形成的产品族如Button,TextBox,CheckBox这一套UI控件。场景上面的UI库每个平台不仅有自己的Button还有自己的TextBox和CheckBox。我们需要确保创建出来的控件都是同一风格的比如不能混用Windows的按钮和Mac的文本框。实现// 抽象产品A class Button { /* ... */ }; class WinButton : public Button { /* ... */ }; class MacButton : public Button { /* ... */ }; // 抽象产品B class TextBox { /* ... */ }; class WinTextBox : public TextBox { /* ... */ }; class MacTextBox : public TextBox { /* ... */ }; // 抽象工厂 class GUIFactory { public: virtual ~GUIFactory() default; virtual std::unique_ptrButton createButton() const 0; virtual std::unique_ptrTextBox createTextBox() const 0; }; // 具体工厂A创建Windows风格产品族 class WinFactory : public GUIFactory { public: std::unique_ptrButton createButton() const override { return std::make_uniqueWinButton(); } std::unique_ptrTextBox createTextBox() const override { return std::make_uniqueWinTextBox(); } }; // 具体工厂B创建Mac风格产品族 class MacFactory : public GUIFactory { /* 类似返回MacButton和MacTextBox */ }; // 客户端代码 class Application { std::unique_ptrGUIFactory factory_; std::unique_ptrButton button_; std::unique_ptrTextBox textBox_; public: Application(std::unique_ptrGUIFactory factory) : factory_(std::move(factory)) { createUI(); } void createUI() { // 使用同一个工厂创建一套风格一致的产品 button_ factory_-createButton(); textBox_ factory_-createTextBox(); } void paint() { button_-render(); textBox_-render(); } }; // 使用 int main() { std::unique_ptrGUIFactory factory; #ifdef _WIN32 factory std::make_uniqueWinFactory(); #else factory std::make_uniqueMacFactory(); #endif Application app(std::move(factory)); app.paint(); // 绘制出一套风格统一的UI }核心价值保证产品兼容性。WinFactory保证创建出来的Button和TextBox都是Windows风格的。客户端Application完全不需要关心当前是什么平台、具体是什么控件类它只需要和抽象的GUIFactory、Button、TextBox打交道。更换平台只需在程序入口换一个具体的工厂对象整个应用的UI风格就全变了。抽象工厂 vs. 工厂方法关注点工厂方法关注单个产品的创建抽象工厂关注一系列相关产品的创建。扩展维度扩展工厂方法新增一个产品如LinuxButton需要新增一个具体工厂LinuxDialog。这是纵向扩展增加新的产品等级结构。扩展抽象工厂新增一个产品族如新增一个CheckBox需要修改所有具体工厂WinFactory,MacFactory的接口和实现。这是横向扩展在已有产品族中增加新产品。相对困难因为违反了开闭原则。但新增一个风格的产品族如LinuxFactory则很容易符合开闭原则。关系抽象工厂通常使用工厂方法来实现。在WinFactory::createButton()内部可能就是return std::make_uniqueWinButton();这本身就是一个工厂方法。注意事项抽象工厂模式虽然强大但它的缺点是“横向扩展”比较麻烦。如果你的产品族需要一起创建的、有关联的产品系列非常稳定很少增加新产品类型但可能需要增加新的产品族风格那么抽象工厂非常合适比如UI风格、数据库驱动等。反之如果产品类型经常变动抽象工厂可能需要频繁修改接口就不太适用。4. 观察者模式深度解析松耦合的事件通知机制观察者模式定义了对象间的一种一对多的依赖关系当一个对象主题的状态发生改变时所有依赖于它的对象观察者都会得到通知并自动更新。它也被称为发布-订阅模式。4.1 模式结构与经典实现场景一个气象站WeatherStation当温度、湿度、气压发生变化时需要实时更新多个显示设备比如当前状况显示器CurrentConditionsDisplay、气象统计显示器StatisticsDisplay和天气预报显示器ForecastDisplay。传统紧耦合做法气象站内部持有所有显示器的指针在数据更新时直接调用每个显示器的update方法。问题增加或移除一个显示器需要修改气象站代码气象站需要知道所有具体的显示器类。观察者模式解耦#include iostream #include memory #include vector #include algorithm // 前向声明 class Observer; // 主题被观察者接口 class Subject { public: virtual ~Subject() default; virtual void registerObserver(std::weak_ptrObserver obs) 0; virtual void removeObserver(std::weak_ptrObserver obs) 0; virtual void notifyObservers() 0; }; // 观察者接口 class Observer { public: virtual ~Observer() default; virtual void update(float temp, float humidity, float pressure) 0; }; // 具体主题气象站 class WeatherStation : public Subject { private: std::vectorstd::weak_ptrObserver observers_; float temperature_; float humidity_; float pressure_; // 清理失效的观察者weak_ptr可能已过期 void cleanupExpiredObservers() { observers_.erase( std::remove_if(observers_.begin(), observers_.end(), [](const std::weak_ptrObserver wp) { return wp.expired(); }), observers_.end()); } public: void registerObserver(std::weak_ptrObserver obs) override { observers_.push_back(obs); } void removeObserver(std::weak_ptrObserver obs) override { // 由于是weak_ptr直接比较可能不准通常在实际项目中会结合ID或标签来移除 // 这里简化处理先清理失效指针然后尝试锁定并比较 cleanupExpiredObservers(); auto it std::find_if(observers_.begin(), observers_.end(), [obs](const std::weak_ptrObserver wp) { auto sp1 wp.lock(); auto sp2 obs.lock(); return sp1 sp2 (sp1 sp2); }); if (it ! observers_.end()) { observers_.erase(it); } } void notifyObservers() override { cleanupExpiredObservers(); // 通知前先清理 for (auto weakObs : observers_) { if (auto obs weakObs.lock()) { // 尝试提升为shared_ptr obs-update(temperature_, humidity_, pressure_); } } } // 气象数据更新并通知所有观察者 void setMeasurements(float temp, float humidity, float pressure) { temperature_ temp; humidity_ humidity; pressure_ pressure; measurementsChanged(); // 数据变化触发通知 } private: void measurementsChanged() { notifyObservers(); } }; // 具体观察者当前状况显示器 class CurrentConditionsDisplay : public Observer, public std::enable_shared_from_thisCurrentConditionsDisplay { private: float temperature_; float humidity_; // 通常还会保存一个对Subject的引用以便于注册和注销这里省略 public: CurrentConditionsDisplay() default; void update(float temp, float humidity, float /*pressure*/) override { temperature_ temp; humidity_ humidity; display(); } void display() const { std::cout [CurrentConditions] Temp: temperature_ C, Humidity: humidity_ %\n; } }; // 具体观察者统计显示器 (省略类似实现) // class StatisticsDisplay : public Observer { ... }; // 客户端使用 int main() { auto weatherStation std::make_sharedWeatherStation(); auto currentDisplay std::make_sharedCurrentConditionsDisplay(); // auto statsDisplay std::make_sharedStatisticsDisplay(); // 观察者注册到主题 weatherStation-registerObserver(currentDisplay); // weatherStation-registerObserver(statsDisplay); // 模拟气象数据更新 weatherStation-setMeasurements(25.0f, 65.0f, 1013.0f); weatherStation-setMeasurements(26.5f, 70.0f, 1012.5f); // 移除观察者 weatherStation-removeObserver(currentDisplay); weatherStation-setMeasurements(27.0f, 68.0f, 1012.0f); // currentDisplay将不会收到通知 return 0; }关键点解析std::weak_ptr的使用这是现代C实现观察者模式时至关重要的一环。主题持有观察者的weak_ptr而不是shared_ptr或原始指针。为什么防止循环引用。如果主题持有shared_ptrObserver而观察者也持有shared_ptrSubject就会形成循环引用导致内存永远无法释放。weak_ptr是“弱引用”它不增加引用计数不会阻止所指向的对象被销毁。lock()方法在通知时使用weak_ptr::lock()尝试获取一个临时的shared_ptr。如果成功说明观察者对象还活着可以安全调用其方法如果失败返回nullptr说明观察者已被销毁本次跳过通知并在后续清理中移除该weak_ptr。std::enable_shared_from_this在CurrentConditionsDisplay中继承这个类是为了能在其成员函数内部安全地获取指向自身的shared_ptr或weak_ptr。这在需要将自身作为观察者注册到主题时非常有用虽然上面的例子是在main函数中注册的。松耦合WeatherStation只知道存在Observer这个接口完全不知道CurrentConditionsDisplay的具体存在。新增一个PhoneAppDisplay观察者气象站代码无需任何改动。4.2 推模型 vs. 拉模型上面的实现是典型的推模型主题在通知观察者时将更新的数据temp,humidity,pressure作为参数“推”给观察者。优点直接、高效观察者立即获得所需数据。缺点主题需要知道观察者需要哪些数据。如果不同的观察者需要的数据不同update方法的参数可能会变得很臃肿或者主题每次都需要推送所有数据可能造成浪费。另一种是拉模型主题在通知观察者时只传递一个指向自身的引用或指针。观察者收到通知后再主动从主题中“拉取”它需要的数据。// 拉模型主题接口 class Subject { public: virtual float getTemperature() const 0; virtual float getHumidity() const 0; virtual float getPressure() const 0; // ... 注册、移除、通知接口同上 }; // 拉模型观察者接口 class Observer { public: virtual void update(const Subject subject) 0; // 传入主题引用 }; // 具体观察者实现 void CurrentConditionsDisplay::update(const Subject subject) { temperature_ subject.getTemperature(); // 主动拉取需要的数据 humidity_ subject.getHumidity(); display(); }优点观察者可以按需索取数据主题接口更稳定。主题不需要关心观察者具体要什么。缺点观察者需要知道主题的接口getTemperature等增加了观察者对主题的依赖并且每次更新观察者都需要主动查询可能略微降低效率。如何选择如果所有观察者都需要主题的全部或大部分状态推模型更简洁。如果观察者需要的数据差异很大或者主题状态非常庞大拉模型更灵活。在实践中也可以结合使用或者定义不同的update重载版本。4.3 在现代C项目中的演进与实践经典的观察者模式实现已经很强大了但在大型项目中我们还可以结合现代C特性做得更好。1. 使用std::function和信号槽在很多场景下观察者可能只是一个函数或一个lambda表达式并不需要专门定义一个类。我们可以使用std::function来存储可调用对象。#include functional #include vector class SimpleSubject { private: float data_; std::vectorstd::functionvoid(float) observers_; // 存储回调函数 public: void registerObserver(std::functionvoid(float) obs) { observers_.push_back(obs); } void setData(float newData) { data_ newData; notifyObservers(); } void notifyObservers() { for (const auto obs : observers_) { obs(data_); // 调用回调函数 } } }; // 使用 SimpleSubject subject; // 注册一个lambda作为观察者 subject.registerObserver([](float data) { std::cout Data updated to: data std::endl; }); // 注册一个普通函数 subject.registerObserver(someGlobalFunction); // 注册一个成员函数需要bind subject.registerObserver(std::bind(MyClass::onDataChanged, myObj, std::placeholders::_1)); subject.setData(42.0f);这就是一个极简的“信号槽”机制。Qt框架的信号槽、Boost.Signals2库等都是基于这种思想的强大实现。它比经典观察者模式更灵活耦合度更低。2. 线程安全考虑如果主题和观察者可能在不同线程中被访问和修改那么registerObserver、removeObserver和notifyObservers都需要考虑线程安全。通常需要使用互斥锁std::mutex来保护观察者列表。#include mutex #include shared_mutex // C17 class ThreadSafeSubject : public Subject { private: mutable std::shared_mutex mtx_; // 读写锁C17 std::vectorstd::weak_ptrObserver observers_; // ... 其他数据成员 public: void registerObserver(std::weak_ptrObserver obs) override { std::unique_lock lock(mtx_); // 写锁 observers_.push_back(obs); } void notifyObservers() override { std::vectorstd::weak_ptrObserver observersCopy; { std::shared_lock lock(mtx_); // 读锁允许多个线程同时读 observersCopy observers_; // 复制一份减少锁持有时间 } // 遍历副本进行通知避免在通知过程中持有锁观察者的update可能很耗时或会回调主题 for (auto weakObs : observersCopy) { if (auto obs weakObs.lock()) { obs-update(...); } } // 注意对observers_的清理如移除失效指针可能需要额外的同步逻辑 } };重要提示在notifyObservers中调用观察者的update方法时绝对不要持有锁。因为update方法的具体实现是未知的它可能会执行很长时间或者它可能会回调主题的registerObserver/removeObserver方法从而导致死锁。这就是为什么上面代码先复制列表再通知的原因。3. 性能优化避免不必要的通知如果主题的状态变化非常频繁但观察者可能只对某些特定变化感兴趣例如温度变化超过1度才通知可以在主题内部增加一些逻辑只在满足条件时才调用notifyObservers。void WeatherStation::setMeasurements(float temp, float humidity, float pressure) { bool changed false; if (std::abs(temp - temperature_) 0.1f) { // 温度变化超过0.1度才算 temperature_ temp; changed true; } // ... 类似判断湿度和气压 if (changed) { measurementsChanged(); } }5. 模式联用与实战场景剖析设计模式很少孤立使用。在实际项目中工厂模式和观察者模式经常联手构建出灵活、可扩展的架构。实战场景一个插件化的事件处理系统假设我们在开发一个图像处理软件。软件核心是一个ImageProcessor主题它可以加载图像、进行各种处理滤镜、缩放等。处理过程中会产生各种事件“图像加载开始”、“滤镜应用完成”、“处理出错”等。我们希望有一个插件系统允许第三方开发者编写插件来响应这些事件比如一个LoggingPlugin将所有事件记录到文件。一个ProgressBarPlugin在UI上显示处理进度。一个AutoSavePlugin在每次滤镜应用后自动保存副本。如何设计观察者模式处理事件通知ImageProcessor作为主题维护一个观察者插件列表。当事件发生时通知所有已注册的插件。事件可以封装成一个Event类包含类型、时间、相关数据等。工厂模式管理插件创建插件需要动态加载比如从.dll或.so文件。我们不可能在核心代码里new LoggingPlugin()。这时需要一个PluginFactory。这个工厂可以读取配置文件根据插件名称利用运行时加载技术如dlopen/LoadLibrary找到插件的创建函数通常是一个约定的extern C create_plugin函数然后创建出具体的插件对象并返回抽象的Plugin即Observer指针给核心系统。代码示意// 事件和观察者接口 struct Event { enum Type { LoadStart, FilterComplete, Error }; Type type; std::string data; }; class Plugin : public Observer { public: virtual ~Plugin() default; virtual void onEvent(const Event e) override 0; virtual std::string name() const 0; }; // 抽象插件工厂 class PluginFactory { public: virtual std::unique_ptrPlugin createPlugin(const std::string pluginPath) 0; }; // 一个具体的动态库加载工厂 class DynamicLibPluginFactory : public PluginFactory { public: std::unique_ptrPlugin createPlugin(const std::string pluginPath) override { // 1. 调用系统API加载动态库 (pluginPath) // 2. 查找库中的 create_plugin 函数符号 // 3. 调用该函数获得一个 Plugin* 原始指针 // 4. 用 unique_ptr 包装并返回 // 5. 注意异常处理和资源清理 // ... 简化实现 return std::unique_ptrPlugin(/* 从动态库创建的插件对象 */); } }; // 核心图像处理器 class ImageProcessor : public Subject { std::vectorstd::weak_ptrObserver plugins_; std::unique_ptrPluginFactory pluginFactory_; public: void loadConfig(const std::string configFile) { // 读取配置文件获得插件路径列表 std::vectorstd::string pluginPaths {/*...*/}; for (const auto path : pluginPaths) { auto plugin pluginFactory_-createPlugin(path); if (plugin) { registerObserver(plugin); std::cout Loaded plugin: plugin-name() std::endl; } } } void processImage() { notifyObservers(Event{Event::LoadStart, image.jpg}); // ... 处理图像 notifyObservers(Event{Event::FilterComplete, GaussianBlur}); // ... } };在这个系统中观察者模式让ImageProcessor核心逻辑与具体的插件实现完全解耦。核心系统只负责发事件谁监听、监听后做什么它不关心。工厂模式特别是抽象工厂或工厂方法让核心系统与插件的具体创建过程复杂的动态库加载解耦。核心系统只需要说“给我一个这个路径的插件”工厂负责搞定一切。如果要换一种插件加载方式比如从网络下载只需要换一个工厂实现。这种“观察者 工厂”的组合是构建可扩展应用程序、框架和中间件的非常经典和强大的架构模式。6. 避坑指南与性能考量即使理解了原理在实际编码中还是会遇到很多坑。这里分享一些血泪教训。工厂模式的坑过度设计这是新手最容易犯的错。如果你的类层次很简单创建逻辑就是一行new或者产品类型几乎不会变那就别用工厂模式。直接new是最清晰、最直接的。引入模式会带来额外的抽象和复杂度。工厂类膨胀简单工厂的create方法容易变成巨无霸switch。解决方法是结合映射表std::unordered_mapstd::string, std::functionstd::unique_ptrProduct()来消除分支逻辑或者考虑升级到工厂方法模式。循环依赖如果具体产品类需要知道工厂类比如在构造函数中传入工厂引用而工厂类又需要包含产品类的头文件来创建它们就可能形成循环依赖。解决方法通常是使用前向声明并在工厂的实现文件.cpp中包含产品类的头文件而不是在工厂的头文件里。对象所有权工厂返回的指针所有权归谁在现代C中强烈建议返回std::unique_ptrProduct明确表示所有权转移给调用者。如果需要有共享所有权可以返回std::shared_ptr但这应该在工厂接口中明确约定。观察者模式的坑内存泄漏与悬挂指针这是最大的坑。主题持有观察者的原始指针如果观察者先于主题被销毁主题的观察者列表里就留下了“悬挂指针”后续通知会导致未定义行为。**必须使用std::weak_ptr**来管理观察者引用。同时观察者必须在析构前将自己从主题中注销。通知顺序依赖如果多个观察者对通知顺序有隐含依赖比如观察者A必须在观察者B之前更新那么这种依赖是脆弱的因为注册顺序可能改变。观察者模式本身不保证顺序。如果需要严格顺序可以考虑引入优先级机制或者在主题中明确管理顺序。在通知过程中修改观察者列表在notifyObservers遍历列表并调用update时如果某个update方法内部又调用了registerObserver或removeObserver可能会使正在遍历的迭代器失效导致崩溃。解决方法在遍历前复制一份观察者列表的副本遍历副本或者使用诸如“标记-清理”的策略在遍历过程中只标记要删除的观察者遍历完毕后再统一清理。性能问题频繁通知如果主题状态以极高频率变化比如游戏中的每一帧通知所有观察者可能成为性能瓶颈。可以考虑批量更新攒够一批变化再通知或让观察者自己轮询拉模型或将高频和低频观察者分开。锁竞争线程安全的观察者模式中锁可能成为瓶颈。可以考虑使用无锁数据结构或更细粒度的锁例如为每个观察者或每组观察者使用独立的锁但这会大大增加复杂度。对于大多数应用一个简单的读写锁保护整个列表已经足够。忘记注销观察者对象在销毁前必须确保自己已经从所有主题中注销。这通常需要在观察者的析构函数中处理。一种常见的做法是在观察者对象中保存一个它注册过的所有主题的弱引用列表在析构时遍历这个列表进行注销。7. 总结与个人体会写了这么多最后再强调几个我认为最关键的点也是我这些年用设计模式最深的体会第一模式是“药”不是“饭”。设计模式是用来解决特定设计问题的良药但没事别乱吃。如果你的代码很简单需求很稳定生搬硬套模式只会增加不必要的复杂度。识别出代码中真正的“痛点”比如增加一个新类型需要修改多处代码对象间关系混乱牵一发而动全身再对症下药选择最合适的模式。第二理解思想远比记住结构重要。工厂模式的核心思想是“将创建与使用分离”观察者模式的核心思想是“定义对象间的一种松耦合的联动关系”。只要你深刻理解了“依赖倒置”、“开闭原则”即使你忘了GoF书上画的类图在需要的时候你也能自己推导出类似的解决方案。这就是“无招胜有招”。第三C给了我们强大的工具要用好。现代C的智能指针unique_ptr,shared_ptr,weak_ptr、std::function、lambda表达式、移动语义等让实现这些模式变得更安全、更简洁、更高效。比如用weak_ptr解决观察者的生命周期问题用std::function实现轻量级的回调用std::make_unique让工厂方法返回资源所有权清晰的智能指针。拥抱现代C特性能让经典模式焕发新生。第四从框架和开源代码中学习。多看看优秀的开源项目如Chromium、LLVM、Boost等是怎么应用这些模式的。你会发现现实中的模式应用往往比教科书上的例子要复杂和灵活得多它们经常是多种模式混合使用并且根据具体场景做了很多变通。这能极大地提升你对模式的理解和运用能力。最后设计模式的学习和实践是一个持续的过程。不要指望读一篇文章、看一本书就能完全掌握。最好的方法就是在自己的项目中有意识地尝试使用。先从简单的场景开始用一用简单工厂用一用std::function做回调。遇到问题了回头再来看看原理思考有没有更好的模式可以应用。慢慢地你就会发现这些模式不再是书本上枯燥的定义而是你工具箱里顺手、可靠的工具。