1. 项目概述为什么C开发者绕不开设计模式干了这么多年C我见过太多项目初期跑得飞快代码写得随心所欲功能堆叠也快。但一到中期需求开始变化或者团队有新成员加入噩梦就开始了。加一个小功能要改十几个文件修一个老Bug牵一发而动全身生怕哪里又冒出个新问题。最后代码库变成了一个没人敢动的“屎山”维护成本指数级上升开发效率断崖式下跌。这时候大家才开始念叨“设计模式”。设计模式不是什么高深莫测的“银弹”它更像是一份由无数前辈程序员在大型项目“战场”上用血泪教训总结出来的“作战地图”或“标准操作流程”。它告诉你当遇到“需要根据不同类型创建对象”、“需要在对象间建立一种一对多的通知机制”、“需要动态地给一个对象添加一些职责”这类常见场景时有哪些经过验证的、相对优雅的代码组织方式。对于C这种强调性能和控制力同时又支持面向对象、泛型编程等多范式的语言来说理解并恰当运用设计模式是写出可维护、可扩展、可读的工业级代码的关键一步。可维护性意味着你的代码结构清晰模块职责分明后人包括三个月后的你自己能快速理解并安全修改。可扩展性意味着当新需求到来时你不需要推倒重来而是在现有框架上“增量”开发像乐高积木一样拼接新功能。可读性则意味着你的代码本身就是最好的文档类名、方法名和结构本身就传达了设计意图。设计模式正是为了系统性地达成这三个目标而存在的工具箱。2. 设计模式的核心基石C语言特性与设计原则在直接套用23种模式之前我们必须先打好地基。在C的世界里这个地基就是其独特的语言特性和那些更底层、更普适的设计原则。不理解这些设计模式就成了无源之水你只能死记硬背无法灵活运用。2.1 C实现设计模式的“武器库”设计模式在C中的实现高度依赖于其几个核心特性其中最关键的就是多态。2.1.1 动态多态运行时多态虚函数与继承体系这是实现大多数行为型模式如策略、观察者、命令和结构型模式如桥接、组合的基石。其核心在于“一个接口多种实现”。我们来看一个典型的虚函数表示例// 抽象接口稳定点 class IDataProcessor { public: virtual ~IDataProcessor() default; // 虚析构函数保证正确释放资源 virtual void process(const std::vectorint data) 0; // 纯虚函数定义接口 virtual std::string getName() const { return BaseProcessor; } // 非纯虚函数提供默认实现 }; // 具体实现A变化点 class SumProcessor : public IDataProcessor { public: void process(const std::vectorint data) override { int sum 0; for (int num : data) sum num; std::cout Sum: sum std::endl; } std::string getName() const override { return SumProcessor; } }; // 具体实现B变化点 class AvgProcessor : public IDataProcessor { public: void process(const std::vectorint data) override { if (data.empty()) return; double avg std::accumulate(data.begin(), data.end(), 0.0) / data.size(); std::cout Average: avg std::endl; } std::string getName() const override { return AvgProcessor; } }; // 客户代码依赖于抽象接口而非具体类 void clientCode(IDataProcessor processor, const std::vectorint data) { std::cout Using processor.getName() : ; processor.process(data); }这里的IDataProcessor就是“抽象”它定义了稳定的接口。SumProcessor和AvgProcessor是“具体实现”它们封装了可能变化的行为。clientCode函数只和抽象接口打交道它根本不在乎传进来的是求和处理器还是求平均处理器。这就是依赖倒置原则的体现高层模块clientCode不依赖低层模块具体Processor它们都依赖抽象IDataProcessor。当需要新增一个“求最大值处理器”时你只需要新增一个类继承IDataProcessor而clientCode一行代码都不用改——这就是对扩展开放对修改关闭。实操心得虚析构函数是生命线但凡一个类打算被继承并且会通过基类指针来删除派生类对象它的析构函数必须是virtual的。这是C里的一条铁律。否则通过Base* ptr new Derived(); delete ptr;这样的操作会导致派生类的析构函数不被调用引发资源泄漏。在定义抽象接口类时直接声明一个virtual ~Interface() default;是最佳实践。2.1.2 静态多态编译时多态模板与策略模式C的模板提供了另一种强大的抽象机制它在编译期完成类型绑定没有运行时开销非常适合性能敏感的场景。这常常是另一种实现“策略模式”或“模板方法模式”的方式。// 策略通过模板参数注入编译时确定 template typename ProcessingStrategy class DataProcessor { private: ProcessingStrategy strategy; public: void execute(const std::vectorint data) { // ... 一些通用的前置或后置处理 ... strategy.process(data); // ... 更多通用处理 ... } }; // 策略A求和 struct SumStrategy { void process(const std::vectorint data) { /* 求和实现 */ } }; // 策略B求平均 struct AvgStrategy { void process(const std::vectorint data) { /* 求平均实现 */ } }; // 使用 DataProcessorSumStrategy processor1; DataProcessorAvgStrategy processor2;模板方式牺牲了一些运行时动态替换策略的灵活性需要重新编译但换来了绝对的性能优势和清晰的接口约束策略类必须拥有process方法否则编译报错。2.1.3 对象组合优于继承的黄金法则“组合优于继承”是面向对象设计的一条核心原则在C中尤为重要。继承是一种“是-a”的强耦合关系而组合是一种“有-a”或“用-a”的弱耦合关系。// 继承方式Car 是一个 Engine (逻辑上并不总是合理) class Car : public Engine { ... }; // 组合方式Car 有一个 Engine class Car { private: std::unique_ptrEngine engine; // 通过指针持有利用多态 public: Car(std::unique_ptrEngine eng) : engine(std::move(eng)) {} void drive() { engine-start(); /* ... */ } };组合的好处显而易见降低耦合Car不依赖于Engine的具体实现类只依赖于抽象接口。可以轻松替换不同的Engine电动、燃油、混动。运行时动态配置可以在程序运行时决定给Car装配什么Engine。避免继承的缺陷如菱形继承问题、父类接口污染等。 很多设计模式如桥接模式、策略模式、装饰器模式其本质都是通过对象组合来动态改变对象的行为。2.2 七大设计原则模式背后的“道”设计模式是“术”而设计原则是“道”。这些原则是衡量代码设计好坏的高层指导方针。单一职责原则 (SRP)一个类应该只有一个引起它变化的原因。如果一个类既负责数据持久化又负责数据格式校验还负责业务逻辑计算那么任何一个需求的变更都可能需要修改它。应该将其拆分为PersistenceService、Validator和BusinessCalculator三个类。开放-封闭原则 (OCP)软件实体类、模块、函数应该对扩展开放对修改关闭。这是设计模式追求的核心目标。如上文的IDataProcessor例子新增处理器类型扩展很容易但使用处理器的客户端代码修改是关闭的。里氏替换原则 (LSP)子类型必须能够替换掉它们的父类型而不影响程序的正确性。这意味着子类不应该重写父类已实现好的非抽象方法导致行为发生破坏性改变。例如父类Rectangle有个方法setWidth子类Square重写它时同时修改了高度以保持正方形这就会违反LSP因为使用Rectangle接口的代码会假设宽高独立。接口隔离原则 (ISP)客户端不应该被迫依赖于它不使用的接口。将一个庞大的接口例如一个拥有print,scan,fax方法的IMachine拆分为多个更小、更具体的接口IPrinter,IScanner,IFax让客户端只需知道它们需要的方法。依赖倒置原则 (DIP)高层模块不应该依赖低层模块二者都应该依赖抽象。抽象不应该依赖细节细节应该依赖抽象。这直接引导我们使用面向接口编程如上文的clientCode依赖IDataProcessor抽象。迪米特法则/最少知识原则 (LoD)一个对象应该对其他对象有最少的了解。只与直接的朋友通信。降低类之间的耦合度。例如A类需要调用B类对象的某个方法而该方法需要C类对象作为参数。不好的做法是A去创建或获取C然后传给B。好的做法是B的方法内部自己去处理CA根本不需要知道C的存在。合成复用原则 (CARP)尽量使用对象组合而不是继承来达到复用的目的。这已在上一节详细讨论。这些原则是相互关联、相辅相成的。当你尝试运用一个设计模式时实际上是在同时实践其中的多项原则。例如使用策略模式你同时实践了OCP对扩展开放、DIP依赖抽象和CARP组合优于继承。3. 三大类设计模式在C中的实战解析设计模式通常分为创建型、结构型和行为型。我们挑出每个类别中最常用、最具代表性的几种结合C特性进行深度剖析。3.1 创建型模式如何优雅地“造对象”创建型模式关注对象实例化的过程将系统与它的对象创建、组合、表示方式分离。3.1.1 工厂方法模式将具体产品的创建延迟到子类当你无法预知需要创建哪种具体对象或者希望将对象的创建与使用代码解耦时工厂方法非常有用。// 产品接口 class IButton { public: virtual ~IButton() default; virtual void render() 0; virtual void onClick() 0; }; // 具体产品A class WindowsButton : public IButton { public: void render() override { std::cout 渲染一个Windows风格按钮 std::endl; } void onClick() override { std::cout Windows按钮被点击 std::endl; } }; // 具体产品B class WebButton : public IButton { public: void render() override { std::cout 渲染一个Web风格按钮 std::endl; } void onClick() override { std::cout Web按钮被点击触发Ajax请求 std::endl; } }; // 创建者抽象类 class Dialog { public: virtual ~Dialog() default; // 这就是“工厂方法” virtual std::unique_ptrIButton createButton() 0; void renderWindow() { // 使用工厂方法创建产品不依赖具体产品类 auto button createButton(); button-render(); // ... 渲染其他对话框组件 ... } }; // 具体创建者A class WindowsDialog : public Dialog { public: std::unique_ptrIButton createButton() override { return std::make_uniqueWindowsButton(); } }; // 具体创建者B class WebDialog : public Dialog { public: std::unique_ptrIButton createButton() override { return std::make_uniqueWebButton(); } }; // 使用 void application(Dialog dialog) { // application只依赖抽象的Dialog不知道具体是什么按钮 dialog.renderWindow(); }Dialog的renderWindow方法调用了抽象的createButton具体创建WindowsButton还是WebButton由WindowsDialog或WebDialog子类决定。这完美遵循了OCP和DIP。3.1.2 抽象工厂模式创建一系列相关或依赖的对象当需要创建一整套具有主题或家族性质的产品时例如为Windows GUI创建按钮、文本框、下拉框为Mac GUI创建另一套抽象工厂比工厂方法更合适。// 抽象产品族接口 class IButton { /* ... */ }; class ITextBox { /* ... */ }; class WinButton : public IButton { /* ... */ }; class WinTextBox : public ITextBox { /* ... */ }; class MacButton : public IButton { /* ... */ }; class MacTextBox : public ITextBox { /* ... */ }; // 抽象工厂 class IGUIFactory { public: virtual std::unique_ptrIButton createButton() 0; virtual std::unique_ptrITextBox createTextBox() 0; }; // 具体工厂 class WinFactory : public IGUIFactory { public: std::unique_ptrIButton createButton() override { return std::make_uniqueWinButton(); } std::unique_ptrITextBox createTextBox() override { return std::make_uniqueWinTextBox(); } }; class MacFactory : public IGUIFactory { public: std::unique_ptrIButton createButton() override { return std::make_uniqueMacButton(); } std::unique_ptrITextBox createTextBox() override { return std::make_uniqueMacTextBox(); } }; // 客户端代码 class Application { std::unique_ptrIGUIFactory factory; std::unique_ptrIButton button; std::unique_ptrITextBox textBox; public: Application(std::unique_ptrIGUIFactory fac) : factory(std::move(fac)) { button factory-createButton(); textBox factory-createTextBox(); } void paint() { button-render(); textBox-render(); } };Application通过传入不同的IGUIFactory实现WinFactory或MacFactory就能获得一套风格一致的产品而无需关心具体类。这保证了产品之间的兼容性。注意事项工厂模式的滥用不要为了用模式而用模式。如果类的创建逻辑非常简单直接new就足够了。只有当对象的创建过程复杂涉及配置、依赖其他对象、需要缓存等或者需要隔离创建逻辑以支持多种产品变体时才考虑工厂模式。滥用工厂会导致代码中充斥着大量只有细微差别的工厂类增加理解成本。3.1.3 单例模式确保一个类只有一个实例单例可能是最知名也最容易被误用的模式。在C中实现一个线程安全的单例需要一些技巧。class Singleton { private: Singleton() default; // 私有构造函数 ~Singleton() default; Singleton(const Singleton) delete; // 禁止拷贝 Singleton operator(const Singleton) delete; // 禁止赋值 static std::atomicSingleton* instance; // C11 原子指针 static std::mutex mtx; public: static Singleton* getInstance() { Singleton* tmp instance.load(std::memory_order_acquire); if (tmp nullptr) { std::lock_guardstd::mutex lock(mtx); tmp instance.load(std::memory_order_relaxed); if (tmp nullptr) { tmp new Singleton(); instance.store(tmp, std::memory_order_release); } } return tmp; } void doSomething() { /* ... */ } }; // C11以后更优雅的Meyers Singleton (线程安全利用局部静态变量) class MeyerSingleton { public: static MeyerSingleton getInstance() { static MeyerSingleton instance; // C11保证局部静态变量初始化是线程安全的 return instance; } void doSomething() { /* ... */ } private: MeyerSingleton() default; ~MeyerSingleton() default; MeyerSingleton(const MeyerSingleton) delete; MeyerSingleton operator(const MeyerSingleton) delete; };重要警告单例的陷阱单例本质上是全局变量它带来了隐式耦合使得单元测试变得困难难以模拟也违反了单一职责原则因为它同时管理自己的生命周期和业务逻辑。在现代C中应谨慎使用单例。优先考虑依赖注入将“单例”作为一个普通对象在应用顶层创建一次然后通过构造函数或参数传递给需要它的模块。3.2 结构型模式如何组合类和对象以形成更大的结构结构型模式关注如何将类或对象按某种布局组成更大的、功能更复杂的结构。3.2.1 适配器模式让不兼容的接口协同工作当你有一个现成的类其接口不符合你的系统需求时适配器可以充当“转换头”。// 已有的、不兼容的接口第三方库、遗留代码 class LegacyRectangle { public: void legacyDraw(int x1, int y1, int w, int h) { std::cout LegacyRectangle: 绘制点( x1 , y1 ), 宽 w , 高 h std::endl; } }; // 系统期望的标准接口 class Shape { public: virtual ~Shape() default; virtual void draw(int x, int y, int width, int height) 0; }; // 适配器继承目标接口组合被适配者 class RectangleAdapter : public Shape { private: LegacyRectangle legacyRect; public: void draw(int x, int y, int width, int height) override { // 将新接口的参数转换为旧接口所需的格式 int x1 x; int y1 y; int w width; int h height; legacyRect.legacyDraw(x1, y1, w, h); } }; // 客户端使用标准接口 void client(Shape shape) { shape.draw(10, 20, 30, 40); }适配器有两种实现方式类适配器通过多重继承在C中可行但不推荐因为耦合度高和对象适配器如上例通过组合更灵活。对象适配器是更常用的方式。3.2.2 装饰器模式动态地给对象添加额外职责装饰器模式提供了一种比继承更灵活的扩展功能的方式。它通过组合和委托在运行时透明地为对象添加行为。// 组件接口 class DataSource { public: virtual ~DataSource() default; virtual void writeData(const std::string data) 0; virtual std::string readData() 0; }; // 具体组件 class FileDataSource : public DataSource { std::string filename; public: FileDataSource(const std::string name) : filename(name) {} void writeData(const std::string data) override { std::cout 将数据 \ data \ 写入文件: filename std::endl; // 实际文件写入操作... } std::string readData() override { /* 从文件读取 */ return ; } }; // 装饰器基类维持一个对组件对象的引用并实现组件接口 class DataSourceDecorator : public DataSource { protected: std::unique_ptrDataSource wrappee; // 被包装的组件 public: DataSourceDecorator(std::unique_ptrDataSource source) : wrappee(std::move(source)) {} void writeData(const std::string data) override { wrappee-writeData(data); } std::string readData() override { return wrappee-readData(); } }; // 具体装饰器A加密装饰 class EncryptionDecorator : public DataSourceDecorator { public: using DataSourceDecorator::DataSourceDecorator; // 继承构造函数 void writeData(const std::string data) override { std::string encrypted 加密( data ); wrappee-writeData(encrypted); } std::string readData() override { std::string encrypted wrappee-readData(); // 解密操作... return encrypted.substr(3, encrypted.length() - 4); // 简单模拟解密 } }; // 具体装饰器B压缩装饰 class CompressionDecorator : public DataSourceDecorator { public: using DataSourceDecorator::DataSourceDecorator; void writeData(const std::string data) override { std::string compressed 压缩( data ); wrappee-writeData(compressed); } std::string readData() override { std::string compressed wrappee-readData(); // 解压操作... return compressed.substr(3, compressed.length() - 4); } }; // 客户端可以动态组合装饰器 int main() { // 一个普通的文件数据源 auto source std::make_uniqueFileDataSource(data.txt); source-writeData(Hello World); // 输出将数据 Hello World 写入文件: data.txt // 一个加密的文件数据源 auto encryptedSource std::make_uniqueEncryptionDecorator(std::make_uniqueFileDataSource(secret.txt)); encryptedSource-writeData(Sensitive Data); // 输出将数据 加密(Sensitive Data) 写入文件: secret.txt // 一个既压缩又加密的文件数据源 auto superSource std::make_uniqueCompressionDecorator( std::make_uniqueEncryptionDecorator( std::make_uniqueFileDataSource(super.secret))); superSource-writeData(Top Secret); // 输出将数据 压缩(加密(Top Secret)) 写入文件: super.secret }装饰器模式的优势在于你可以在运行时通过嵌套多个装饰器来组合任意多的新功能而无需修改底层组件或其他装饰器的代码。这比通过继承创建EncryptedAndCompressedFileDataSource这样的类要灵活得多。3.3 行为型模式对象间的高效职责分配与通信行为型模式关注对象之间如何交互、如何分配职责以及算法的封装。3.3.1 策略模式定义算法家族使其可互换策略模式将算法策略封装成独立的类使得它们可以相互替换让算法的变化独立于使用它的客户端。// 策略接口 class PaymentStrategy { public: virtual ~PaymentStrategy() default; virtual void pay(int amount) 0; }; // 具体策略 class CreditCardStrategy : public PaymentStrategy { std::string cardNumber; public: CreditCardStrategy(const std::string num) : cardNumber(num) {} void pay(int amount) override { std::cout 使用信用卡尾号 cardNumber.substr(cardNumber.length()-4) 支付了 amount 元。 std::endl; } }; class PayPalStrategy : public PaymentStrategy { std::string email; public: PayPalStrategy(const std::string e) : email(e) {} void pay(int amount) override { std::cout 使用PayPal账户 email 支付了 amount 元。 std::endl; } }; // 上下文Context class ShoppingCart { private: std::vectorint items; std::unique_ptrPaymentStrategy paymentStrategy; public: void addItem(int price) { items.push_back(price); } int calculateTotal() { int sum 0; for (int price : items) sum price; return sum; } void setPaymentStrategy(std::unique_ptrPaymentStrategy strategy) { paymentStrategy std::move(strategy); } void checkout() { int total calculateTotal(); if (paymentStrategy) { paymentStrategy-pay(total); } else { std::cout 请先设置支付方式。 std::endl; } } }; // 使用 int main() { ShoppingCart cart; cart.addItem(100); cart.addItem(50); cart.setPaymentStrategy(std::make_uniqueCreditCardStrategy(1234-5678-9876-5432)); cart.checkout(); // 输出信用卡支付信息 cart.setPaymentStrategy(std::make_uniquePayPalStrategy(userexample.com)); cart.checkout(); // 输出PayPal支付信息 }策略模式将支付这个“行为”抽象出来使得ShoppingCart上下文不再依赖于具体的支付实现。新增一种支付方式如数字货币只需要新增一个策略类ShoppingCart的代码完全不用动。这完美体现了OCP和DIP。3.3.2 观察者模式建立对象间的一对多依赖当一个对象主题的状态发生改变时所有依赖于它的对象观察者都会自动得到通知并更新。这是GUI事件处理、消息队列等场景的基石。#include iostream #include vector #include memory #include algorithm // 观察者接口 class IObserver { public: virtual ~IObserver() default; virtual void update(const std::string message) 0; }; // 主题被观察者接口 class ISubject { public: virtual ~ISubject() default; virtual void attach(std::shared_ptrIObserver observer) 0; virtual void detach(std::shared_ptrIObserver observer) 0; virtual void notify(const std::string message) 0; }; // 具体主题 class WeatherStation : public ISubject { private: std::vectorstd::weak_ptrIObserver observers; // 使用weak_ptr避免循环引用 float temperature; public: void setTemperature(float temp) { temperature temp; notify(温度已更新为: std::to_string(temp) °C); } void attach(std::shared_ptrIObserver observer) override { observers.push_back(std::weak_ptrIObserver(observer)); } void detach(std::shared_ptrIObserver observer) override { // 从weak_ptr列表中移除对应的observer需要一些处理这里简化 observers.erase( std::remove_if(observers.begin(), observers.end(), [observer](const std::weak_ptrIObserver wp) { auto sp wp.lock(); return !sp || sp observer; }), observers.end()); } void notify(const std::string message) override { // 遍历观察者通知更新 for (auto it observers.begin(); it ! observers.end(); ) { if (auto observer it-lock()) { observer-update(message); it; } else { // 观察者对象已失效移除 it observers.erase(it); } } } }; // 具体观察者A class PhoneDisplay : public IObserver { public: void update(const std::string message) override { std::cout [手机显示屏] 收到天气更新: message std::endl; } }; // 具体观察者B class DesktopDisplay : public IObserver { public: void update(const std::string message) override { std::cout [桌面显示屏] 天气数据: message std::endl; } }; // 使用 int main() { auto station std::make_sharedWeatherStation(); auto phone std::make_sharedPhoneDisplay(); auto desktop std::make_sharedDesktopDisplay(); station-attach(phone); station-attach(desktop); station-setTemperature(25.5f); // 输出 // [手机显示屏] 收到天气更新: 温度已更新为: 25.500000°C // [桌面显示屏] 天气数据: 温度已更新为: 25.500000°C station-detach(phone); station-setTemperature(30.0f); // 输出 // [桌面显示屏] 天气数据: 温度已更新为: 30.000000°C }实操心得观察者模式中的资源管理在C中实现观察者模式要特别注意主题和观察者之间的生命周期管理避免循环引用导致内存泄漏。上例中使用std::weak_ptr来持有观察者是解决此问题的经典方法。主题持有观察者的弱引用观察者持有主题的shared_ptr或普通指针。当观察者被销毁时主题的弱引用会自动失效在notify时被清理掉。4. 设计模式在C项目中的综合应用与避坑指南理解了单个模式后更重要的是如何在真实项目中组合运用它们并避开常见的陷阱。4.1 模式组合实战一个简单的游戏引擎组件设计假设我们在设计一个游戏引擎的简单渲染模块。需求是支持渲染多种图形形状并且能为这些图形动态添加不同的视觉效果如边框、阴影同时渲染器的创建需要灵活配置。我们可以综合运用多个模式工厂方法/抽象工厂用于创建不同类型的Shape圆形、矩形和不同的RendererOpenGL渲染器、Vulkan渲染器。装饰器模式用于为Shape动态添加BorderEffect、ShadowEffect等。策略模式将渲染算法封装在Renderer中使得Shape不依赖于具体的图形API。// 策略接口渲染器 class IRenderer { public: virtual ~IRenderer() default; virtual void renderCircle(float x, float y, float radius) 0; virtual void renderRectangle(float x, float y, float w, float h) 0; }; // 具体策略OpenGL渲染器 class OpenGLRenderer : public IRenderer { /* 实现OpenGL绘制命令 */ }; // 具体策略Vulkan渲染器 class VulkanRenderer : public IRenderer { /* 实现Vulkan绘制命令 */ }; // 组件接口图形 class IShape { protected: std::shared_ptrIRenderer renderer; // 组合一个渲染策略 public: IShape(std::shared_ptrIRenderer r) : renderer(r) {} virtual ~IShape() default; virtual void draw() 0; virtual std::string getName() const 0; }; // 具体组件圆形 class Circle : public IShape { float x, y, radius; public: Circle(float x, float y, float r, std::shared_ptrIRenderer rnd) : IShape(rnd), x(x), y(y), radius(r) {} void draw() override { renderer-renderCircle(x, y, radius); } std::string getName() const override { return Circle; } }; // 装饰器基类图形效果 class ShapeEffectDecorator : public IShape { protected: std::shared_ptrIShape wrappee; public: ShapeEffectDecorator(std::shared_ptrIShape shape) : IShape(shape-renderer), wrappee(shape) {} void draw() override { wrappee-draw(); // 先绘制原始图形 applyEffect(); // 再应用效果 } virtual void applyEffect() 0; }; // 具体装饰器边框效果 class BorderEffect : public ShapeEffectDecorator { public: using ShapeEffectDecorator::ShapeEffectDecorator; void applyEffect() override { std::cout 为 wrappee-getName() 应用了边框效果。 std::endl; // 实际会调用renderer的接口绘制边框 } }; // 工厂创建图形简单工厂示例 class ShapeFactory { public: static std::shared_ptrIShape createCircle(float x, float y, float r, std::shared_ptrIRenderer renderer) { return std::make_sharedCircle(x, y, r, renderer); } // ... 创建其他形状的方法 }; // 使用 int main() { auto glRenderer std::make_sharedOpenGLRenderer(); // auto vkRenderer std::make_sharedVulkanRenderer(); // 创建一个带边框的圆形 auto simpleCircle ShapeFactory::createCircle(100, 100, 50, glRenderer); auto fancyCircle std::make_sharedBorderEffect(simpleCircle); fancyCircle-draw(); // 输出调用OpenGLRenderer绘制圆形 // 为 Circle 应用了边框效果。 }这个例子展示了如何通过组合模式让系统各个部分图形、效果、渲染都能独立变化和扩展。新增一种图形、一种效果或一种渲染后端都只需要添加新的类而不需要修改现有代码。4.2 C设计模式常见“坑”与最佳实践过度设计Over-engineering这是新手最容易犯的错误。在需求简单、变化可能性极低的地方强行引入设计模式只会增加代码的复杂度和理解成本。准则一开始用最简单直接的方式实现功能。当变化第一次出现时进行重构而不是预判所有可能的变化。误用单例Singleton Abuse单例的全局状态使得代码难以测试并隐藏了类之间的依赖关系。准则优先考虑依赖注入。如果确实需要一个全局访问点考虑使用命名空间、静态类成员函数或者更优雅地使用一个明确的“应用上下文”或“服务定位器”这本身也是一个模式来管理这些“准单例”对象。深层次的继承链过深的继承层次会带来脆弱的基类问题、菱形继承等麻烦。准则遵守“组合优于继承”原则。如果继承层次超过2层就要思考是否可以用组合来替代。使用继承主要是为了“多态”而不是为了代码复用。模式混用导致结构混乱例如在装饰器内部又大量使用策略或者在工厂方法中硬编码大量条件判断。准则每个模式解决一个特定问题。保持模式职责的单一性。如果一个类变得过于复杂考虑将其拆分为更小的、职责单一的类。忽视C资源管理RAII在设计模式中对象的所有权和生命周期管理至关重要。准则充分利用智能指针std::unique_ptr,std::shared_ptr,std::weak_ptr来表达所有权语义。在工厂方法中返回std::unique_ptr在需要共享所有权时使用std::shared_ptr在观察者模式等可能引起循环引用的地方使用std::weak_ptr。忽略性能开销虚函数调用、动态内存分配new会带来一定的运行时开销。在性能极其关键的场景如游戏循环、高频交易需要谨慎评估。准则使用模板和静态多态CRTP奇异递归模板模式可以在编译期确定类型消除虚函数开销。但这也牺牲了部分运行时灵活性。生搬硬套GoF模式《设计模式》一书中的例子是基于Smalltalk和C的早期版本。有些模式在Modern CC11/14/17/20中有更优雅的实现方式或者已经被标准库组件所实现。准则例如迭代器模式已由STL迭代器完美实现命令模式可以用std::function简化观察者模式可以用信号/槽库如Boost.Signals2更安全地实现。多了解现代C特性和库。设计模式不是刻板的教条而是经验的总结。真正的精通是在深刻理解其意图和适用场景后能够忘记具体模式的“形”而把握其“神”在合适的时机用合适的方式写出清晰、灵活、健壮的代码。对于C开发者而言结合语言的强大特性RAII、模板、移动语义等来运用这些模式才能发挥出最大的威力。
C++设计模式实战:从多态到工厂模式,构建可维护的工业级代码
1. 项目概述为什么C开发者绕不开设计模式干了这么多年C我见过太多项目初期跑得飞快代码写得随心所欲功能堆叠也快。但一到中期需求开始变化或者团队有新成员加入噩梦就开始了。加一个小功能要改十几个文件修一个老Bug牵一发而动全身生怕哪里又冒出个新问题。最后代码库变成了一个没人敢动的“屎山”维护成本指数级上升开发效率断崖式下跌。这时候大家才开始念叨“设计模式”。设计模式不是什么高深莫测的“银弹”它更像是一份由无数前辈程序员在大型项目“战场”上用血泪教训总结出来的“作战地图”或“标准操作流程”。它告诉你当遇到“需要根据不同类型创建对象”、“需要在对象间建立一种一对多的通知机制”、“需要动态地给一个对象添加一些职责”这类常见场景时有哪些经过验证的、相对优雅的代码组织方式。对于C这种强调性能和控制力同时又支持面向对象、泛型编程等多范式的语言来说理解并恰当运用设计模式是写出可维护、可扩展、可读的工业级代码的关键一步。可维护性意味着你的代码结构清晰模块职责分明后人包括三个月后的你自己能快速理解并安全修改。可扩展性意味着当新需求到来时你不需要推倒重来而是在现有框架上“增量”开发像乐高积木一样拼接新功能。可读性则意味着你的代码本身就是最好的文档类名、方法名和结构本身就传达了设计意图。设计模式正是为了系统性地达成这三个目标而存在的工具箱。2. 设计模式的核心基石C语言特性与设计原则在直接套用23种模式之前我们必须先打好地基。在C的世界里这个地基就是其独特的语言特性和那些更底层、更普适的设计原则。不理解这些设计模式就成了无源之水你只能死记硬背无法灵活运用。2.1 C实现设计模式的“武器库”设计模式在C中的实现高度依赖于其几个核心特性其中最关键的就是多态。2.1.1 动态多态运行时多态虚函数与继承体系这是实现大多数行为型模式如策略、观察者、命令和结构型模式如桥接、组合的基石。其核心在于“一个接口多种实现”。我们来看一个典型的虚函数表示例// 抽象接口稳定点 class IDataProcessor { public: virtual ~IDataProcessor() default; // 虚析构函数保证正确释放资源 virtual void process(const std::vectorint data) 0; // 纯虚函数定义接口 virtual std::string getName() const { return BaseProcessor; } // 非纯虚函数提供默认实现 }; // 具体实现A变化点 class SumProcessor : public IDataProcessor { public: void process(const std::vectorint data) override { int sum 0; for (int num : data) sum num; std::cout Sum: sum std::endl; } std::string getName() const override { return SumProcessor; } }; // 具体实现B变化点 class AvgProcessor : public IDataProcessor { public: void process(const std::vectorint data) override { if (data.empty()) return; double avg std::accumulate(data.begin(), data.end(), 0.0) / data.size(); std::cout Average: avg std::endl; } std::string getName() const override { return AvgProcessor; } }; // 客户代码依赖于抽象接口而非具体类 void clientCode(IDataProcessor processor, const std::vectorint data) { std::cout Using processor.getName() : ; processor.process(data); }这里的IDataProcessor就是“抽象”它定义了稳定的接口。SumProcessor和AvgProcessor是“具体实现”它们封装了可能变化的行为。clientCode函数只和抽象接口打交道它根本不在乎传进来的是求和处理器还是求平均处理器。这就是依赖倒置原则的体现高层模块clientCode不依赖低层模块具体Processor它们都依赖抽象IDataProcessor。当需要新增一个“求最大值处理器”时你只需要新增一个类继承IDataProcessor而clientCode一行代码都不用改——这就是对扩展开放对修改关闭。实操心得虚析构函数是生命线但凡一个类打算被继承并且会通过基类指针来删除派生类对象它的析构函数必须是virtual的。这是C里的一条铁律。否则通过Base* ptr new Derived(); delete ptr;这样的操作会导致派生类的析构函数不被调用引发资源泄漏。在定义抽象接口类时直接声明一个virtual ~Interface() default;是最佳实践。2.1.2 静态多态编译时多态模板与策略模式C的模板提供了另一种强大的抽象机制它在编译期完成类型绑定没有运行时开销非常适合性能敏感的场景。这常常是另一种实现“策略模式”或“模板方法模式”的方式。// 策略通过模板参数注入编译时确定 template typename ProcessingStrategy class DataProcessor { private: ProcessingStrategy strategy; public: void execute(const std::vectorint data) { // ... 一些通用的前置或后置处理 ... strategy.process(data); // ... 更多通用处理 ... } }; // 策略A求和 struct SumStrategy { void process(const std::vectorint data) { /* 求和实现 */ } }; // 策略B求平均 struct AvgStrategy { void process(const std::vectorint data) { /* 求平均实现 */ } }; // 使用 DataProcessorSumStrategy processor1; DataProcessorAvgStrategy processor2;模板方式牺牲了一些运行时动态替换策略的灵活性需要重新编译但换来了绝对的性能优势和清晰的接口约束策略类必须拥有process方法否则编译报错。2.1.3 对象组合优于继承的黄金法则“组合优于继承”是面向对象设计的一条核心原则在C中尤为重要。继承是一种“是-a”的强耦合关系而组合是一种“有-a”或“用-a”的弱耦合关系。// 继承方式Car 是一个 Engine (逻辑上并不总是合理) class Car : public Engine { ... }; // 组合方式Car 有一个 Engine class Car { private: std::unique_ptrEngine engine; // 通过指针持有利用多态 public: Car(std::unique_ptrEngine eng) : engine(std::move(eng)) {} void drive() { engine-start(); /* ... */ } };组合的好处显而易见降低耦合Car不依赖于Engine的具体实现类只依赖于抽象接口。可以轻松替换不同的Engine电动、燃油、混动。运行时动态配置可以在程序运行时决定给Car装配什么Engine。避免继承的缺陷如菱形继承问题、父类接口污染等。 很多设计模式如桥接模式、策略模式、装饰器模式其本质都是通过对象组合来动态改变对象的行为。2.2 七大设计原则模式背后的“道”设计模式是“术”而设计原则是“道”。这些原则是衡量代码设计好坏的高层指导方针。单一职责原则 (SRP)一个类应该只有一个引起它变化的原因。如果一个类既负责数据持久化又负责数据格式校验还负责业务逻辑计算那么任何一个需求的变更都可能需要修改它。应该将其拆分为PersistenceService、Validator和BusinessCalculator三个类。开放-封闭原则 (OCP)软件实体类、模块、函数应该对扩展开放对修改关闭。这是设计模式追求的核心目标。如上文的IDataProcessor例子新增处理器类型扩展很容易但使用处理器的客户端代码修改是关闭的。里氏替换原则 (LSP)子类型必须能够替换掉它们的父类型而不影响程序的正确性。这意味着子类不应该重写父类已实现好的非抽象方法导致行为发生破坏性改变。例如父类Rectangle有个方法setWidth子类Square重写它时同时修改了高度以保持正方形这就会违反LSP因为使用Rectangle接口的代码会假设宽高独立。接口隔离原则 (ISP)客户端不应该被迫依赖于它不使用的接口。将一个庞大的接口例如一个拥有print,scan,fax方法的IMachine拆分为多个更小、更具体的接口IPrinter,IScanner,IFax让客户端只需知道它们需要的方法。依赖倒置原则 (DIP)高层模块不应该依赖低层模块二者都应该依赖抽象。抽象不应该依赖细节细节应该依赖抽象。这直接引导我们使用面向接口编程如上文的clientCode依赖IDataProcessor抽象。迪米特法则/最少知识原则 (LoD)一个对象应该对其他对象有最少的了解。只与直接的朋友通信。降低类之间的耦合度。例如A类需要调用B类对象的某个方法而该方法需要C类对象作为参数。不好的做法是A去创建或获取C然后传给B。好的做法是B的方法内部自己去处理CA根本不需要知道C的存在。合成复用原则 (CARP)尽量使用对象组合而不是继承来达到复用的目的。这已在上一节详细讨论。这些原则是相互关联、相辅相成的。当你尝试运用一个设计模式时实际上是在同时实践其中的多项原则。例如使用策略模式你同时实践了OCP对扩展开放、DIP依赖抽象和CARP组合优于继承。3. 三大类设计模式在C中的实战解析设计模式通常分为创建型、结构型和行为型。我们挑出每个类别中最常用、最具代表性的几种结合C特性进行深度剖析。3.1 创建型模式如何优雅地“造对象”创建型模式关注对象实例化的过程将系统与它的对象创建、组合、表示方式分离。3.1.1 工厂方法模式将具体产品的创建延迟到子类当你无法预知需要创建哪种具体对象或者希望将对象的创建与使用代码解耦时工厂方法非常有用。// 产品接口 class IButton { public: virtual ~IButton() default; virtual void render() 0; virtual void onClick() 0; }; // 具体产品A class WindowsButton : public IButton { public: void render() override { std::cout 渲染一个Windows风格按钮 std::endl; } void onClick() override { std::cout Windows按钮被点击 std::endl; } }; // 具体产品B class WebButton : public IButton { public: void render() override { std::cout 渲染一个Web风格按钮 std::endl; } void onClick() override { std::cout Web按钮被点击触发Ajax请求 std::endl; } }; // 创建者抽象类 class Dialog { public: virtual ~Dialog() default; // 这就是“工厂方法” virtual std::unique_ptrIButton createButton() 0; void renderWindow() { // 使用工厂方法创建产品不依赖具体产品类 auto button createButton(); button-render(); // ... 渲染其他对话框组件 ... } }; // 具体创建者A class WindowsDialog : public Dialog { public: std::unique_ptrIButton createButton() override { return std::make_uniqueWindowsButton(); } }; // 具体创建者B class WebDialog : public Dialog { public: std::unique_ptrIButton createButton() override { return std::make_uniqueWebButton(); } }; // 使用 void application(Dialog dialog) { // application只依赖抽象的Dialog不知道具体是什么按钮 dialog.renderWindow(); }Dialog的renderWindow方法调用了抽象的createButton具体创建WindowsButton还是WebButton由WindowsDialog或WebDialog子类决定。这完美遵循了OCP和DIP。3.1.2 抽象工厂模式创建一系列相关或依赖的对象当需要创建一整套具有主题或家族性质的产品时例如为Windows GUI创建按钮、文本框、下拉框为Mac GUI创建另一套抽象工厂比工厂方法更合适。// 抽象产品族接口 class IButton { /* ... */ }; class ITextBox { /* ... */ }; class WinButton : public IButton { /* ... */ }; class WinTextBox : public ITextBox { /* ... */ }; class MacButton : public IButton { /* ... */ }; class MacTextBox : public ITextBox { /* ... */ }; // 抽象工厂 class IGUIFactory { public: virtual std::unique_ptrIButton createButton() 0; virtual std::unique_ptrITextBox createTextBox() 0; }; // 具体工厂 class WinFactory : public IGUIFactory { public: std::unique_ptrIButton createButton() override { return std::make_uniqueWinButton(); } std::unique_ptrITextBox createTextBox() override { return std::make_uniqueWinTextBox(); } }; class MacFactory : public IGUIFactory { public: std::unique_ptrIButton createButton() override { return std::make_uniqueMacButton(); } std::unique_ptrITextBox createTextBox() override { return std::make_uniqueMacTextBox(); } }; // 客户端代码 class Application { std::unique_ptrIGUIFactory factory; std::unique_ptrIButton button; std::unique_ptrITextBox textBox; public: Application(std::unique_ptrIGUIFactory fac) : factory(std::move(fac)) { button factory-createButton(); textBox factory-createTextBox(); } void paint() { button-render(); textBox-render(); } };Application通过传入不同的IGUIFactory实现WinFactory或MacFactory就能获得一套风格一致的产品而无需关心具体类。这保证了产品之间的兼容性。注意事项工厂模式的滥用不要为了用模式而用模式。如果类的创建逻辑非常简单直接new就足够了。只有当对象的创建过程复杂涉及配置、依赖其他对象、需要缓存等或者需要隔离创建逻辑以支持多种产品变体时才考虑工厂模式。滥用工厂会导致代码中充斥着大量只有细微差别的工厂类增加理解成本。3.1.3 单例模式确保一个类只有一个实例单例可能是最知名也最容易被误用的模式。在C中实现一个线程安全的单例需要一些技巧。class Singleton { private: Singleton() default; // 私有构造函数 ~Singleton() default; Singleton(const Singleton) delete; // 禁止拷贝 Singleton operator(const Singleton) delete; // 禁止赋值 static std::atomicSingleton* instance; // C11 原子指针 static std::mutex mtx; public: static Singleton* getInstance() { Singleton* tmp instance.load(std::memory_order_acquire); if (tmp nullptr) { std::lock_guardstd::mutex lock(mtx); tmp instance.load(std::memory_order_relaxed); if (tmp nullptr) { tmp new Singleton(); instance.store(tmp, std::memory_order_release); } } return tmp; } void doSomething() { /* ... */ } }; // C11以后更优雅的Meyers Singleton (线程安全利用局部静态变量) class MeyerSingleton { public: static MeyerSingleton getInstance() { static MeyerSingleton instance; // C11保证局部静态变量初始化是线程安全的 return instance; } void doSomething() { /* ... */ } private: MeyerSingleton() default; ~MeyerSingleton() default; MeyerSingleton(const MeyerSingleton) delete; MeyerSingleton operator(const MeyerSingleton) delete; };重要警告单例的陷阱单例本质上是全局变量它带来了隐式耦合使得单元测试变得困难难以模拟也违反了单一职责原则因为它同时管理自己的生命周期和业务逻辑。在现代C中应谨慎使用单例。优先考虑依赖注入将“单例”作为一个普通对象在应用顶层创建一次然后通过构造函数或参数传递给需要它的模块。3.2 结构型模式如何组合类和对象以形成更大的结构结构型模式关注如何将类或对象按某种布局组成更大的、功能更复杂的结构。3.2.1 适配器模式让不兼容的接口协同工作当你有一个现成的类其接口不符合你的系统需求时适配器可以充当“转换头”。// 已有的、不兼容的接口第三方库、遗留代码 class LegacyRectangle { public: void legacyDraw(int x1, int y1, int w, int h) { std::cout LegacyRectangle: 绘制点( x1 , y1 ), 宽 w , 高 h std::endl; } }; // 系统期望的标准接口 class Shape { public: virtual ~Shape() default; virtual void draw(int x, int y, int width, int height) 0; }; // 适配器继承目标接口组合被适配者 class RectangleAdapter : public Shape { private: LegacyRectangle legacyRect; public: void draw(int x, int y, int width, int height) override { // 将新接口的参数转换为旧接口所需的格式 int x1 x; int y1 y; int w width; int h height; legacyRect.legacyDraw(x1, y1, w, h); } }; // 客户端使用标准接口 void client(Shape shape) { shape.draw(10, 20, 30, 40); }适配器有两种实现方式类适配器通过多重继承在C中可行但不推荐因为耦合度高和对象适配器如上例通过组合更灵活。对象适配器是更常用的方式。3.2.2 装饰器模式动态地给对象添加额外职责装饰器模式提供了一种比继承更灵活的扩展功能的方式。它通过组合和委托在运行时透明地为对象添加行为。// 组件接口 class DataSource { public: virtual ~DataSource() default; virtual void writeData(const std::string data) 0; virtual std::string readData() 0; }; // 具体组件 class FileDataSource : public DataSource { std::string filename; public: FileDataSource(const std::string name) : filename(name) {} void writeData(const std::string data) override { std::cout 将数据 \ data \ 写入文件: filename std::endl; // 实际文件写入操作... } std::string readData() override { /* 从文件读取 */ return ; } }; // 装饰器基类维持一个对组件对象的引用并实现组件接口 class DataSourceDecorator : public DataSource { protected: std::unique_ptrDataSource wrappee; // 被包装的组件 public: DataSourceDecorator(std::unique_ptrDataSource source) : wrappee(std::move(source)) {} void writeData(const std::string data) override { wrappee-writeData(data); } std::string readData() override { return wrappee-readData(); } }; // 具体装饰器A加密装饰 class EncryptionDecorator : public DataSourceDecorator { public: using DataSourceDecorator::DataSourceDecorator; // 继承构造函数 void writeData(const std::string data) override { std::string encrypted 加密( data ); wrappee-writeData(encrypted); } std::string readData() override { std::string encrypted wrappee-readData(); // 解密操作... return encrypted.substr(3, encrypted.length() - 4); // 简单模拟解密 } }; // 具体装饰器B压缩装饰 class CompressionDecorator : public DataSourceDecorator { public: using DataSourceDecorator::DataSourceDecorator; void writeData(const std::string data) override { std::string compressed 压缩( data ); wrappee-writeData(compressed); } std::string readData() override { std::string compressed wrappee-readData(); // 解压操作... return compressed.substr(3, compressed.length() - 4); } }; // 客户端可以动态组合装饰器 int main() { // 一个普通的文件数据源 auto source std::make_uniqueFileDataSource(data.txt); source-writeData(Hello World); // 输出将数据 Hello World 写入文件: data.txt // 一个加密的文件数据源 auto encryptedSource std::make_uniqueEncryptionDecorator(std::make_uniqueFileDataSource(secret.txt)); encryptedSource-writeData(Sensitive Data); // 输出将数据 加密(Sensitive Data) 写入文件: secret.txt // 一个既压缩又加密的文件数据源 auto superSource std::make_uniqueCompressionDecorator( std::make_uniqueEncryptionDecorator( std::make_uniqueFileDataSource(super.secret))); superSource-writeData(Top Secret); // 输出将数据 压缩(加密(Top Secret)) 写入文件: super.secret }装饰器模式的优势在于你可以在运行时通过嵌套多个装饰器来组合任意多的新功能而无需修改底层组件或其他装饰器的代码。这比通过继承创建EncryptedAndCompressedFileDataSource这样的类要灵活得多。3.3 行为型模式对象间的高效职责分配与通信行为型模式关注对象之间如何交互、如何分配职责以及算法的封装。3.3.1 策略模式定义算法家族使其可互换策略模式将算法策略封装成独立的类使得它们可以相互替换让算法的变化独立于使用它的客户端。// 策略接口 class PaymentStrategy { public: virtual ~PaymentStrategy() default; virtual void pay(int amount) 0; }; // 具体策略 class CreditCardStrategy : public PaymentStrategy { std::string cardNumber; public: CreditCardStrategy(const std::string num) : cardNumber(num) {} void pay(int amount) override { std::cout 使用信用卡尾号 cardNumber.substr(cardNumber.length()-4) 支付了 amount 元。 std::endl; } }; class PayPalStrategy : public PaymentStrategy { std::string email; public: PayPalStrategy(const std::string e) : email(e) {} void pay(int amount) override { std::cout 使用PayPal账户 email 支付了 amount 元。 std::endl; } }; // 上下文Context class ShoppingCart { private: std::vectorint items; std::unique_ptrPaymentStrategy paymentStrategy; public: void addItem(int price) { items.push_back(price); } int calculateTotal() { int sum 0; for (int price : items) sum price; return sum; } void setPaymentStrategy(std::unique_ptrPaymentStrategy strategy) { paymentStrategy std::move(strategy); } void checkout() { int total calculateTotal(); if (paymentStrategy) { paymentStrategy-pay(total); } else { std::cout 请先设置支付方式。 std::endl; } } }; // 使用 int main() { ShoppingCart cart; cart.addItem(100); cart.addItem(50); cart.setPaymentStrategy(std::make_uniqueCreditCardStrategy(1234-5678-9876-5432)); cart.checkout(); // 输出信用卡支付信息 cart.setPaymentStrategy(std::make_uniquePayPalStrategy(userexample.com)); cart.checkout(); // 输出PayPal支付信息 }策略模式将支付这个“行为”抽象出来使得ShoppingCart上下文不再依赖于具体的支付实现。新增一种支付方式如数字货币只需要新增一个策略类ShoppingCart的代码完全不用动。这完美体现了OCP和DIP。3.3.2 观察者模式建立对象间的一对多依赖当一个对象主题的状态发生改变时所有依赖于它的对象观察者都会自动得到通知并更新。这是GUI事件处理、消息队列等场景的基石。#include iostream #include vector #include memory #include algorithm // 观察者接口 class IObserver { public: virtual ~IObserver() default; virtual void update(const std::string message) 0; }; // 主题被观察者接口 class ISubject { public: virtual ~ISubject() default; virtual void attach(std::shared_ptrIObserver observer) 0; virtual void detach(std::shared_ptrIObserver observer) 0; virtual void notify(const std::string message) 0; }; // 具体主题 class WeatherStation : public ISubject { private: std::vectorstd::weak_ptrIObserver observers; // 使用weak_ptr避免循环引用 float temperature; public: void setTemperature(float temp) { temperature temp; notify(温度已更新为: std::to_string(temp) °C); } void attach(std::shared_ptrIObserver observer) override { observers.push_back(std::weak_ptrIObserver(observer)); } void detach(std::shared_ptrIObserver observer) override { // 从weak_ptr列表中移除对应的observer需要一些处理这里简化 observers.erase( std::remove_if(observers.begin(), observers.end(), [observer](const std::weak_ptrIObserver wp) { auto sp wp.lock(); return !sp || sp observer; }), observers.end()); } void notify(const std::string message) override { // 遍历观察者通知更新 for (auto it observers.begin(); it ! observers.end(); ) { if (auto observer it-lock()) { observer-update(message); it; } else { // 观察者对象已失效移除 it observers.erase(it); } } } }; // 具体观察者A class PhoneDisplay : public IObserver { public: void update(const std::string message) override { std::cout [手机显示屏] 收到天气更新: message std::endl; } }; // 具体观察者B class DesktopDisplay : public IObserver { public: void update(const std::string message) override { std::cout [桌面显示屏] 天气数据: message std::endl; } }; // 使用 int main() { auto station std::make_sharedWeatherStation(); auto phone std::make_sharedPhoneDisplay(); auto desktop std::make_sharedDesktopDisplay(); station-attach(phone); station-attach(desktop); station-setTemperature(25.5f); // 输出 // [手机显示屏] 收到天气更新: 温度已更新为: 25.500000°C // [桌面显示屏] 天气数据: 温度已更新为: 25.500000°C station-detach(phone); station-setTemperature(30.0f); // 输出 // [桌面显示屏] 天气数据: 温度已更新为: 30.000000°C }实操心得观察者模式中的资源管理在C中实现观察者模式要特别注意主题和观察者之间的生命周期管理避免循环引用导致内存泄漏。上例中使用std::weak_ptr来持有观察者是解决此问题的经典方法。主题持有观察者的弱引用观察者持有主题的shared_ptr或普通指针。当观察者被销毁时主题的弱引用会自动失效在notify时被清理掉。4. 设计模式在C项目中的综合应用与避坑指南理解了单个模式后更重要的是如何在真实项目中组合运用它们并避开常见的陷阱。4.1 模式组合实战一个简单的游戏引擎组件设计假设我们在设计一个游戏引擎的简单渲染模块。需求是支持渲染多种图形形状并且能为这些图形动态添加不同的视觉效果如边框、阴影同时渲染器的创建需要灵活配置。我们可以综合运用多个模式工厂方法/抽象工厂用于创建不同类型的Shape圆形、矩形和不同的RendererOpenGL渲染器、Vulkan渲染器。装饰器模式用于为Shape动态添加BorderEffect、ShadowEffect等。策略模式将渲染算法封装在Renderer中使得Shape不依赖于具体的图形API。// 策略接口渲染器 class IRenderer { public: virtual ~IRenderer() default; virtual void renderCircle(float x, float y, float radius) 0; virtual void renderRectangle(float x, float y, float w, float h) 0; }; // 具体策略OpenGL渲染器 class OpenGLRenderer : public IRenderer { /* 实现OpenGL绘制命令 */ }; // 具体策略Vulkan渲染器 class VulkanRenderer : public IRenderer { /* 实现Vulkan绘制命令 */ }; // 组件接口图形 class IShape { protected: std::shared_ptrIRenderer renderer; // 组合一个渲染策略 public: IShape(std::shared_ptrIRenderer r) : renderer(r) {} virtual ~IShape() default; virtual void draw() 0; virtual std::string getName() const 0; }; // 具体组件圆形 class Circle : public IShape { float x, y, radius; public: Circle(float x, float y, float r, std::shared_ptrIRenderer rnd) : IShape(rnd), x(x), y(y), radius(r) {} void draw() override { renderer-renderCircle(x, y, radius); } std::string getName() const override { return Circle; } }; // 装饰器基类图形效果 class ShapeEffectDecorator : public IShape { protected: std::shared_ptrIShape wrappee; public: ShapeEffectDecorator(std::shared_ptrIShape shape) : IShape(shape-renderer), wrappee(shape) {} void draw() override { wrappee-draw(); // 先绘制原始图形 applyEffect(); // 再应用效果 } virtual void applyEffect() 0; }; // 具体装饰器边框效果 class BorderEffect : public ShapeEffectDecorator { public: using ShapeEffectDecorator::ShapeEffectDecorator; void applyEffect() override { std::cout 为 wrappee-getName() 应用了边框效果。 std::endl; // 实际会调用renderer的接口绘制边框 } }; // 工厂创建图形简单工厂示例 class ShapeFactory { public: static std::shared_ptrIShape createCircle(float x, float y, float r, std::shared_ptrIRenderer renderer) { return std::make_sharedCircle(x, y, r, renderer); } // ... 创建其他形状的方法 }; // 使用 int main() { auto glRenderer std::make_sharedOpenGLRenderer(); // auto vkRenderer std::make_sharedVulkanRenderer(); // 创建一个带边框的圆形 auto simpleCircle ShapeFactory::createCircle(100, 100, 50, glRenderer); auto fancyCircle std::make_sharedBorderEffect(simpleCircle); fancyCircle-draw(); // 输出调用OpenGLRenderer绘制圆形 // 为 Circle 应用了边框效果。 }这个例子展示了如何通过组合模式让系统各个部分图形、效果、渲染都能独立变化和扩展。新增一种图形、一种效果或一种渲染后端都只需要添加新的类而不需要修改现有代码。4.2 C设计模式常见“坑”与最佳实践过度设计Over-engineering这是新手最容易犯的错误。在需求简单、变化可能性极低的地方强行引入设计模式只会增加代码的复杂度和理解成本。准则一开始用最简单直接的方式实现功能。当变化第一次出现时进行重构而不是预判所有可能的变化。误用单例Singleton Abuse单例的全局状态使得代码难以测试并隐藏了类之间的依赖关系。准则优先考虑依赖注入。如果确实需要一个全局访问点考虑使用命名空间、静态类成员函数或者更优雅地使用一个明确的“应用上下文”或“服务定位器”这本身也是一个模式来管理这些“准单例”对象。深层次的继承链过深的继承层次会带来脆弱的基类问题、菱形继承等麻烦。准则遵守“组合优于继承”原则。如果继承层次超过2层就要思考是否可以用组合来替代。使用继承主要是为了“多态”而不是为了代码复用。模式混用导致结构混乱例如在装饰器内部又大量使用策略或者在工厂方法中硬编码大量条件判断。准则每个模式解决一个特定问题。保持模式职责的单一性。如果一个类变得过于复杂考虑将其拆分为更小的、职责单一的类。忽视C资源管理RAII在设计模式中对象的所有权和生命周期管理至关重要。准则充分利用智能指针std::unique_ptr,std::shared_ptr,std::weak_ptr来表达所有权语义。在工厂方法中返回std::unique_ptr在需要共享所有权时使用std::shared_ptr在观察者模式等可能引起循环引用的地方使用std::weak_ptr。忽略性能开销虚函数调用、动态内存分配new会带来一定的运行时开销。在性能极其关键的场景如游戏循环、高频交易需要谨慎评估。准则使用模板和静态多态CRTP奇异递归模板模式可以在编译期确定类型消除虚函数开销。但这也牺牲了部分运行时灵活性。生搬硬套GoF模式《设计模式》一书中的例子是基于Smalltalk和C的早期版本。有些模式在Modern CC11/14/17/20中有更优雅的实现方式或者已经被标准库组件所实现。准则例如迭代器模式已由STL迭代器完美实现命令模式可以用std::function简化观察者模式可以用信号/槽库如Boost.Signals2更安全地实现。多了解现代C特性和库。设计模式不是刻板的教条而是经验的总结。真正的精通是在深刻理解其意图和适用场景后能够忘记具体模式的“形”而把握其“神”在合适的时机用合适的方式写出清晰、灵活、健壮的代码。对于C开发者而言结合语言的强大特性RAII、模板、移动语义等来运用这些模式才能发挥出最大的威力。