ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C++策略模式实战:用接口替换if-else,告别订单折扣代码泥潭

C++策略模式实战:用接口替换if-else,告别订单折扣代码泥潭 做C开发这几年我发现自己写的最多的一段代码就是if(条件) then A else B。尤其到了业务功能膨胀的阶段一个函数里塞上七八种分支是常有的事。前两年我做一个订单结算模块时就被这种写法折腾得不轻每加一种优惠方式就得改动核心计算函数改完还得祈祷别把其他用户的逻辑搞坏。后来我把这块重构成了策略模式用一个策略接口替换掉散落四处的条件判断几个月下来新增了三四套规则核心函数一行没动。这篇文章就围绕这套实战经验展开聊聊在C里怎么用策略模式重构这类分支逻辑、有哪些实现姿势可选、以及哪些坑是文档里不会写但你迟早会碰到的。C里的策略模式简单说就是把“算法”单独抽成一组可替换的类或函数对象让业务代码只依赖一个稳定的接口。它最适合的场景就是同一个行为有多种实现方式而且运行期间可能来回切换。比如支付渠道、折扣规则、压缩算法、排序策略都能用它收拾得干干净净。适合刚接触设计模式、想在真实项目里落地的C开发者参考也适合被一堆if-else折磨得想重构的老手。1. 为什么要在C项目里用策略模式1.1 从一个真实的“if-else地狱”开始先看一段我当年写出来的“原型代码”你是不是也觉得眼熟enum class UserType { kNormal, kVip, kNewUser, kEmployee }; double ComputePrice(UserType type, double price) { if (type UserType::kNormal) { if (price 100) return price * 0.9; return price; } else if (type UserType::kVip) { return price * 0.8; } else if (type UserType::kNewUser) { return price * 0.85; } else if (type UserType::kEmployee) { return price * 0.75; } return price; }这段代码的问题不是“能不能跑”而是“后续怎么改”。产品经理告诉你下个版本要加“满三件打六折”你得改这个函数要加“会员日折上折”你还得改这个函数。每改一次所有调用它的地方都在跟着冒险——可能影响老用户的折扣计算可能影响新用户的首单价格。而且随着策略越来越多函数体越来越长条件之间的优先级稍微排错bug就藏进逻辑里了。我当时遇到的实际场景更夸张这个函数被三个业务模块同时调用有两个模块已经各自在外面包了几层临时判断。冰淇淋加巧克力再撒坚果最后谁都不敢动了。这时候才意识到问题不在“代码写得好不好”而在“设计上就没有给变化留位置”。1.2 策略模式到底帮你解决了什么策略模式的答案很朴素把每一种优惠算法封装成独立的类或函数业务代码只保留一个“策略接口”的指针或引用。上面那个例子用策略模式重构后计算函数不再关心用户是什么类型只关心“你给我哪个策略对象”。它解决的核心问题有三个开闭原则新增策略时不改动原有业务代码只新增一个策略类。对扩展开放对修改关闭。消除条件分支分支逻辑从业务函数里移到策略创建的地方核心计算函数保持精简。运行时切换同一个上下文对象可以随时替换内部策略setStrategy一调用行为立刻改变。你可以用导航App来类比从A到B有驾车路线、公交路线、骑行路线。你不关心每条路线的具体计算逻辑你只负责选一种导航引擎拿着你选的策略去算路。新增一种“步行路线”导航引擎完全不用改。这个理解到位了再看C的实现姿势就顺理成章了。2. C实现策略模式的三种常见姿势2.1 经典写法抽象基类加虚函数这是设计模式书上最常见的写法。定义一个抽象策略类定义纯虚函数然后让不同策略类各自实现。class IDiscountStrategy { public: virtual ~IDiscountStrategy() default; virtual double Calculate(double originalPrice) const 0; }; class NormalDiscount : public IDiscountStrategy { public: double Calculate(double originalPrice) const override { return originalPrice 100 ? originalPrice * 0.9 : originalPrice; } }; class VipDiscount : public IDiscountStrategy { public: double Calculate(double originalPrice) const override { return originalPrice * 0.8; } };配合一个上下文类来使用class PriceCalculator { public: explicit PriceCalculator(std::shared_ptrIDiscountStrategy strategy) : strategy_(std::move(strategy)) {} void SetStrategy(std::shared_ptrIDiscountStrategy strategy) { strategy_ std::move(strategy); } double GetPrice(double originalPrice) const { if (!strategy_) return originalPrice; return strategy_-Calculate(originalPrice); } private: std::shared_ptrIDiscountStrategy strategy_; };老生常谈的几点基类一定要声明虚析构函数override要写上Calculate如果不会改变内部状态就加上const。这套写法最大的优点是结构清晰、一看就懂类之间的关系明明白白最大的缺点是“重”——你定义一个策略要单独建一整个类类多了以后源文件也多了命名、组织、维护都有成本。2.2 现代写法std::function函数对象从C11开始std::function让策略模式轻量了很多。策略不再必须是“类”也可以是一个lambda表达式、函数指针、仿函数只要签名匹配就行。#include functional using DiscountStrategy std::functiondouble(double); class PriceCalculatorModern { public: explicit PriceCalculatorModern(DiscountStrategy strategy) : strategy_(std::move(strategy)) {} void SetStrategy(DiscountStrategy strategy) { strategy_ std::move(strategy); } double GetPrice(double originalPrice) const { return strategy_ ? strategy_(originalPrice) : originalPrice; } private: DiscountStrategy_; };调用的时候就很爽了PriceCalculatorModern calc([](double price) { return price 100 ? price * 0.9 : price; }); calc.SetStrategy([](double price) { return price * 0.8; });如果你需要在lambda里捕获状态比如VIP等级、用户ID那更好用int vipLevel 3; PriceCalculatorModern calc2([vipLevel](double price) { return vipLevel 2 ? price * 0.7 : price; });如果你是配置好环境跟着跑这里提一嘴这种单文件demo在VSCode里装好C/C扩展再配一个tasks.json直接编译运行就够了不用开大型IDE折腾工程配置。等代码量上来了再迁移到CMake工程也不迟。2.3 选型不是所有策略都必须用虚函数我把三种实现方式放一起对比过方便你按场景选实现方式运行时多态捕获状态扩展成本适用场景抽象基类虚函数完全支持只能靠成员变量中等策略数量多、策略内部逻辑重、需要组合复用std::function完全支持lambda可直接捕获极低策略简单、策略数量不多、快速迭代普通函数指针支持不支持低无状态策略、追求极致性能我个人在实际项目里的体会是如果你的策略只是“一个函数”的差别比如打折、减满、计价比例调整直接用std::function别过度设计。如果你的策略本身有一组相关操作比如一个“完整加密方案”包含加密、解密、握手、校验等多个方法那抽象基类更合适。还有一种组合思路对外暴露抽象基类接口对内用std::function作为实现细节兼顾扩展性和灵活性。3. 实战案例订单折扣策略系统3.1 需求梳理与接口设计说个我可以完整复现的案例。假设要做一套订单折扣系统规则一开始就有三种新人首单打85折满100减20VIP用户直接7折产品还特别强调同一个订单结算时只能选一种最优惠的规则而且后续规则随时可能增加。我拿到需求后第一件事就是把“变化点”标出来。变化的是折扣算法不变的是“给定原价算出一个折扣价”这个动作。于是接口就长这样class IDiscountStrategy { public: virtual ~IDiscountStrategy() default; virtual double Discount(double originalPrice) const 0; virtual std::string Name() const 0; };把Name()也放进接口是为了后面打印日志、告诉用户“你享受了哪种优惠”用的。实战项目里千万别省这种“看起来不必要”的方法以后排查线上问题时它就是救命稻草。3.2 核心实现代码运行起来三个策略类分别实现。class NewUserDiscount : public IDiscountStrategy { public: double Discount(double price) const override { return price * 0.85; } std::string Name() const override { return 新人85折; } }; class FullReductionDiscount : public IDiscountStrategy { public: double Discount(double price) const override { return price 100 ? price - 20 : price; } std::string Name() const override { return 满100减20; } }; class VipDiscount : public IDiscountStrategy { public: double Discount(double price) const override { return price * 0.7; } std::string Name() const override { return VIP七折; } };上下文类负责持有策略并支持运行期切换class OrderDiscounter { public: explicit OrderDiscounter(std::unique_ptrIDiscountStrategy strategy) : strategy_(std::move(strategy)) {} void SetStrategy(std::unique_ptrIDiscountStrategy strategy) { strategy_ std::move(strategy); } double Calculate(double originalPrice) const { if (!strategy_) return originalPrice; double finalPrice strategy_-Discount(originalPrice); // 这里可以统一记日志、统计、校验最低价等 return finalPrice; } const IDiscountStrategy* GetStrategy() const { return strategy_.get(); } private: std::unique_ptrIDiscountStrategy strategy_; };接下来是主流程。用户根据自己的身份拿到一个策略对象塞进OrderDiscounter里算出结果#include iostream #include memory int main() { auto discounter OrderDiscounter(std::make_uniqueNewUserDiscount()); std::cout 原价200新人实付: discounter.Calculate(200) std::endl; discounter.SetStrategy(std::make_uniqueFullReductionDiscount()); std::cout 原价200满减免实付: discounter.Calculate(200) std::endl; discounter.SetStrategy(std::make_uniqueVipDiscount()); std::cout 原价200VIP实付: discounter.Calculate(200) std::endl; return 0; }我实测的三种结果是170、180、140。逻辑符合预期新用户考满减不如新人折扣优惠VIP还是最猛的那一个。值得注意的是这里OrderDiscounter本身不负责创建策略对象。把“策略的创建”和“策略的使用”分开是策略模式保持扩展性的关键一环。创建的职责我后面会挪到工厂里。3.3 如何扩展新的优惠策略把策略应用于业务代码做法一般是这样的创建实现IDiscountStrategy的新类或新增lambda。在策略创建的地方注册这个新策略。业务侧根据用户身份获取对应策略对象。调用discounter.SetStrategy(...)替换。以“员工6折”为例只需要新增一个EmployeeDiscount类然后接入工厂订单计算逻辑完全不用动。这叫对修改关闭对扩展开放。如果要更自动化一点可以做个策略注册表稍后我会在常见问题部分详细写。这个注册表的价值在于新增策略时连业务代码里的创建分支都不用改只往注册表里插一条记录即可。3.4 为什么这样选型这个案例我选择抽象基类虚函数而不是std::function是因为折扣策略未来很可能不仅仅是“一个计算函数”。它可能还要附带活动名称、生效时间、适用人群甚至要对接外部优惠券系统。一旦策略内部复杂了lambda那张皮就包不住了。抽象基类允许你自然地往接口里加方法策略类本身也能扩展自己的属性。但是我也很清楚如果我只需要临时启用一个折扣根本用不着动工程结构。直接用lambda Settle 掉一天不用写类。这个判断思路很重要策略模式是为“变化频率高、替换方向明确”的算法准备的不加区分地套用只会给项目增加无谓的复杂度。4. 实操中常见问题与排查技巧4.1 六个容易踩的坑我在项目里踩过、也在Code Review时见别人踩过的坑整理成一张表坑现象解决办法基类漏写虚析构函数删除策略对象时子类析构不被调用内存泄漏基类析构函数声明为virtual ~IDiscountStrategy() default;将策略对象按值传入多态被切割子类行为丢失传指针、引用或std::unique_ptr禁止传裸对象值策略对象生命周期管理混乱上下文持裸指针策略被提前释放野指针崩溃统一用std::unique_ptr或std::shared_ptr接口方法忘记加const无法在常成员函数中调用或无法传递常量引用策略计算函数尽量设计为const策略创建逻辑散落在调用方每处都if-else创建策略和没重构一样收拢到工厂函数或策略注册表空策略对象不处理上下文拿到nullptr直接崩溃Calculate入口判空返回原价或抛出明确异常第一条我尤其想多说一句。C的内存安全是靠纪律堆出来的。基类析构函数如果非虚delete一个指向子类对象的基类指针时子类部分不会析构。设计模式书里都会提这一点但实战中从std::unique_ptrIDiscountStrategy拿get()传给某个不规范的代码前一定要确认这个析构链路是完整的。4.2 策略模式和状态模式别搞混很多人写代码时容易把“状态”误当作“策略”。这两者有个本质区别策略模式外部主动切换算法对象本不知道也不关心切换的原因。状态模式行为随内部状态自发改变状态的转移通常由对象自身逻辑驱动。举个能区分开的例子。一个订单对象处于“待支付”状态时执行支付操作处于“已支付”状态时同一个方法可能变成“退款”。这通常是状态模式。而“不同会员等级使用不同折扣算法”这是策略模式。如果你发现自己的策略模式里上下文对象频繁主动调用SetStrategy而且策略的切换依赖当前业务状态那你要么需要的是一个状态机要么需要把状态封装和策略切换结合起来否则最后会出现一堆“在A策略里调用SetStrategy切到B策略”的诡异代码。4.3 策略注册表让新增策略不再改业务代码实战里我比较推荐一个组合拳策略接口 策略工厂 字符串注册表。class DiscountStrategyFactory { public: using Creator std::functionstd::unique_ptrIDiscountStrategy(); static void Register(const std::string key, Creator creator) { registry()[key] std::move(creator); } static std::unique_ptrIDiscountStrategy Create(const std::string key) { auto it registry().find(key); if (it registry().end()) return nullptr; return it-second(); } private: static std::mapstd::string, Creator registry() { static std::mapstd::string, Creator instance{}; return instance; } };然后在每个策略类里用静态初始化完成自注册class EmployeeDiscount : public IDiscountStrategy { public: static bool registered; double Discount(double price) const override { return price * 0.6; } std::string Name() const override { return 员工六折; } }; bool EmployeeDiscount::registered DiscountStrategyFactory::Register(employee, []() { return std::make_uniqueEmployeeDiscount(); });这样新增策略时只需要写好策略类并注册主工程代码完全不用改。产品上线后要上一种新活动开发基本就是“加一个文件”的事。缺点也有注册的时机依赖静态初始化顺序跨编译单元时可能要小心策略多了以后隐藏依赖反而不好排查。我一般是在中型项目里这么用小型项目直接工厂函数够了。4.4 性能与可测试性的心得关于虚函数的性能焦虑一个虚函数调用通常只是一次间接跳转开销在纳秒级别。如果你真的在一个千万级循环里调策略可以考虑把Calculate标记为final让编译器有机会去虚函数化。关于测试这是我用策略模式之后最顺手的地方。以前测试折扣逻辑要构造各种用户类型、各种订单状态现在每个策略类都是独立的小单元一个测试文件测一个策略数据表直接铺开边界情况清清楚楚。业务代码的测试只需要验证“正确调用了对应策略”mock 掉策略接口就行。最后分享一个小技巧如果策略与策略之间有“公共的前置处理逻辑”不要在每个策略里重复写而是放到上下文类的Calculate入口里统一处理。例如所有优惠都不能让最后实付低于0或者都要先四舍五入到分——这些规则固定不变放在上下文里一次搞定。注意这个“固定不变”的判断要慎重一旦某天某个策略对这个公共逻辑有特殊要求你就得重新评估是把它下沉到上下文还是上浮到策略类了。我个人在实际项目里踩过的最大弯路是刚开始重构时恨不得把所有分支全部换成策略模式结果一个十几行的函数被拆成五个类加一个工厂阅读成本不降反增。策略模式不是万能的它解决的是“算法族替换频繁、新增策略需要隔离风险”的问题。如果你的分支一年都动不了几回老老实实写switch就够了。用它的边界感和用好它本身一样重要。
返回列表