C++模板方法模式:用虚函数与继承实现算法骨架与步骤分离

C++模板方法模式:用虚函数与继承实现算法骨架与步骤分离
1. 项目概述从“固定流程”到“灵活定制”在软件开发的日常里我们经常会遇到一类问题一个算法或操作的骨架是确定的但其中某些步骤的具体实现却可能千变万化。比如你要写一个数据处理的流程它总是遵循“读取数据 - 清洗数据 - 分析数据 - 输出报告”这几个步骤。这个流程的骨架是固定的但“清洗数据”这一步对于文本数据、图像数据或者数值数据清洗的规则和算法完全不同。如果你为每一种数据都从头写一遍整个流程代码会充斥着大量的重复维护起来简直是噩梦。这就是“模板方法”模式要解决的核心痛点。它不是什么高深莫测的黑科技而是一种极其朴素却强大的设计思想将不变的部分算法骨架提升到父类中实现将可变的部分具体步骤延迟到子类中去实现。在C的世界里我们通常通过虚函数Virtual Function和继承Inheritance这两个核心机制来实现它。简单来说父类定义一个“模板方法”这个方法里按顺序调用了一系列的“步骤方法”。其中那些固定的步骤父类自己就实现了而那些需要变化的步骤父类只声明为虚函数具体实现则交给子类去“填空”。为什么在C里谈这个特别有感觉因为C没有像Java或C#那样语言层面内置的“抽象类”或“接口”关键字它依靠虚函数和纯虚函数来达成类似的多态效果。这使得模板方法模式在C中的实现更贴近语言本身的特性理解起来也更能触及面向对象设计的本质。接下来我会带你拆解这个模式的每一个细节并用你能立刻上手的代码示例让你彻底掌握如何在自己的C项目中运用它。2. 核心原理深度拆解不变的骨架与可变的细节2.1 设计意图与问题场景模板方法模式属于行为型设计模式其意图非常明确定义一个操作中的算法骨架而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。让我们看几个具体的场景你立刻就能共鸣构建系统无论是使用Make、CMake还是其他工具构建流程大体都是“配置(Configure) - 编译(Compile) - 链接(Link) - 安装(Install)”。但配置阶段在Windows上可能是生成.sln文件在Linux上则是生成Makefile。模板方法可以将“配置”这个步骤抽象出来由不同平台的子类具体实现。单元测试框架像Google Test这样的框架每个测试用例的执行流程是固定的“SetUp() - TestBody() - TearDown()”。SetUp和TearDown是准备和清理环境TestBody是具体的测试逻辑。框架提供了前两者的默认实现可能是空的而你必须重写TestBody。这就是一个经典的模板方法。游戏引擎的主循环一个游戏循环的基本步骤是“处理输入(ProcessInput) - 更新状态(Update) - 渲染画面(Render)”。引擎提供这个循环骨架而具体的“更新什么状态”、“渲染什么画面”则由你的游戏逻辑子类来实现。数据处理管道如前所述这是最直观的例子。一个报表生成器流程是“获取数据 - 计算指标 - 格式化 - 导出”。获取数据可能来自数据库、API或文件导出可能是PDF、Excel或网页。骨架不变细节可变。如果不使用模板方法代码会写成什么样子你可能会写一个庞大的函数里面用一堆if-else或switch-case来判断不同的类型然后执行不同的分支。这种代码的坏处显而易见违反开放-封闭原则对扩展开放对修改关闭。每增加一种新的类型或实现你都需要去修改这个核心的函数引入风险。而模板方法通过继承和多态将变化的部分隔离到了子类新增一种实现只需要增加一个新的子类核心骨架完全不用动。2.2 模式结构与C映射模板方法模式的结构非常简单通常只涉及两个角色抽象类Abstract Class在C中这就是一个包含至少一个纯虚函数的类。它负责定义算法的骨架即“模板方法”。这个方法通常被声明为public和非虚的final在C11以后也可以考虑以确保子类不能改变算法的执行顺序。在这个骨架方法内部它会调用一系列其他方法。这些方法可以分为两类具体方法Concrete Methods在抽象类中已经实现好的、通用的步骤。子类可以直接继承使用也可以选择重写如果它们不是final的。抽象方法Abstract Methods/ 钩子方法Hook Methods在抽象类中声明为纯虚函数virtual ReturnType Func() 0;的方法。这些就是留给子类去实现的“空位”。钩子方法是一种特殊的抽象方法它在父类中可能有一个默认的通常是空的实现子类可以选择性地重写它来影响模板方法的流程。具体子类Concrete Class继承自抽象类并实现重写父类中所有的纯虚函数抽象方法。一个具体子类就代表了算法的一种特定实现。在C中这种关系的实现非常直接// 抽象类 class AbstractClass { public: // 模板方法定义算法骨架通常是非虚的 void TemplateMethod() final { // C11 后可用final防止子类重写 this-BaseOperation1(); this-RequiredOperations1(); this-BaseOperation2(); this-Hook1(); // 钩子方法子类可选择性重写 this-RequiredOperation2(); this-BaseOperation3(); this-Hook2(); } // 具体方法已实现 void BaseOperation1() const { std::cout AbstractClass says: I am doing the bulk of the work (BaseOperation1)\n; } void BaseOperation2() const { std::cout AbstractClass says: But I let subclasses override some operations (BaseOperation2)\n; } void BaseOperation3() const { std::cout AbstractClass says: But I am doing the bulk of the work anyway (BaseOperation3)\n; } // 抽象方法子类必须实现 virtual void RequiredOperations1() const 0; virtual void RequiredOperation2() const 0; // 钩子方法子类可选择实现 virtual void Hook1() const {} virtual void Hook2() const {} virtual ~AbstractClass() default; // 虚析构函数确保正确释放资源 }; // 具体子类A class ConcreteClassA : public AbstractClass { protected: void RequiredOperations1() const override { std::cout ConcreteClassA says: Implemented Operation1\n; } void RequiredOperation2() const override { std::cout ConcreteClassA says: Implemented Operation2\n; } // 可以选择性重写钩子 void Hook1() const override { std::cout ConcreteClassA says: Overridden Hook1\n; } }; // 具体子类B class ConcreteClassB : public AbstractClass { protected: void RequiredOperations1() const override { std::cout ConcreteClassB says: Implemented Operation1 differently\n; } void RequiredOperation2() const override { std::cout ConcreteClassB says: Implemented Operation2 differently\n; } // 不重写Hook1和Hook2使用父类默认的空实现 };这段代码清晰地展示了结构。AbstractClass::TemplateMethod是算法的固定流程。ConcreteClassA和ConcreteClassB提供了RequiredOperations的不同实现从而让同一个模板方法产生了不同的行为效果。2.3 关键机制虚函数与继承模板方法模式在C中能工作的核心依赖于面向对象的两个基石继承和虚函数动态多态。继承建立了“是一类is-a”的关系。ConcreteClassA是一个AbstractClass。这保证了子类拥有父类定义的算法骨架模板方法。虚函数提供了“延迟绑定”或“运行时多态”的能力。当在TemplateMethod中调用this-RequiredOperations1()时this指针可能指向AbstractClass也可能指向ConcreteClassA或ConcreteClassB。编译器在编译时并不知道具体是哪个类直到程序运行时根据this实际指向的对象类型才决定调用哪个版本的RequiredOperations1。这就是“将步骤延迟到子类实现”的魔法所在。这里有一个非常重要的实操心得将模板方法声明为non-virtual或C11后的final。这是模板方法模式的一个关键设计点。模板方法定义了不可更改的算法流程这是模式的契约。如果子类可以重写模板方法那就破坏了整个模式的根基可能导致不可预期的行为。父类通过将模板方法设为非虚牢牢掌控了算法的执行顺序。注意在C中析构函数应该被声明为虚函数virtual ~AbstractClass()尤其是在有继承关系且可能通过基类指针删除派生类对象时。这是C的一个基本规则与模板方法模式本身无直接关系但却是实现该模式时必须遵守的“安全守则”否则会导致资源泄漏。3. 实战演练构建一个跨平台的数据导出工具光说不练假把式。我们现在来设计一个更贴近实际需求的例子一个数据导出工具。它的固定流程是“连接数据源 - 执行查询 - 转换数据格式 - 写入文件”。其中“连接数据源”和“写入文件”这两个步骤会因为目标例如导出到本地CSV文件或者导出到网络API的不同而有巨大差异。3.1 抽象类设计定义导出骨架首先我们定义抽象基类DataExporter。// data_exporter.h #ifndef DATA_EXPORTER_H #define DATA_EXPORTER_H #include string #include vector #include memory // 假设的数据结构 struct DataRecord { int id; std::string name; double value; }; class DataExporter { public: // 模板方法整个导出流程。声明为final禁止子类篡改流程。 void ExportData(const std::string query) final { std::cout [开始导出流程]\n; // 1. 建立连接 if (!Connect()) { std::cerr 连接失败导出中止。\n; return; } // 2. 执行查询获取数据 auto data ExecuteQuery(query); if (data.empty()) { std::cout 查询结果为空。\n; Disconnect(); // 记得断开连接 return; } // 3. 转换数据格式这是一个具体方法但子类可重写 std::string formattedData FormatData(data); // 4. 写入目标抽象方法 WriteToDestination(formattedData); // 5. 清理连接 Disconnect(); std::cout [导出流程结束]\n; } virtual ~DataExporter() default; protected: // 抽象方法子类必须实现 virtual bool Connect() 0; virtual void Disconnect() 0; virtual void WriteToDestination(const std::string data) 0; // 具体方法父类提供默认实现子类可按需重写 virtual std::vectorDataRecord ExecuteQuery(const std::string query) { std::cout 执行查询: query std::endl; // 这里模拟一些数据 return { {1, Alice, 100.5}, {2, Bob, 200.3}, {3, Charlie, 150.0} }; } virtual std::string FormatData(const std::vectorDataRecord records) { std::cout 格式化数据默认CSV格式...\n; std::string result id,name,value\n; for (const auto record : records) { result std::to_string(record.id) , record.name , std::to_string(record.value) \n; } return result; } // 钩子方法子类可选择性地重写以影响流程 virtual void OnBeforeWrite() { // 默认什么都不做 } virtual void OnAfterWrite(bool success) { if (success) { std::cout 数据写入成功。\n; } } }; #endif // DATA_EXPORTER_H在这个设计中ExportData是模板方法它用final修饰确保了“连接-查询-转换-写入-断开”这个核心流程不可被破坏。Connect,Disconnect,WriteToDestination是抽象方法纯虚函数。不同的导出目标本地文件、网络API、数据库必须提供自己的实现。ExecuteQuery和FormatData是具体方法。这里提供了简单的默认实现模拟查询和CSV格式化。子类如果数据来源或格式不同比如从JSON API获取数据或格式化为XML可以重写它们。OnBeforeWrite和OnAfterWrite是钩子方法。它们有默认的空实现。子类可以重写它们来在写入前后插入一些特定的逻辑比如日志记录、权限检查、发送通知等而不需要修改模板方法。3.2 具体子类实现CSV文件导出器现在我们实现一个导出到本地CSV文件的具体类。// csv_file_exporter.h / csv_file_exporter.cpp #include data_exporter.h #include fstream #include iostream class CsvFileExporter : public DataExporter { public: explicit CsvFileExporter(const std::string filepath) : filepath_(filepath) {} protected: bool Connect() override { // 对于文件导出“连接”就是检查文件路径是否可用 std::cout [CsvFileExporter] 检查文件路径: filepath_ std::endl; // 这里可以添加更复杂的检查如目录是否存在、是否有写入权限等 if (filepath_.empty()) { std::cerr 错误文件路径为空。\n; return false; } return true; } void Disconnect() override { // 对于文件导出“断开连接”通常不需要做任何事情 std::cout [CsvFileExporter] 文件写入完成清理完成。\n; } void WriteToDestination(const std::string data) override { // 钩子方法调用示例 OnBeforeWrite(); std::ofstream outFile(filepath_); if (!outFile.is_open()) { std::cerr 错误无法打开文件 filepath_ 进行写入。\n; OnAfterWrite(false); return; } outFile data; outFile.close(); bool success !outFile.fail(); OnAfterWrite(success); if (success) { std::cout [CsvFileExporter] 数据已成功写入文件: filepath_ std::endl; } } // 重写钩子方法添加特定逻辑 void OnBeforeWrite() override { std::cout [CsvFileExporter] 即将写入文件进行最终检查...\n; } private: std::string filepath_; };3.3 具体子类实现网络API导出器我们再实现一个导出到某个HTTP API的具体类。这里我们简化网络请求使用伪代码。// api_exporter.h / api_exporter.cpp #include data_exporter.h #include iostream // 假设有一个简单的HTTP客户端库 // #include http_client.h class ApiExporter : public DataExporter { public: ApiExporter(const std::string endpoint, const std::string apiKey) : endpoint_(endpoint), apiKey_(apiKey) {} protected: bool Connect() override { std::cout [ApiExporter] 尝试连接到API端点: endpoint_ std::endl; // 模拟网络连接检查比如ping一下服务器 // bool isReachable HttpClient::Ping(endpoint_); bool isReachable true; // 假设连接成功 if (!isReachable) { std::cerr 错误无法连接到API服务器。\n; return false; } std::cout [ApiExporter] API连接成功。\n; return true; } void Disconnect() override { std::cout [ApiExporter] 断开与API的连接。\n; // 可能需要进行会话清理等 } std::vectorDataRecord ExecuteQuery(const std::string query) override { // 重写查询方法从网络API获取数据而不是模拟数据 std::cout [ApiExporter] 通过API执行查询: query std::endl; // 伪代码发起网络请求获取JSON数据 // std::string jsonResponse HttpClient::Get(endpoint_ /data?query query, apiKey_); // auto records ParseJsonToRecords(jsonResponse); // 为了示例我们返回一些模拟的API数据 return { {101, ProductA, 299.99}, {102, ProductB, 159.50} }; } std::string FormatData(const std::vectorDataRecord records) override { // 重写格式化方法将数据格式化为API要求的JSON格式而不是CSV std::cout [ApiExporter] 将数据格式化为JSON...\n; std::string json {\records\: [; for (size_t i 0; i records.size(); i) { const auto r records[i]; json {\id\: std::to_string(r.id) ,\name\:\ r.name \ ,\value\: std::to_string(r.value) }; if (i ! records.size() - 1) json ,; } json ]}; return json; } void WriteToDestination(const std::string data) override { std::cout [ApiExporter] 向API发送数据...\n; // 伪代码发送HTTP POST请求 // bool success HttpClient::Post(endpoint_ /upload, data, application/json, apiKey_); bool success true; // 假设发送成功 OnAfterWrite(success); if (success) { std::cout [ApiExporter] 数据已成功推送至API。\n; } else { std::cerr [ApiExporter] 数据推送失败。\n; } } private: std::string endpoint_; std::string apiKey_; };3.4 客户端使用与效果现在客户端代码可以轻松地使用不同的导出器而完全不用关心内部复杂的流程。// main.cpp #include csv_file_exporter.h #include api_exporter.h #include memory int main() { std::string query SELECT * FROM sales WHERE date 2023-10-01; // 使用CSV文件导出器 std::cout \n 使用CSV文件导出器 \n; std::unique_ptrDataExporter csvExporter std::make_uniqueCsvFileExporter(./export/sales_data.csv); csvExporter-ExportData(query); // 使用API导出器 std::cout \n 使用API导出器 \n; std::unique_ptrDataExporter apiExporter std::make_uniqueApiExporter(https://api.example.com/v1, my-secret-key); apiExporter-ExportData(query); return 0; }运行这个程序你会看到两个导出器遵循完全相同的ExportData模板流程但每个步骤的内部实现却截然不同。这就是模板方法模式的威力统一了接口隔离了变化。如果你想增加一个新的导出目标比如直接写入数据库你只需要创建一个新的DataExporter子类实现那几个抽象方法即可原有的所有代码都无需改动。4. 高级技巧与避坑指南掌握了基本实现后我们来看看在实际项目中应用模板方法模式时有哪些需要特别注意的地方和可以提升的技巧。4.1 钩子方法的妙用钩子方法是模板方法模式中一个非常灵活的特性。它是在抽象类中声明并提供了默认实现通常是空实现的虚函数。子类可以选择性地重写它从而在不改变算法骨架的前提下“钩入”自己的逻辑影响模板方法的执行。在上面的DataExporter例子中OnBeforeWrite和OnAfterWrite就是钩子。CsvFileExporter重写了OnBeforeWrite来做写入前的最后检查而ApiExporter可能没有这个需求所以它就不重写使用父类的空实现。什么时候使用钩子条件执行你可以在模板方法中判断钩子的返回值或状态来决定是否执行某个步骤。例如可以有一个bool ShouldCompressData()的钩子如果子类返回true模板方法就在写入前加入一个压缩步骤。额外操作在算法关键步骤前后插入日志、性能统计、权限验证等“横切关注点”逻辑。提供扩展点为未来可能的需求预留位置避免以后为了加一个小功能而不得不修改抽象类。实操心得钩子命名要清晰。钩子方法的命名最好能明确表达其调用时机或目的比如BeforeStepX,AfterStepY,CanProceed,NeedsSpecialHandling等。避免使用过于泛泛的Hook1,Hook2。4.2 控制子类的行为protected访问控制与final在C中良好的访问控制是设计健壮基类的关键。模板方法TemplateMethod应该声明为public。这是给客户端调用的主要接口。基本方法PrimitiveOperation即抽象方法和具体方法应该声明为protected。这些方法是算法骨架的组成部分是给子类重写或使用的不应该暴露给外部客户端。如果声明为public客户端可能会直接调用它们破坏了算法的封装性。使用final对模板方法使用finalC11及以上防止子类意外重写并改变核心流程。对不希望子类重写的具体方法使用final。比如DataExporter中的ExportData流程是固定的或者某个具体方法如一个复杂的内部计算CalculateHash其逻辑必须保持一致就可以将其声明为final。class RobustAbstractClass { public: void TemplateMethod() final { // 禁止子类重写骨架 // ... PrimitiveOperation1(); // 调用protected方法 // ... } virtual ~RobustAbstractClass() default; protected: virtual void PrimitiveOperation1() 0; // 子类必须实现 void ConcreteOperation() final { // 子类不能重写这个具体方法 // 固定不变的逻辑 } private: void InternalHelper() { // 完全私有的辅助函数子类不可见 // ... } };4.3 模板方法模式 vs. 策略模式这是初学者容易混淆的两个模式。它们都用于封装算法但意图不同模板方法模式基于继承。定义一个算法的骨架部分步骤由子类实现。它强调的是算法结构的不变性变化的是算法中某些步骤的具体实现。父类控制着算法的流程。策略模式基于组合。定义一系列算法策略将它们分别封装起来并且使它们可以相互替换。策略模式让算法的变化独立于使用它的客户端。它强调的是算法的可互换性整个算法实现都可以被替换。如何选择如果算法的整体结构是稳定的但其中某些步骤的具体实现可能变化并且你希望子类能够复用这个固定结构用模板方法。如果你需要动态地在多种完整的、可互换的算法之间进行选择并且算法的客户端不希望关心算法的具体细节用策略。有时它们可以结合使用。比如在策略接口的某个具体策略实现内部其自身可能就是用模板方法模式来构建的。4.4 C中的性能考量与非虚接口NVI惯用法虚函数调用会带来一定的运行时开销通过虚函数表vtable进行间接调用。在极端注重性能的场景下需要谨慎评估。不过对于大多数应用这点开销是可接受的它换来了巨大的设计灵活性。在C社区有一个与模板方法模式高度相关的惯用法叫做“非虚接口Non-Virtual Interface, NVI”。NVI主张将公有成员函数设为非虚函数让它成为“接口”而在其中调用私有的虚函数来实现多态。这其实就是模板方法模式的一种严格应用。class NVIExample { public: // 公有非虚接口 void PublicInterface() { // 这里可以添加一些所有派生类共有的前置/后置逻辑 // 例如锁定互斥锁、日志记录、参数验证等 std::cout NVI: Pre-processing...\n; // 调用私有的虚函数实现 DoExecute(); // 后置逻辑 std::cout NVI: Post-processing...\n; } virtual ~NVIExample() default; private: // 私有虚函数真正的实现留给派生类 virtual void DoExecute() 0; }; class ConcreteNVI : public NVIExample { private: void DoExecute() override { std::cout ConcreteNVI: Doing the real work.\n; } };NVI惯用法的好处在于基类通过非虚的公有接口牢牢掌握了调用流程的控制权。它可以在调用真正的工作函数私有虚函数前后插入所有派生类都需要的通用逻辑如线程同步、日志、资源管理避免了代码重复也保证了这些关键逻辑一定会被执行。这比简单的模板方法模式控制力更强是C中实现“模板方法”思想的一种更优雅、更安全的方式。5. 常见问题与排查技巧实录在实际使用模板方法模式时你可能会遇到一些典型问题。下面是我踩过的一些坑和对应的解决方案。5.1 问题一子类忘记实现纯虚函数这是最经典的编译错误。error: invalid new-expression of abstract class type ‘ConcreteExporter’ note: because the following virtual functions are pure within ‘ConcreteExporter’: note: virtual bool DataExporter::Connect()排查与解决仔细阅读编译器错误信息。它会明确列出哪些纯虚函数没有被实现。检查你的具体子类确保对基类中所有0的纯虚函数都进行了override。使用C11的override关键字是个好习惯。它能让编译器帮你检查函数签名是否完全匹配避免因拼写错误或参数类型不匹配导致的“看似重写实则重载”的bug。class MyExporter : public DataExporter { protected: bool Connect() override; // 正确 // bool Connect(int port) override; // 错误签名不匹配编译报错 };5.2 问题二模板方法流程中的异常安全在ExportData这样的模板方法中如果Connect()成功了但WriteToDestination()抛出了异常Disconnect()可能不会被调用导致资源泄漏比如网络连接未关闭、文件句柄未释放。void ExportData(...) final { Connect(); // 如果这里或后续步骤抛出异常... ExecuteQuery(...); // 可能抛异常 WriteToDestination(...); // 可能抛异常 Disconnect(); // 这行可能执行不到 }解决方案使用RAII资源获取即初始化。将资源连接、文件锁等的管理封装在独立的RAII类中。在模板方法内使用栈上的RAII对象。或者在模板方法中使用try-catch块确保Disconnect被调用但这会让代码变丑。更好的设计是修改抽象方法让Connect返回一个代表资源所有权的智能指针或RAII对象而Disconnect由该对象的析构函数自动完成。这样无论流程如何结束正常或异常资源都会被正确释放。class ConnectionRAII { public: ~ConnectionRAII() { /* 自动断开连接 */ } // ... 其他接口 }; virtual std::unique_ptrConnectionRAII EstablishConnection() 0; void ExportData(...) final { auto conn EstablishConnection(); // RAII对象离开作用域自动析构 // ... 其他步骤即使抛出异常conn也会被销毁并断开连接 }5.3 问题三基类构造函数/析构函数中调用虚函数这是一个C的经典陷阱。class Base { public: Base() { Initialize(); } // 在构造函数中调用虚函数 virtual ~Base() { Cleanup(); } // 在析构函数中调用虚函数 virtual void Initialize() { std::cout Base::Initialize\n; } virtual void Cleanup() { std::cout Base::Cleanup\n; } }; class Derived : public Base { public: void Initialize() override { std::cout Derived::Initialize\n; } void Cleanup() override { std::cout Derived::Cleanup\n; } }; int main() { Derived d; // 输出什么 return 0; } // 实际输出 // Base::Initialize // Derived::Cleanup (可能但行为在析构时已部分销毁不安全)在构造Derived对象时会先调用Base的构造函数。此时Derived的部分尚未构造完成因此Base::Initialize调用的是Base版本的Initialize而不是Derived的版本。析构时顺序相反但同样可能因为对象已被部分销毁而导致未定义行为。黄金法则绝对不要在构造函数和析构函数中调用虚函数来实现多态。如果基类需要在构造/析构时进行初始化/清理应该让子类通过构造函数参数传递必要信息或者使用非虚函数并在文档中明确要求子类在其构造函数中调用它。对于模板方法模式这意味着你的模板方法不应该在基类的构造函数中被调用。模板方法是一个完整的操作流程它依赖于子类所有部分的完全构造。5.4 问题四过度设计与模式滥用不是所有流程都适合用模板方法模式。如果算法的步骤经常需要大幅调整顺序或者每个步骤的实现都高度独立且组合方式多变那么强行套用模板方法会导致一个僵化的类层次结构增加不必要的复杂度。判断标准算法的骨架是否真正稳定如果未来很可能需要增加、删除或重排步骤那么模板方法可能不是最佳选择。考虑策略模式或简单的函数组合。子类之间的共性是否足够多如果每个子类都重写了大部分具体方法那么继承带来的代码复用好处就很小了反而引入了紧耦合。这时组合策略模式可能更合适。记住设计模式是工具不是教条。最简单的、能工作的设计往往就是最好的设计。不要为了用模式而用模式。模板方法模式是C工具箱里一把朴实但极其有用的扳手。它完美体现了“好莱坞原则”“别调用我们我们会调用你”让父类掌控大局子类专注细节。通过清晰地分离不变与可变的部分它极大地提升了代码的复用性和可维护性。下次当你发现自己在复制粘贴代码只为了修改其中的一小部分时停下来想一想这里是不是藏着一个“模板方法”