C++设计模式实战:从Java到C++的思维转换与现代化实现
1. 项目概述为什么用C重写《Head First 设计模式》如果你和我一样是个从C入门的开发者第一次翻开《Head First 设计模式》这本书时心情大概是既兴奋又有点别扭。兴奋的是书里那些生动的比喻和场景确实把抽象的设计模式讲得活灵活现别扭的是满眼的Java代码示例总让人觉得隔了一层。书里大谈特谈“针对接口编程”但C里的“接口”和Java的interface能是一回事吗书里说观察者模式要维护一个观察者列表那在C里这个列表里的指针生命周期谁来管内存泄露了怎么办这就是我做这个“C案例笔记”项目的初衷。它不是简单地照搬书上的Java代码然后用C语法翻译一遍。那样做意义不大因为两种语言的核心哲学和资源管理模型截然不同。我的目标是以《Head First 设计模式》为蓝本结合C的语言特性比如RAII、智能指针、值语义、移动语义重新思考和实现每一个模式记录下在转换过程中遇到的“坑”以及C视角下对模式本质的理解。这更像是一次穿越语言屏障的设计思想探险最终产出的代码和笔记是给那些熟悉C、又想深入理解设计模式的同行们的一份实战参考。你会发现有些模式在C里实现起来更自然比如策略模式用函数对象或std::function简直天生一对而有些则会面临挑战比如观察者模式中对象生命周期的同步问题。这个过程会迫使你不仅理解模式“是什么”更去深究“为什么”要这么设计以及在C的语境下“如何”做得更好、更安全。接下来我们就从最基础的策略模式开始看看这场探险的第一站会遇到什么。2. 环境准备与项目结构搭建2.1 开发环境与工具链选择工欲善其事必先利其器。虽然设计模式的核心是思想不依赖于特定工具但一个顺手的C开发环境能让你更专注于设计本身而不是和编译器搏斗。首先我选择了Visual Studio 2019/2022作为主要的集成开发环境IDE。原因很简单它对C标准支持比较新确保能用上C11/14/17的特性调试器强大直观对于在Windows平台上的开发非常友好。当然如果你偏爱跨平台CLion或VSCode CMake Tools插件也是极好的选择。关键在于你的环境要能方便地管理项目、设置断点、查看内存。注意很多新手在配置VSCode的C环境时会卡在“Microsoft Visual C 14.0 or greater is required”这类错误上。这通常是因为缺少构建工具。最省事的办法是直接安装Visual Studio Build Tools或者完整版的Visual Studio选择“使用C的桌面开发”工作负载。别试图手动拼凑各种Redistributable那是一条荆棘之路。编译器方面我使用了MSVCVisual Studio自带和GCC通过WSL2或MinGW进行交叉验证。这能帮你发现一些平台相关的未定义行为。项目构建工具我直接用了Visual Studio的解决方案.sln和项目文件.vcxproj因为结构简单直观。对于更复杂的、需要跨平台的项目CMake是现在的行业标准它能让你的项目结构更清晰也方便其他人构建。2.2 项目命名与结构规范清晰的代码结构是良好设计的开端。参考了开源社区的一些惯例和我个人的习惯我为这个项目制定了简单的规范根目录以HeadFirstDesignPatternsCpp命名一目了然。模式隔离每个设计模式独占一个文件夹例如DesignPatterns.Strategy。这样物理上就隔离了不同模式的代码避免耦合也方便单独编译、测试和删除。解决方案文件在根目录放置HeadFirstDesignPatterns.sln将所有模式的项目文件汇聚在一起管理。代码文件组织在每个模式文件夹内通常包含PatternName.h/PatternName.cpp模式的核心接口和类定义。Main.cpp或Example.cpp该模式的使用示例和测试代码。可能还会有ConcreteClassA.h等具体的实现类文件。关于命名我采用了一套自定的规则虽然不像Google C Style Guide那样全面但足够让代码意图清晰接口类以大写字母I开头例如IFlyBehavior。在C中这通常是一个只包含纯虚函数的抽象基类。具体实现类以大写字母C开头例如CFlyWithWings。这个C代表“Concrete”具体的或“Class”有助于快速区分接口和实现。设计模式命名空间为了避免全局命名污染也为了一目了然地知道这段代码属于哪个模式我为每个模式定义了独立的命名空间格式如DesignPatterns_Strategy。虽然名字有点长但在阅读代码时非常有用。这样的结构虽然简单但保证了项目的可维护性和可读性尤其当你实现的模式越来越多时不会变成一团乱麻。3. 核心模式解析与C实现精讲3.1 策略模式从“橡皮鸭”到std::function策略模式是《Head First》开篇第一个模式也是理解“封装变化”和“组合优于继承”的绝佳范例。书中的例子是鸭子模拟器不同的鸭子有不同的飞行fly和呱呱叫quack行为。与其为每一种鸭子创建子类并重写这些方法导致爆炸的子类和维护噩梦不如将行为抽象出来成为一组可互换的“策略”。3.1.1 Java思路的直译与困境最初我试图忠实地翻译Java版本定义IFlyBehavior和IQuackBehavior两个接口抽象基类然后为每种行为创建实现类如FlyWithWings,FlyNoWay,Quack,Squeak。Duck基类持有这两个接口的指针并在运行时通过SetFlyBehavior等方法动态更换行为。// 直译思路示例 class IFlyBehavior { public: virtual void Fly() 0; virtual ~IFlyBehavior() default; }; class FlyWithWings : public IFlyBehavior { public: void Fly() override { std::cout Im flying with wings!\n; } }; class Duck { protected: std::unique_ptrIFlyBehavior flyBehavior; std::unique_ptrIQuackBehavior quackBehavior; public: void PerformFly() { if (flyBehavior) flyBehavior-Fly(); } void SetFlyBehavior(std::unique_ptrIFlyBehavior fb) { flyBehavior std::move(fb); } // ... 其他方法 };这样做没问题也完全体现了策略模式的思想。但它引入了额外的类层次结构一个接口N个实现对于简单的行为比如只是打印一行字符串显得有些“重”。3.1.2 C的现代化实现函数对象与std::functionC提供了更轻量、更灵活的“策略”载体函数对象Functor和std::function。一个行为策略本质上就是一个可调用对象Callable。在C11之后我们可以这样做#include functional #include iostream #include memory class Duck { public: // 使用 std::function 替代具体的接口类 using FlyBehavior std::functionvoid(); using QuackBehavior std::functionvoid(); void PerformFly() const { if (flyBehavior_) { flyBehavior_(); } } void PerformQuack() const { if (quackBehavior_) { quackBehavior_(); } } void SetFlyBehavior(FlyBehavior fb) { flyBehavior_ std::move(fb); } void SetQuackBehavior(QuackBehavior qb) { quackBehavior_ std::move(qb); } // ... 其他公共接口如 Display() 等 private: FlyBehavior flyBehavior_; QuackBehavior quackBehavior_; }; // 使用示例 int main() { Duck mallardDuck; // 策略1使用lambda表达式最简洁 mallardDuck.SetFlyBehavior([]() { std::cout Flying high with wings!\n; }); mallardDuck.SetQuackBehavior([]() { std::cout Quack! Quack!\n; }); // 策略2使用普通函数 void RocketFly() { std::cout Zooooom! Rocket powered flight!\n; } mallardDuck.SetFlyBehavior(RocketFly); // 策略3使用函数对象仿函数 class SqueakSound { public: void operator()() const { std::cout Squeak! Squeak!\n; } }; mallardDuck.SetQuackBehavior(SqueakSound{}); mallardDuck.PerformFly(); // 输出Flying high with wings! mallardDuck.PerformQuack(); // 输出Quack! Quack! // 动态改变行为 mallardDuck.SetFlyBehavior(RocketFly); mallardDuck.PerformFly(); // 输出Zooooom! Rocket powered flight! return 0; }为什么这样更好更少的样板代码无需为每个行为定义接口类和一系列实现类。Lambda、普通函数、函数对象、成员函数指针等都可以直接作为策略代码量大幅减少。更强的表现力std::function可以绑定任何可调用实体策略的来源极其灵活。符合C习惯这是现代C中实现回调、策略等概念的惯用法学习成本低社区接受度高。实操心得std::function虽然方便但它有类型擦除的开销通常很小。对于性能极度敏感的场合可以考虑使用模板。例如将Duck类做成模板直接接受可调用对象类型。但这会使得Duck的类型随策略类型而变化失去了运行时动态替换策略的部分灵活性。需要根据实际场景权衡。3.2 观察者模式C中的生命周期难题观察者模式定义了对象间的一对多依赖关系当一个对象主题状态改变时所有依赖它的对象观察者都会得到通知并自动更新。在Java中因为有垃圾回收器主题持有观察者的引用通常不是大问题。但在C里这变成了一个需要精心设计的资源管理问题。3.2.1 “Pull” vs “Push” 模型《Head First》书中提到了两种通知方式Push主题将数据推送给观察者和Pull观察者主动从主题拉取数据。在GitHub的参考代码中作者提到“使用C无法按照Java那样去实现push的方式”最终实现了Pull。我的理解是他指的“麻烦”在于类型安全的数据传递。在Java的Push模型中update(Observable o, Object arg)方法的arg参数是Object类型可以传递任何数据。在C中要实现类似的通用性要么使用不安全的void*绝对不推荐要么使用像std::anyC17或boost::any这样的类型擦除容器但这会引入复杂性和运行时开销。因此在C中实现Pull模型更为常见和自然主题提供一个GetState()之类的常量方法观察者在收到通知后自己调用这个方法拉取所需数据。这样类型是安全的也符合C强调的显式控制。3.2.2 核心实现与智能指针管理观察者模式的核心结构是ISubject主题接口维护一个IObserver观察者接口的集合。这里最大的坑就是对象生命周期。如果主题持有一个观察者的原始指针而该观察者已经被销毁那么主题再进行通知就会导致悬垂指针引用程序崩溃。解决方案是使用std::weak_ptr。观察者自己拥有其生命周期的控制权通常由std::shared_ptr管理。主题只持有观察者的std::weak_ptr。在通知前主题尝试将weak_ptr提升lock()为shared_ptr。如果提升成功说明观察者还活着可以安全调用其更新方法如果失败说明观察者已死主题应该将该weak_ptr从列表中移除。#include memory #include vector #include algorithm #include iostream // 前向声明 class ISubject; class IObserver : public std::enable_shared_from_thisIObserver { public: virtual ~IObserver() default; virtual void Update(const ISubject subject) 0; // Pull 模型 }; class ISubject { public: virtual ~ISubject() default; virtual void Attach(std::weak_ptrIObserver observer) 0; virtual void Detach(std::weak_ptrIObserver observer) 0; virtual void Notify() 0; }; // 一个具体的主题例如气象站 class WeatherStation : public ISubject { public: void SetMeasurements(float temp, float humidity, float pressure) { temperature_ temp; humidity_ humidity; pressure_ pressure; Notify(); // 数据更新后通知所有观察者 } // Pull模型观察者通过这些getter拉取数据 float GetTemperature() const { return temperature_; } float GetHumidity() const { return humidity_; } float GetPressure() const { return pressure_; } void Attach(std::weak_ptrIObserver observer) override { observers_.push_back(observer); } void Detach(std::weak_ptrIObserver observer) override { // 由于是weak_ptr比较需要小心。通常我们比较其对应的shared_ptr是否指向同一对象。 // 简化处理在实际项目中可能需要为每个观察者分配唯一ID。 observers_.erase( std::remove_if(observers_.begin(), observers_.end(), [observer](const std::weak_ptrIObserver wp) { auto sp1 wp.lock(); auto sp2 observer.lock(); return sp1 sp2 sp1.get() sp2.get(); }), observers_.end()); } void Notify() override { // 重要在遍历过程中观察者可能被销毁Detach需要处理失效的weak_ptr auto it observers_.begin(); while (it ! observers_.end()) { if (auto sp it-lock()) { sp-Update(*this); // 调用观察者的更新方法传入主题引用供其Pull数据 it; } else { // weak_ptr已失效从列表中移除 it observers_.erase(it); } } } private: std::vectorstd::weak_ptrIObserver observers_; float temperature_ 0.0f; float humidity_ 0.0f; float pressure_ 0.0f; }; // 一个具体的观察者例如当前状况布告板 class CurrentConditionsDisplay : public IObserver, public std::enable_shared_from_thisCurrentConditionsDisplay { public: explicit CurrentConditionsDisplay(std::weak_ptrISubject weatherStation) : weatherStation_(std::move(weatherStation)) { if (auto ws weatherStation_.lock()) { ws-Attach(weak_from_this()); // 使用 weak_from_this() 安全地传递自身的weak_ptr } } ~CurrentConditionsDisplay() { if (auto ws weatherStation_.lock()) { ws-Detach(weak_from_this()); } } void Update(const ISubject subject) override { // 我们知道subject是WeatherStation但这里应该用动态转换确认略去RTTI检查 // 采用Pull方式获取数据 const auto* ws dynamic_castconst WeatherStation*(subject); if (ws) { temperature_ ws-GetTemperature(); humidity_ ws-GetHumidity(); Display(); } } void Display() const { std::cout Current conditions: temperature_ C degrees, humidity_ % humidity.\n; } private: std::weak_ptrISubject weatherStation_; float temperature_ 0.0f; float humidity_ 0.0f; }; // 使用示例 int main() { auto station std::make_sharedWeatherStation(); auto display1 std::make_sharedCurrentConditionsDisplay(station); auto display2 std::make_sharedCurrentConditionsDisplay(station); station-SetMeasurements(25.0f, 65.0f, 1013.0f); // 输出两次Current conditions: 25C degrees, 65% humidity. // display1 被销毁自动从主题的观察者列表中移除 display1.reset(); station-SetMeasurements(26.5f, 70.0f, 1012.0f); // 只输出一次来自display2Current conditions: 26.5C degrees, 70% humidity. return 0; }关键点解析std::weak_ptr与std::shared_ptr的配合这是解决生命周期问题的核心。主题持有weak_ptr不延长观察者的生命周期。观察者自身由shared_ptr管理。std::enable_shared_from_this为了让一个对象能安全地获取指向自身的weak_ptr用于在构造时Attach和析构时Detach它的类需要继承这个模板类并调用weak_from_this()方法。失效weak_ptr的清理在Notify或定期任务中必须清理那些已经失效lock()失败的weak_ptr否则列表会无限膨胀。线程安全上述代码不是线程安全的。在多线程环境下对observers_向量的修改和遍历需要加锁例如使用std::mutex这又会引入死锁的风险设计时需要格外小心。踩坑实录我曾尝试让主题持有shared_ptrIObserver这导致了循环引用观察者也可能持有主题的shared_ptr对象永远无法被释放内存泄漏。weak_ptr就是用来打破这种循环引用的标准工具。记住在可能存在循环引用的地方总有一方应该持有weak_ptr。3.3 装饰者模式动态组合优于静态继承装饰者模式允许向一个现有对象添加新的功能同时又不改变其结构。它提供了比继承更有弹性的扩展方案。书中的经典例子是咖啡店一杯咖啡Beverage可以被摩卡Mocha、奶泡Whip等“装饰者”层层包裹最终计算总价。3.3.1 继承方式的局限最直观的做法是用继承HouseBlend,HouseBlendWithMocha,HouseBlendWithMochaAndWhip... 类的数量会呈组合爆炸增长任何新的调料或饮料都会让类体系难以维护。3.3.2 装饰者模式的C实现要点装饰者模式的关键在于装饰者类CondimentDecorator和被装饰的组件类Beverage继承自同一个抽象基类。装饰者内部持有一个指向该基类的指针或引用从而可以将自己包裹在任何组件包括另一个装饰者外面。#include memory #include string #include iostream // 组件抽象基类 class IBeverage { public: virtual ~IBeverage() default; virtual std::string GetDescription() const 0; virtual double Cost() const 0; }; // 具体组件浓缩咖啡 class Espresso : public IBeverage { public: std::string GetDescription() const override { return Espresso; } double Cost() const override { return 1.99; } }; // 装饰者抽象基类 class CondimentDecorator : public IBeverage { protected: std::shared_ptrIBeverage beverage_; // 持有被装饰对象的智能指针 public: explicit CondimentDecorator(std::shared_ptrIBeverage beverage) : beverage_(std::move(beverage)) {} // GetDescription 和 Cost 仍然是纯虚函数由具体装饰者实现 }; // 具体装饰者摩卡 class Mocha : public CondimentDecorator { public: using CondimentDecorator::CondimentDecorator; // 继承构造函数 std::string GetDescription() const override { return beverage_-GetDescription() , Mocha; } double Cost() const override { return beverage_-Cost() 0.20; } }; // 具体装饰者奶泡 class Whip : public CondimentDecorator { public: using CondimentDecorator::CondimentDecorator; std::string GetDescription() const override { return beverage_-GetDescription() , Whip; } double Cost() const override { return beverage_-Cost() 0.10; } }; // 使用示例 int main() { // 点一杯浓缩咖啡 auto myCoffee std::make_sharedEspresso(); std::cout myCoffee-GetDescription() $ myCoffee-Cost() std::endl; // 用摩卡装饰它 myCoffee std::make_sharedMocha(myCoffee); // 注意用新的装饰者对象替换原指针 std::cout myCoffee-GetDescription() $ myCoffee-Cost() std::endl; // 再用奶泡装饰它装饰在已经加了摩卡的咖啡上 myCoffee std::make_sharedWhip(myCoffee); std::cout myCoffee-GetDescription() $ myCoffee-Cost() std::endl; // 输出 // Espresso $1.99 // Espresso, Mocha $2.19 // Espresso, Mocha, Whip $2.29 return 0; }实现细节与思考所有权与智能指针这里使用了std::shared_ptr来管理IBeverage对象。装饰者持有被装饰对象的shared_ptr这意味着它们共享所有权。当最外层的装饰者被销毁时如果它是最后一个持有者会连带销毁内部的所有对象。这种设计简单但有时可能不必要地共享所有权。另一种方案是使用std::unique_ptr并转移所有权但代码会稍复杂。动态组合myCoffee std::make_sharedMocha(myCoffee);这行代码是精髓。它将原本指向Espresso的智能指针重新赋值为指向一个新创建的Mocha对象而这个Mocha对象内部包裹着原来的Espresso。这个过程可以无限重复实现动态的、递归的装饰。与策略模式的区别装饰者模式改变了对象的行为通过添加职责但保持接口一致策略模式则是完全替换整个算法或行为。装饰者关注于动态添加功能而策略关注于灵活切换算法。注意事项装饰者模式会导致大量的小对象产生每个装饰者都是一个对象在性能要求极高的场景需要注意。另外由于装饰是透明的装饰者和组件接口一致客户端代码可能不知道它正在处理一个被装饰过的对象这有时是优点无感扩展有时也可能导致困惑比如调试时对象类型看起来和预期不同。4. 常见问题与排查技巧实录在将设计模式从Java移植到C以及在实际应用这些模式进行开发时我踩过不少坑。这里总结几个最常见的问题和解决思路希望能帮你绕开这些弯路。4.1 内存管理智能指针的误用与循环引用问题场景在观察者模式、组合模式等涉及对象间复杂关系的模式中滥用std::shared_ptr导致循环引用对象无法自动释放内存泄漏。排查与解决使用工具检测在Linux/macOS下可以用valgrind --leak-checkfull在Windows下可以使用Visual Studio自带的内存诊断工具或Dr. Memory来检测内存泄漏。分析对象关系图画一张简单的对象引用关系图。如果发现A持有B的shared_ptrB也持有A的shared_ptr或通过其他对象间接形成环就构成了循环引用。引入std::weak_ptr在循环引用中将其中一方的持有关系改为std::weak_ptr。weak_ptr不增加引用计数因此不会阻止对象被销毁。就像在观察者模式中主题持有观察者的weak_ptr。重新审视设计有时候循环引用意味着你的对象职责划分可能有问题。考虑是否可以通过引入第三方管理类如一个全局的或局部的管理器来持有这些shared_ptr而让对象之间只通过原始指针或引用进行非拥有的访问。4.2 对象切片与多态失效问题场景在工厂模式或需要拷贝对象时误用值传递导致“对象切片”Object Slicing丢失了派生类的信息多态行为失效。class Animal { public: virtual void Speak() const { std::cout ...\n; } }; class Dog : public Animal { public: void Speak() const override { std::cout Woof!\n; } }; void MakeSound(Animal a) { // 错误按值传递 a.Speak(); // 总是调用 Animal::Speak() } int main() { Dog d; MakeSound(d); // 发生对象切片d的Dog部分被“切掉”只拷贝了Animal基类部分 return 0; }排查与解决使用指针或引用传递在需要多态的场合永远使用指针Animal*或引用Animal来传递对象。使用智能指针更现代和安全的方式是使用std::unique_ptrAnimal或std::shared_ptrAnimal进行传递和存储。禁用拷贝如果类层次结构设计上就不应该被拷贝比如抽象基类可以将拷贝构造函数和拷贝赋值运算符声明为 delete。使用克隆模式如果确实需要多态地拷贝对象可以在基类中定义一个虚函数virtual std::unique_ptrAnimal Clone() const 0;然后在每个派生类中实现它。4.3 接口设计纯虚析构函数与default问题场景定义接口类抽象基类时忘记将析构函数声明为虚函数或者声明不当导致通过基类指针删除派生类对象时行为未定义通常只调用了基类的析构函数派生类部分没被销毁。正确做法class IMyInterface { public: virtual ~IMyInterface() default; // 正确虚析构函数且使用default让编译器生成 virtual void DoSomething() 0; // ... 其他纯虚函数 };必须声明为虚函数确保通过基类指针删除时能正确调用整个对象链的析构函数。使用 default即使是一个纯虚析构函数也需要提供实现因为派生类的析构函数会隐式调用它。 default让编译器生成一个默认实现是最简洁的方式。你也可以写一个空的实现体{}但 default更清晰。C11之后对于像IMyInterface这样的纯接口类只有纯虚函数也可以将析构函数声明为纯虚但必须提供定义virtual ~IMyInterface() 0;然后在类外定义IMyInterface::~IMyInterface() default;。使用 default的虚析构函数更简单且该类仍被认为是抽象类因为有其他纯虚函数。4.4 模式滥用与过度设计问题场景学习了设计模式后看什么都想用模式去套导致简单的业务逻辑被复杂的类层次结构包裹代码难以理解和维护。排查与解决遵循KISS原则Keep It Simple, Stupid首先尝试用最简单的代码解决问题。如果只是几行if-else或一个函数就能搞定的绝不用模式。识别真正的“变化点”设计模式的核心是“封装变化”。在引入一个模式前先问自己这个部分在未来真的会频繁变化吗如果变化可能性很低那么预先抽象可能带来不必要的复杂度。重构驱动不要一开始就设计完美的模式化架构。可以先写出能工作的、可能有点“丑”的代码。当变化真的来临时或者当你发现重复代码、难以扩展的迹象时再运用相应的模式进行重构。马丁·福勒的《重构》一书是这方面的绝佳指南。C特有简化记住C有模板、std::function、lambda等强大的工具它们有时可以替代传统的面向对象模式让代码更简洁。比如策略模式用std::function往往比定义一整套接口和类更轻量。5. 从笔记到实战如何将模式融入C项目学习设计模式的最终目的是为了写出更好的代码。看完这些案例笔记你可能想知道如何在实际项目中开始应用。这里分享一些我的个人经验。第一步识别代码中的“坏味道”在你现有的或新启动的C项目中留意以下信号它们可能提示你需要引入某个设计模式冗长的条件语句特别是switch-case或长的if-else if链用于根据类型或状态选择不同行为。这可能是策略模式或状态模式的应用场景。散弹式修改每增加一个新功能都需要修改多个分散的类。考虑是否可以用观察者模式事件通知或中介者模式集中通信来解耦。庞大的类一个类做了太多事情职责不清。尝试用装饰者模式动态添加功能或用桥接模式将抽象和实现分离。复杂的对象创建过程如果创建一个对象需要一系列步骤或者依赖很多参数建造者模式或抽象工厂模式可以帮你理清逻辑。直接依赖具体类高层模块直接new一个具体类导致耦合紧密。依赖注入常结合工厂模式和模板方法模式定义算法骨架可以改善这一点。第二步小范围重构而非重写不要试图一次性用模式重构整个项目。选择一个痛点最明显、边界相对清晰的模块入手。例如你有一个处理不同文件格式的模块里面全是if (format “json”) ... else if (format “xml”) ...。你可以尝试将其重构成策略模式定义一个IParser接口然后实现JsonParser,XmlParser再用一个简单的工厂或映射表来根据格式选择具体的解析器。测试通过后你会获得信心也能评估模式引入带来的收益和成本。第三步结合现代C特性如前所述不要被经典的面向对象实现方式束缚。在C中考虑用std::variant或std::any谨慎使用来实现类似访问者模式的功能。用std::function和lambda简化策略模式和命令模式。用RAII和智能指针自动管理模式中涉及的对象生命周期如组合模式中的子树、观察者模式中的观察者列表。用模板元编程可以在编译期完成一些模式的工作如策略选择带来零运行时开销但这属于进阶技术。第四步注重可测试性一个好的设计模式应用应该能提高代码的可测试性。例如依赖注入让你可以轻松地将一个真实的服务替换为Mock对象进行单元测试。模板方法模式将不变的部分封装在基类你可以单独测试可变的部分子类实现。在应用模式时时刻思考这样改过之后写单元测试是更容易了还是更难了最后记住一句老话“模式是解决问题的工具不是用来寻找问题的锤子。” 不要为了用模式而用模式。当你发现代码因为某种模式变得更清晰、更灵活、更易维护时你才真正掌握了它。这份C案例笔记就是希望能帮你跨越从“知道”到“会用”这道坎在C的世界里写出既有高性能、又具备良好设计美感的代码。