C++策略模式实战:重构数据校验器,告别硬编码if-else
1. 项目概述从“硬编码”到“策略模式”的思维跃迁最近在重构一个老旧的C数据处理模块时我又一次深刻体会到了设计模式的价值。这个模块的核心功能是根据不同的数据源比如文件、网络流、数据库和不同的业务规则比如校验、清洗、转换来处理一批数据。最初的版本写得很“直男”——一个巨大的switch-case或者一堆if-else嵌套在核心处理函数里。每增加一种数据源或者一种处理规则就得去修改这个核心函数小心翼翼地添加新的分支。时间一长这个函数膨胀到了几百行逻辑耦合得像一团乱麻测试起来也异常痛苦生怕改A功能把B功能搞崩了。这其实就是典型的“硬编码”逻辑膨胀问题。而策略模式Strategy Pattern正是解决这类问题的“银弹”之一。它不是什么高深莫测的黑科技其核心思想非常朴素将一组可互换的算法或行为封装成独立的类并使它们可以相互替换。这样使用算法的对象我们称之为上下文Context就不需要关心具体用的是哪个算法它只和算法的抽象接口打交道。算法的增减、修改都变成了独立模块的变更与核心逻辑彻底解耦。今天我就用一个从真实项目中简化出来的例子带你一步步用C的策略模式重构一个数据处理程序。我们会从一个典型的、充满if-else的“面条代码”开始分析其痛点然后引入策略模式进行改造最后对比重构前后的代码看看策略模式到底带来了哪些实实在在的好处可维护性、可扩展性、可测试性的提升以及如何优雅地应对未来的变化。无论你是正在学习设计模式的C新手还是想优化手中“祖传代码”的老手相信这个从“原始”到“优化”的完整过程都能给你带来启发。2. 原始版本剖析一个典型的“数据校验器”为了聚焦于模式本身我们设计一个足够典型但又不过于复杂的场景一个数据校验器。它需要处理来自不同渠道的字符串数据每种渠道比如“用户输入”、“配置文件”、“API响应”都有自己独特的校验规则。原始版本通常会怎么写我们直接看代码。2.1 “面条式”代码实现// 原始版本DataValidator_Naive.cpp #include iostream #include string #include cctype class DataValidator { public: enum class DataSource { USER_INPUT, CONFIG_FILE, API_RESPONSE }; // 核心校验函数充满了if-else bool validate(const std::string data, DataSource source) { bool isValid true; std::string errorMsg; if (source DataSource::USER_INPUT) { // 规则1: 用户输入不能为空 if (data.empty()) { errorMsg 用户输入不能为空; isValid false; } // 规则2: 用户输入不能包含脚本标签 else if (data.find(script) ! std::string::npos || data.find(/script) ! std::string::npos) { errorMsg 用户输入包含非法脚本标签; isValid false; } // 规则3: 长度限制在1-100字符 else if (data.length() 1 || data.length() 100) { errorMsg 用户输入长度必须在1到100字符之间; isValid false; } } else if (source DataSource::CONFIG_FILE) { // 规则1: 配置文件内容不能为空 if (data.empty()) { errorMsg 配置文件内容不能为空; isValid false; } // 规则2: 必须包含等号简单的键值对格式检查 else if (data.find() std::string::npos) { errorMsg 配置文件行格式错误缺少等号; isValid false; } // 规则3: 键名等号前部分不能以数字开头 else { size_t eqPos data.find(); if (eqPos 0 std::isdigit(data[0])) { errorMsg 配置键名不能以数字开头; isValid false; } } } else if (source DataSource::API_RESPONSE) { // 规则1: API响应必须是合法的JSON开头简单检查 if (data.empty() || (data[0] ! { data[0] ! [)) { errorMsg API响应格式错误非JSON格式; isValid false; } // 规则2: 响应长度不能超过5000字符防止过大响应 else if (data.length() 5000) { errorMsg API响应数据过长; isValid false; } } else { errorMsg 未知的数据源类型; isValid false; } if (!isValid) { std::cerr 校验失败: errorMsg std::endl; } else { std::cout 校验通过. std::endl; } return isValid; } }; int main() { DataValidator validator; // 测试用户输入 std::cout 测试1 - 用户输入正常: ; validator.validate(Hello World, DataValidator::DataSource::USER_INPUT); std::cout 测试2 - 用户输入含脚本: ; validator.validate(Helloscriptalert(1)/script, DataValidator::DataSource::USER_INPUT); // 测试配置文件 std::cout 测试3 - 配置文件正常: ; validator.validate(timeout30, DataValidator::DataSource::CONFIG_FILE); std::cout 测试4 - 配置文件无等号: ; validator.validate(timeout 30, DataValidator::DataSource::CONFIG_FILE); // 测试API响应 std::cout 测试5 - API响应正常JSON: ; validator.validate({\status\: \ok\}, DataValidator::DataSource::API_RESPONSE); std::cout 测试6 - API响应非JSON: ; validator.validate(Not a JSON, DataValidator::DataSource::API_RESPONSE); return 0; }2.2 原始代码的四大痛点运行上面的代码功能是正常的但它暴露了在软件工程中几个非常典型的问题违反开闭原则Open-Closed Principle这是最核心的问题。开闭原则要求软件实体类、模块、函数应该对扩展开放对修改关闭。如果想增加一种新的数据源比如“数据库记录”我必须修改DataValidator::validate这个核心函数的内部实现添加一个新的else if分支。每次修改都意味着引入新bug的风险并且需要重新测试整个函数。高耦合与低内聚校验逻辑what和校验的执行流程how紧紧耦合在一起。validate函数既负责调度根据source选择分支又负责实现所有具体的校验规则。这使得函数职责不清内聚性很低。一个函数里混杂了三种完全不同业务的逻辑。可测试性差由于所有逻辑都在一个函数里想要单元测试“用户输入校验”这个独立功能你无法将其隔离出来。你必须构建整个DataValidator对象并调用validate函数这可能会受到其他分支逻辑的间接影响。测试用例也会变得冗长复杂。代码膨胀与维护困难随着数据源类型和校验规则的增加这个validate函数会像滚雪球一样越来越大。想象一下如果有10种数据源每种有5条规则这个函数将会有50个if-else片段。阅读、调试、修改这样的代码将是一场噩梦。任何一点改动都可能产生“牵一发而动全身”的副作用。注意在实际项目中这种代码的变体可能更隐蔽比如使用工厂方法返回不同的校验函数指针但核心的“策略”逻辑依然分散在多个地方没有统一的抽象和管理方式。3. 策略模式重构核心思想与架构设计策略模式提供了一种优雅的解决方案。它的UML类图非常简单但其思想却非常强大。我们先从概念上理解它再动手改造。3.1 策略模式的三要素策略接口Strategy Interface定义所有具体策略类必须实现的方法。在我们的例子中就是一个“校验”操作。它抽象了“做什么”验证数据而不关心“怎么做”具体规则。具体策略类Concrete Strategy实现策略接口的一系列类。每个类封装了一种独立的、完整的算法或行为。对应我们的例子就是UserInputValidationStrategy、ConfigFileValidationStrategy等。每个类只关心自己那套校验规则。上下文类Context持有一个策略接口的引用或指针。上下文不负责决定使用哪个具体策略它只负责调用策略接口定义的方法。它通过这个接口与具体策略交互从而实现了与具体策略类的解耦。在我们的例子中DataValidator类将扮演这个角色。3.2 重构后的架构设计基于以上思想我们对原有代码进行解耦设计抽象将“校验”这个行为抽象成一个纯虚基类ValidationStrategy它只声明一个validate方法。分离将原来糅杂在if-else里的三种校验逻辑分别剥离到三个独立的类中UserInputStrategyConfigFileStrategyApiResponseStrategy。它们都继承自ValidationStrategy。委托原来的DataValidator类不再自己实现校验逻辑而是持有一个ValidationStrategy的指针。它的validate方法现在只是简单地调用这个指针所指对象的validate方法。校验逻辑被“委托”给了具体的策略对象。装配由客户端代码例如main函数来决定创建哪种具体的策略对象并将其设置给DataValidator。这样策略的选择权就从上下文类转移到了客户端上下文类变得更加纯粹和稳定。这个设计的精髓在于增加新的校验策略不再需要修改DataValidator或任何已有的策略类。你只需要新建一个类实现ValidationStrategy接口然后在客户端代码中使用它即可。这完美符合了开闭原则。4. 策略模式优化版实现详解理论说完了我们直接上代码看看重构后的世界有多清晰。4.1 定义策略抽象接口首先我们定义所有校验策略都必须遵守的“契约”。// ValidationStrategy.h - 策略抽象接口 #ifndef VALIDATION_STRATEGY_H #define VALIDATION_STRATEGY_H #include string #include memory // 为std::unique_ptr做准备 class ValidationStrategy { public: virtual ~ValidationStrategy() default; // 虚析构函数确保正确释放派生类资源 // 核心校验接口 virtual bool validate(const std::string data) const 0; // 可选获取校验策略的名称便于日志和调试 virtual std::string getStrategyName() const 0; }; #endif // VALIDATION_STRATEGY_H关键点1虚析构函数。在C中如果打算通过基类指针来删除派生类对象比如std::unique_ptrValidationStrategy基类必须有一个虚析构函数。否则会导致派生类的析构函数不被调用引发资源泄漏。这是一个非常容易踩坑的细节。4.2 实现具体策略类接下来我们将原来if-else里的每一块逻辑独立成一个类。// UserInputValidationStrategy.h #ifndef USER_INPUT_VALIDATION_STRATEGY_H #define USER_INPUT_VALIDATION_STRATEGY_H #include ValidationStrategy.h #include string class UserInputValidationStrategy : public ValidationStrategy { public: bool validate(const std::string data) const override { if (data.empty()) { lastError_ 用户输入不能为空; return false; } if (data.find(script) ! std::string::npos || data.find(/script) ! std::string::npos) { lastError_ 用户输入包含非法脚本标签; return false; } if (data.length() 1 || data.length() 100) { lastError_ 用户输入长度必须在1到100字符之间; return false; } return true; } std::string getStrategyName() const override { return UserInputValidationStrategy; } // 提供一个方法获取最后一次的错误信息可选增强可用性 std::string getLastError() const { return lastError_; } private: // 使用mutable允许在const成员函数中修改用于缓存错误信息。 // 这是一种设计权衡如果追求纯const可以改为返回std::pairbool, std::string。 mutable std::string lastError_; }; #endif // USER_INPUT_VALIDATION_STRATEGY_H// ConfigFileValidationStrategy.h #ifndef CONFIG_FILE_VALIDATION_STRATEGY_H #define CONFIG_FILE_VALIDATION_STRATEGY_H #include ValidationStrategy.h #include string #include cctype class ConfigFileValidationStrategy : public ValidationStrategy { public: bool validate(const std::string data) const override { if (data.empty()) { lastError_ 配置文件内容不能为空; return false; } size_t eqPos data.find(); if (eqPos std::string::npos) { lastError_ 配置文件行格式错误缺少等号; return false; } // 检查键名等号前的部分是否以数字开头 if (eqPos 0 std::isdigit(static_castunsigned char(data[0]))) { lastError_ 配置键名不能以数字开头; return false; } // 可以添加更多规则例如检查键名是否包含空格等 return true; } std::string getStrategyName() const override { return ConfigFileValidationStrategy; } std::string getLastError() const { return lastError_; } private: mutable std::string lastError_; }; #endif // CONFIG_FILE_VALIDATION_STRATEGY_H// ApiResponseValidationStrategy.h #ifndef API_RESPONSE_VALIDATION_STRATEGY_H #define API_RESPONSE_VALIDATION_STRATEGY_H #include ValidationStrategy.h #include string class ApiResponseValidationStrategy : public ValidationStrategy { public: bool validate(const std::string data) const override { if (data.empty()) { lastError_ API响应内容不能为空; return false; } // 非常简单的JSON格式开头检查 if (data[0] ! { data[0] ! [) { lastError_ API响应格式错误应为JSON对象或数组; return false; } // 长度限制 if (data.length() 5000) { lastError_ API响应数据过长超过5000字符限制; return false; } // 实际项目中这里可能会集成一个轻量级的JSON解析器进行更彻底的语法检查 return true; } std::string getStrategyName() const override { return ApiResponseValidationStrategy; } std::string getLastError() const { return lastError_; } private: mutable std::string lastError_; }; #endif // API_RESPONSE_VALIDATION_STRATEGY_H关键点2单一职责。每个策略类现在只负责一件事实现自己领域内的校验规则。代码逻辑清晰内聚性极高。修改用户输入的规则绝对不会影响到配置文件或API响应的校验逻辑。4.3 重构上下文类DataValidator现在DataValidator类的职责发生了根本性变化。它从“什么都做的大家长”变成了“协调工作的管理者”。// DataValidator.h - 重构后的上下文类 #ifndef DATA_VALIDATOR_H #define DATA_VALIDATOR_H #include ValidationStrategy.h #include memory #include iostream class DataValidator { public: // 构造函数接受一个策略对象的unique_ptr取得其所有权 explicit DataValidator(std::unique_ptrValidationStrategy strategy) : strategy_(std::move(strategy)) { if (!strategy_) { std::cerr 警告传入的策略指针为空校验将始终失败。 std::endl; } } // 允许运行时动态更换策略 void setStrategy(std::unique_ptrValidationStrategy strategy) { strategy_ std::move(strategy); } // 核心的校验方法现在只是委托给策略对象 bool validate(const std::string data) const { if (!strategy_) { std::cerr 错误未设置校验策略。 std::endl; return false; } std::cout 正在使用策略 [ strategy_-getStrategyName() ] 校验数据... std::endl; return strategy_-validate(data); } // 获取当前策略名称 std::string getCurrentStrategyName() const { return strategy_ ? strategy_-getStrategyName() : No Strategy Set; } private: std::unique_ptrValidationStrategy strategy_; }; #endif // DATA_VALIDATOR_H关键点3对象所有权与std::unique_ptr。这里使用了std::unique_ptr来管理策略对象的内存生命周期。DataValidator接管了策略对象的所有权这意味着当DataValidator对象销毁时它所持有的策略对象也会被自动销毁无需手动delete避免了内存泄漏。这是现代CC11及以上推荐的做法。std::move用于转移所有权符合unique_ptr不可复制的语义。4.4 客户端代码灵活装配与使用最后我们看看客户端main函数是如何使用这套新体系的。代码的灵活性和清晰度有了质的飞跃。// main.cpp #include iostream #include memory #include “DataValidator.h” #include “UserInputValidationStrategy.h” #include “ConfigFileValidationStrategy.h” #include “ApiResponseValidationStrategy.h” // 一个辅助函数用于演示校验过程 void testValidation(DataValidator validator, const std::string testName, const std::string data) { std::cout “\n 测试: “ testName ” ” std::endl; std::cout “数据: \”” data “\”” std::endl; std::cout “当前策略: “ validator.getCurrentStrategyName() std::endl; bool result validator.validate(data); std::cout “结果: “ (result ? “通过” : “失败”) std::endl; } int main() { std::cout “ C 策略模式示例 - 数据校验器 \n” std::endl; // 示例1针对用户输入进行校验 { auto userInputStrategy std::make_uniqueUserInputValidationStrategy(); DataValidator validator(std::move(userInputStrategy)); // 构造时注入策略 testValidation(validator, “用户输入 - 正常短文本”, “Hello World”); testValidation(validator, “用户输入 - 空字符串”, “”); testValidation(validator, “用户输入 - 包含脚本”, “scriptalert(‘xss’)/script”); testValidation(validator, “用户输入 - 过长文本”, std::string(150, ‘a’)); } // validator 和其策略对象在此作用域结束自动销毁 std::cout “\n——————————\n” std::endl; // 示例2动态切换策略 - 同一个validator对象处理不同类型数据 { // 初始化为用户输入策略 auto initialStrategy std::make_uniqueUserInputValidationStrategy(); DataValidator validator(std::move(initialStrategy)); testValidation(validator, “动态切换前 - 用户输入测试”, “Test123”); // 动态切换到配置文件策略 validator.setStrategy(std::make_uniqueConfigFileValidationStrategy()); testValidation(validator, “动态切换后 - 配置文件正常行”, “server_port8080”); testValidation(validator, “动态切换后 - 配置文件错误行”, “port 8080”); // 再次动态切换到API响应策略 validator.setStrategy(std::make_uniqueApiResponseValidationStrategy()); testValidation(validator, “再次切换 - API响应(JSON对象)”, “{\”code\”: 0}”); testValidation(validator, “再次切换 - API响应(普通文本)”, “Success”); } std::cout “\n——————————\n” std::endl; // 示例3在容器中管理多种策略 (模拟策略工厂或配置化) { std::cout “模拟从配置中加载策略列表进行批量校验:\n”; // 假设我们有一个需要校验的数据和策略类型的映射列表 std::vectorstd::pairstd::string, std::unique_ptrValidationStrategy testCases; testCases.emplace_back(“Alice”, std::make_uniqueUserInputValidationStrategy()); testCases.emplace_back(“timeout30”, std::make_uniqueConfigFileValidationStrategy()); testCases.emplace_back(“[1,2,3]”, std::make_uniqueApiResponseValidationStrategy()); testCases.emplace_back(“”, std::make_uniqueUserInputValidationStrategy()); // 空数据 for (auto [data, strategy] : testCases) { DataValidator tempValidator(std::move(strategy)); // 为每个用例创建临时validator bool ok tempValidator.validate(data); std::cout “ 数据 ‘“ data “‘ - “ (ok ? “OK” : “FAIL”) std::endl; } } return 0; }5. 策略模式带来的核心优势分析对比重构前后的代码策略模式的优势是全方位、立竿见影的。5.1 可维护性大幅提升代码结构清晰每个策略类都是一个独立的单元功能明确。UserInputValidationStrategy的代码只会因为用户输入规则的改变而改变。阅读和维护者可以快速定位到相关逻辑无需在数百行的巨型函数里大海捞针。逻辑隔离BUG被限制在单个策略类中。如果配置文件校验出了问题你几乎可以百分百确定要去ConfigFileValidationStrategy里找原因而不会误伤用户输入或API响应的逻辑。调试效率成倍提高。5.2 可扩展性极强完美符合开闭原则这是策略模式最闪耀的优点。假设产品经理提出新需求“我们需要支持对来自消息队列如Kafka的数据进行校验规则是消息不能为空且必须包含消息ID字段”。在原始版本中你需要在enum DataSource里添加KAFKA_MESSAGE。打开庞大的validate函数在if-else链的末尾小心翼翼地添加一个新的else if分支实现Kafka的校验逻辑。重新编译、测试整个DataValidator类风险很高。在策略模式版本中你需要新建一个头文件KafkaMessageValidationStrategy.h。新建一个类KafkaMessageValidationStrategy继承ValidationStrategy实现validate方法在里面写不能为空和检查消息ID的逻辑。在客户端代码如main函数或某个工厂类中像使用其他策略一样使用它validator.setStrategy(std::make_uniqueKafkaMessageValidationStrategy())。看到了吗我们扩展了系统的功能新增了一种校验行为但没有修改任何现有的、经过测试的类DataValidator、UserInputValidationStrategy等。这极大地降低了引入回归错误的风险也使得并行开发成为可能——张三可以开发Kafka策略李四可以优化API策略互不干扰。5.3 可测试性变得简单单元测试变得非常纯粹。要测试用户输入校验你只需要实例化一个UserInputValidationStrategy对象调用它的validate方法传入各种边界值空字符串、带脚本的字符串、99个字符、101个字符等进行断言即可。你不再需要构建复杂的DataValidator上下文。可以轻松使用Mock对象。在测试DataValidator本身的委托逻辑时即它是否正确调用了策略接口你可以创建一个实现了ValidationStrategy的Mock类注入进去验证validate方法是否被以正确的参数调用。这得益于依赖注入通过构造函数或setStrategy带来的松耦合。5.4 运行时灵活性DataValidator::setStrategy方法赋予了我们在程序运行时动态切换算法策略的能力。这在一些场景下非常有用配置化行为校验策略可以从配置文件中读取并动态加载。例如在测试环境使用宽松的校验策略在生产环境使用严格的策略。根据上下文选择策略程序可以根据数据的某些特征如来源标识、数据头信息实时决定使用哪种校验策略而无需写死一堆if-else。A/B测试算法可以同时部署两种不同的校验策略比如一种快但不够精确一种慢但非常严格让一部分流量走A策略一部分走B策略根据结果选择最优算法。5.5 代码复用与组合策略类本身就是高度可复用的组件。UserInputValidationStrategy不仅可以被这个数据校验器使用如果系统其他地方也需要对“用户输入”进行同样的校验比如在另一个数据导入模块你可以直接包含这个头文件并使用这个类无需复制粘贴代码。更进一步你甚至可以运用**组合模式Composite Pattern**的思想创建一个CompositeValidationStrategy它内部持有一个策略列表validate时依次执行所有子策略类似责任链。这样就能实现“先进行基础格式校验再进行业务规则校验”的复杂组合校验逻辑而DataValidator对此一无所知它依然只调用一个validate接口。这种“模式组合”的能力是简单if-else完全无法比拟的。6. 策略模式的适用场景与实战心得策略模式并非银弹它有自己最擅长的战场。6.1 典型使用场景多种算法变体当你有多种功能相似但实现不同的算法并且需要在运行时选择其中一种时。除了我们的校验例子还有排序算法根据数据量大小选择快速排序、归并排序或冒泡排序。压缩算法根据对速度/压缩比的要求选择ZIP、GZIP或BZIP2策略。支付方式根据用户选择使用支付宝、微信支付、信用卡等不同的支付策略。导航计算根据实时路况选择时间最短、距离最短、避开收费等不同路径计算策略。消除条件判断当一个类中存在大量的条件语句if-else或switch-case且这些条件分支仅仅是选择不同的行为时使用策略模式可以将每个分支提取为独立的策略类从而消除冗长的条件判断。需要隔离算法细节当算法的实现细节比较复杂或者需要保密不希望暴露给客户端时可以用策略模式将其封装起来只提供简洁的接口。6.2 实战中的注意事项与避坑指南在实际项目中使用策略模式我总结了几条心得接口设计要稳定ValidationStrategy接口一旦确定尤其是公开给其他团队或模块使用时修改成本会很高。在设计初期就要充分考虑未来可能的需求变化尽量让接口抽象、通用。例如我们的validate接口只接收一个std::string如果未来需要校验二进制数据或结构化数据可能就需要重构。一种思路是使用std::variant或模板来设计更通用的数据参数。策略对象的创建与管理如果策略种类很多且经常需要根据配置或输入动态创建可以考虑引入简单工厂模式Simple Factory或工厂方法模式Factory Method来封装对象的创建逻辑避免客户端代码中散落着大量的new或std::make_unique。例如可以有一个ValidationStrategyFactory根据一个枚举或字符串返回对应的策略对象。class ValidationStrategyFactory { public: static std::unique_ptrValidationStrategy create(const std::string type) { if (type user_input) return std::make_uniqueUserInputValidationStrategy(); if (type config_file) return std::make_uniqueConfigFileValidationStrategy(); if (type api_response) return std::make_uniqueApiResponseValidationStrategy(); // ... 处理未知类型或返回默认策略 return nullptr; } }; // 使用auto strategy ValidationStrategyFactory::create(user_input);性能考量策略模式引入了额外的抽象层虚函数调用和对象创建开销动态内存分配。对于性能极其敏感、被频繁调用的代码段例如在每秒处理百万次请求的核心循环中需要评估这种开销是否可接受。在大多数业务逻辑层这点开销微不足道带来的结构清晰度收益远大于代价。如果确实有性能问题可以考虑使用策略模式与C模板的结合在编译期确定策略类型消除虚函数开销这属于更高级的“策略模式”变体或称“静态策略模式”。与状态模式State Pattern的区别初学者容易混淆策略模式和状态模式。两者类图相似但意图不同策略模式客户端主动选择一个策略并显式地将其设置给上下文。策略之间通常独立不知道彼此的存在。关注的是替换不同的算法。状态模式状态之间的转换通常由上下文对象内部驱动或者由状态对象自身根据条件决定下一个状态。状态对象知道其他状态的存在。关注的是对象内部状态的改变导致其行为的改变。例如一个TCP连接对象其行为建立连接、发送数据、关闭连接会根据当前状态LISTEN,ESTABLISHED,CLOSE_WAIT自动变化。避免过度设计如果算法的种类很少比如只有一两种且未来几乎不可能变化那么直接使用if-else或函数指针可能是更简单直接的选择。设计模式是用来解决复杂问题的工具而不是用来增加复杂性的教条。“如无必要勿增实体”同样适用于设计模式的使用。7. 从本例延伸更复杂的策略组合与高级技巧我们这个例子展示了策略模式的基础用法。在实际的大型项目中策略模式可以玩出更多花样。7.1 带参数的策略与策略组合有时策略的执行需要一些外部参数或配置。例如我们的长度限制100和5000可能是可配置的。我们可以在策略类的构造函数中传入这些参数class UserInputValidationStrategy : public ValidationStrategy { public: explicit UserInputValidationStrategy(size_t minLen1, size_t maxLen100) : minLen_(minLen), maxLen_(maxLen) {} // ... validate 方法中使用 minLen_ 和 maxLen_ private: size_t minLen_; size_t maxLen_; }; // 使用auto strategy std::make_uniqueUserInputValidationStrategy(5, 200);更进一步我们可以创建组合策略Composite Strategy它本身也是一个策略但内部管理着一组子策略按顺序执行它们全部通过才算通过或有一个通过就算通过。class CompositeValidationStrategy : public ValidationStrategy { public: void addStrategy(std::unique_ptrValidationStrategy strategy) { strategies_.push_back(std::move(strategy)); } bool validate(const std::string data) const override { for (const auto strategy : strategies_) { if (!strategy-validate(data)) { return false; // 所有策略必须全部通过 // 或者可以记录错误继续执行下一个最后汇总报告 } } return true; } // ... private: std::vectorstd::unique_ptrValidationStrategy strategies_; }; // 使用创建一个组合策略先添加格式校验策略再添加业务规则校验策略。7.2 使用标准库组件std::function作为轻量级策略对于非常简单的策略比如只是一个判断条件使用完整的类层次结构可能显得笨重。C11的std::function和Lambda表达式提供了一种更轻量级的替代方案。class DataValidatorLight { public: using ValidatorFunc std::functionbool(const std::string); explicit DataValidatorLight(ValidatorFunc func) : validator_(std::move(func)) {} bool validate(const std::string data) const { return validator_ ? validator_(data) : false; } void setValidator(ValidatorFunc func) { validator_ std::move(func); } private: ValidatorFunc validator_; }; int main() { // 使用Lambda定义策略 auto lengthValidator [](const std::string s) { return !s.empty() s.length() 100; }; auto noScriptValidator [](const std::string s) { return s.find(“script”) std::string::npos; }; // 可以方便地组合 auto combinedValidator [](const std::string s) { return lengthValidator(s) noScriptValidator(s); }; DataValidatorLight validator(combinedValidator); bool ok validator.validate(“test”); }这种方式非常灵活适合策略逻辑简单、且不需要复杂状态和继承关系的场景。但它牺牲了策略作为“类”的封装性、可复用性和通过接口进行统一管理的能力。需要根据实际情况权衡。7.3 策略模式的测试策略如何有效地测试策略模式下的代码这里提供一些思路策略类单元测试对每个具体的ConcreteStrategy进行独立的单元测试。测试用例应覆盖其所有校验规则的正向和反向情况。由于策略类无状态或状态简单测试起来非常容易。上下文类单元测试测试DataValidator时重点在于验证其“委托”行为是否正确。我们需要使用测试替身Test Double比如Mock对象。使用Google Mock/GTest可以创建一个MockValidationStrategy继承自ValidationStrategy并使用MOCK_METHOD宏来模拟validate方法。在测试中我们可以精确断言validate方法是否被调用、调用了几次、传入的参数是什么。// 伪代码示例 class MockValidationStrategy : public ValidationStrategy { public: MOCK_METHOD(bool, validate, (const std::string), (const, override)); MOCK_METHOD(std::string, getStrategyName, (), (const, override)); }; TEST(DataValidatorTest, DelegatesToStrategy) { auto mockStrategy std::make_uniqueMockValidationStrategy(); EXPECT_CALL(*mockStrategy, validate(“test data”)).WillOnce(Return(true)); DataValidator validator(std::move(mockStrategy)); EXPECT_TRUE(validator.validate(“test data”)); }手动模拟如果不使用Mock框架可以创建一个简单的测试专用策略类在validate方法中记录调用信息供测试断言。集成测试创建真实的策略对象和上下文对象进行端到端的业务逻辑测试确保整个链条工作正常。通过策略模式我们将原本铁板一块的代码拆分成了一系列高内聚、低耦合的组件。每个组件都变得易于理解、测试和维护。当需求变更时我们不再需要心惊胆战地修改核心函数而是可以自信地扩展新的策略类。这种代码组织方式是应对软件复杂性的有力武器也是写出高质量、可长期维护的C代码的关键技巧之一。下次当你看到代码中又出现那些熟悉的、不断增长的if-else或switch语句时不妨停下来想一想这里是不是策略模式该登场的地方了