ARTICLE DETAIL

资讯详情

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

策略模式实战:用Java与C++优雅替代if-else

策略模式实战:用Java与C++优雅替代if-else 我先问一个很现实的问题你是不是也写过那种几十个分支的if-else或者switch-case每次产品经理过来说“加个新规则”你都得在方法里偷偷补一段逻辑生怕影响其他分支如果有那么设计模式里的策略模式大概率就是你要找的那个解药。这篇文章我不打算讲太多教科书式的大道理而是结合 Java 和 C 的实战代码把策略模式从头到尾拆一遍包括它到底在解决什么问题、三个角色的职责划分、完整重构过程、常见误用以及我实际踩过的坑。无论你是在准备期末考试、做设计模式大作业还是已经在项目里被变更需求折磨得头大这篇都适合你。1. 策略模式到底在解决什么问题1.1 先从一堆 if-else 说起很多人第一次接触策略模式是被《Head First 设计模式》里那个鸭子例子搞懵的一会儿 FlyBehavior一会儿 QuackBehavior好像很抽象跟手上的业务代码完全对不上。我换个场景你马上就能懂。假设你写了一个订单计费系统目前有三种用户等级普通用户、会员、VIP。计费逻辑长这样public double calculate(double price, String userLevel) { if (normal.equals(userLevel)) { return price; } else if (member.equals(userLevel)) { return price * 0.9; } else if (vip.equals(userLevel)) { return price * 0.8; } return price; }第一版只有三行看着还行。但接下来产品经理说大客户打七折、节假日叠加九五折、新用户首单五折、内部员工特殊折扣……你会发现这个方法的 body 会越来越长每个折扣的计算规则互相纠缠改一个分支可能影响另一个分支。最可怕的是测试的时候要把每个分支都覆盖一遍而每次新增分支回归的范围都指向同一个方法。这个问题的本质是什么是“算法”和“使用算法的客户端”被硬耦合在一起了。策略模式做的事情就是把一组可以互相替换的算法封装起来让客户端可以独立于算法本身去使用它们。1.2 三个角色的职责划分策略模式的标准结构只有三个角色一定要记牢角色类名习惯职责策略接口DiscountStrategy定义算法族的统一入口通常是一个抽象方法具体策略NormalDiscount、MemberDiscount各自实现一套算法互不干扰上下文OrderCalculator持有策略接口引用负责调用策略也可能负责切换策略用一句话概括上下文是“老板”策略是“员工”老板不关心员工具体怎么干只关心你干的活能不能满足接口约定的要求。类图不需要死记你只需要记住依赖方向上下文依赖策略接口具体策略也实现策略接口上下文中绝对不要直接 new 某个具体策略类。一旦你在上下文里写了某个具体策略类的名字这个模式的扩展性就废了一半。1.3 为什么“多用组合少用继承”在这里最有说服力设计模式里有条很出名的原则叫“Favor composition over inheritance”很多新手不理解。策略模式就是最好的例子。如果你用继承去实现折扣逻辑会怎么做定义一个VIPUser继承User然后重写calculate方法。这看起来很自然但问题在于折扣是一个容易变化的维度用户身份又是一个容易变化的维度两个维度叠加起来继承树就会爆炸。比如“VIP用户 节假日 新客首单”你怎么继承总不能为每种组合都搞一个子类吧策略模式换了一个思路把“变化的部分”折扣算法单独抽出来用组合的方式注入到上下文里。身份还是那个身份但是算钱的时候你可以随时给这个订单换上不同的折扣策略。这样两个维度的变化就解耦了。这就是策略模式最核心的思维把变化封装起来用组合替代继承。2. 动手实现一个计费接口的完整重构过程2.1 需求场景一个刚需的计费系统我拿一个很常见的业务来演示订单结算服务。要求如下普通用户原价会员95 折VIP8 折企业客户按协议价可能是固定折扣也可能是固定减免金额后续还可能有满减活动、限时折扣、内部测试价……注意最后一条这是关键。设计模式从来不是因为当前需求复杂才上而是因为你已经能预见到“规则会继续变”才上。如果你确定十年不变那写 if-else 也没毛病不用为了模式而模式。2.2 第一步先定义策略接口策略接口的设计是整个模式的灵魂。接口的方法参数怎么定、返回值怎么定决定了后续每个策略长什么样。public interface DiscountStrategy { /** * 计算最终支付金额 * * param originalPrice 订单原始金额 * param orderInfo 订单信息可能包含用户等级、商品分类等上下文数据 * return 优惠后的金额 */ double calculate(double originalPrice, OrderInfo orderInfo); }我建议参数不要只传一个 price因为很多策略需要额外的上下文信息。比如企业客户可能要根据合同号查一下协议折扣满减策略要根据订单里有没有指定品类来判断。所以传一个OrderInfo对象进去扩展性会好很多。这是我在实际业务里用出来的经验策略接口的方法参数尽量传一个上下文对象而不是一堆零散参数不然每次加规则都要改接口签名。2.3 第二步实现具体策略类每个策略一个类类名就是业务规则的名字看了就知道它干什么。public class NormalDiscount implements DiscountStrategy { Override public double calculate(double originalPrice, OrderInfo orderInfo) { return originalPrice; } } public class MemberDiscount implements DiscountStrategy { Override public double calculate(double originalPrice, OrderInfo orderInfo) { return originalPrice * 0.95; } } public class VipDiscount implements DiscountStrategy { Override public double calculate(double originalPrice, OrderInfo orderInfo) { return originalPrice * 0.8; } } public class EnterpriseDiscount implements DiscountStrategy { Override public double calculate(double originalPrice, OrderInfo orderInfo) { // 企业协议价根据合同号查询折扣率 double ratio queryContractRatio(orderInfo.getContractNo()); return originalPrice * ratio; } private double queryContractRatio(String contractNo) { // 伪代码这里走RPC或者查本地配置表 return 0.75; } }你看每个策略内部怎么做都是自己的事。EnterpriseDiscount里查合同、走 RPC其他策略完全不知道也不受影响。这就是“封装变化”的直观好处你新增一个策略只需要加一个类不需要动其他任何已有类。2.4 第三步写上下文类并改造调用方上下文类在简单场景下可以只是一个持有策略引用的工具类复杂场景下还可以负责初始化默认策略、策略切换、记录日志等。public class OrderCalculator { private DiscountStrategy strategy; public OrderCalculator(DiscountStrategy strategy) { this.strategy strategy; } public void setStrategy(DiscountStrategy strategy) { this.strategy strategy; } public double settle(double originalPrice, OrderInfo orderInfo) { double result this.strategy.calculate(originalPrice, orderInfo); // 这里可以做日志、埋点、审计等横切操作 return result; } }客户端这样用OrderInfo order new OrderInfo(); order.setUserLevel(vip); order.setOriginalPrice(1000); DiscountStrategy strategy StrategyFactory.getStrategy(order.getUserLevel()); OrderCalculator calculator new OrderCalculator(strategy); double finalPrice calculator.settle(order.getOriginalPrice(), order);改造完成之后你原来的calculate方法变成了一组类核心业务逻辑和算法细节分开了后续加策略、改策略都变得可控。这一步走完策略模式在项目里已经基本落地了。2.5 高配版简单工厂 策略让调用方彻底解耦上面的代码里客户端还要自己判断userLevel对应哪个策略。这一步判断逻辑如果散落各处还是会重复。更常见的做法是引入一个策略工厂public class StrategyFactory { private static final MapString, DiscountStrategy STRATEGY_MAP new HashMap(); static { STRATEGY_MAP.put(normal, new NormalDiscount()); STRATEGY_MAP.put(member, new MemberDiscount()); STRATEGY_MAP.put(vip, new VipDiscount()); STRATEGY_MAP.put(enterprise, new EnterpriseDiscount()); } public static DiscountStrategy getStrategy(String userLevel) { DiscountStrategy strategy STRATEGY_MAP.get(userLevel); if (strategy null) { throw new IllegalArgumentException(No strategy for level: userLevel); } return strategy; } }客户端代码会变成OrderInfo order buildOrderInfo(); double finalPrice new OrderCalculator(StrategyFactory.getStrategy(order.getUserLevel())) .settle(order.getOriginalPrice(), order);这个组合非常经典工厂负责“根据条件选策略”策略对象负责“具体怎么做”上下文负责“调用框架”。三者的责任边界非常清晰。我在真实项目中就用这个套路做过优惠券引擎几十种券模板就是几十个策略类工厂通过券类型编码去加载对应策略新增券模板时业务代码一行都不用改。3. Java 与 C 实现对照套路相同细节不同既然热搜词里反复出现“设计模式 java 实现”和“C 23 种设计模式”说明大家在看策略模式时通常是在学两门语言。我在这里把 C 的常见写法也一起讲了。3.1 C 经典写法抽象基类 智能指针C 里最正统的写法是用抽象基类定义策略接口用std::unique_ptr或std::shared_ptr管理策略对象生命周期。#include iostream #include memory class DiscountStrategy { public: virtual double calculate(double price) const 0; virtual ~DiscountStrategy() default; }; class NormalDiscount : public DiscountStrategy { public: double calculate(double price) const override { return price; } }; class MemberDiscount : public DiscountStrategy { public: double calculate(double price) const override { return price * 0.95; } }; class OrderCalculator { public: explicit OrderCalculator(std::unique_ptrDiscountStrategy strategy) : strategy_(std::move(strategy)) {} void setStrategy(std::unique_ptrDiscountStrategy strategy) { strategy_ std::move(strategy); } double settle(double price) const { return strategy_-calculate(price); } private: std::unique_ptrDiscountStrategy strategy_; }; int main() { auto calc OrderCalculator(std::make_uniqueMemberDiscount()); std::cout calc.settle(1000) std::endl; // 950 calc.setStrategy(std::make_uniqueNormalDiscount()); std::cout calc.settle(1000) std::endl; // 1000 return 0; }这里有个 C 特有的注意点析构函数必须声明为 virtual。如果你忘了写虚析构通过基类指针删除派生类对象时就是未定义行为轻则内存泄漏重则程序崩溃。这也是面试官最爱问的 C 策略模式陷阱。3.2 C 现代写法std::function 让策略“轻”起来如果你的策略只是“一个函数”而不是“一族对象”C 里完全可以用std::function实现更轻量的策略模式连接口类和策略类都省了#include iostream #include functional class OrderCalculator { public: using Strategy std::functiondouble(double); explicit OrderCalculator(Strategy strategy) : strategy_(std::move(strategy)) {} void setStrategy(Strategy strategy) { strategy_ std::move(strategy); } double settle(double price) const { return strategy_(price); } private: Strategy strategy_; }; int main() { OrderCalculator calc([](double p) { return p * 0.95; }); std::cout calc.settle(1000) std::endl; // 950 calc.setStrategy([](double p) { return p * 0.8; }); std::cout calc.settle(1000) std::endl; // 800 return 0; }这也是我在实际 C 项目里用得最多的形式。当策略无状态、只关注算法本身时lambda 表达式比定义一堆策略类高效得多代码也更紧凑。但是要注意如果策略本身有状态比如要持有合同号、要连接外部服务还是老老实实用类方案。3.3 Java 中的函数式写法Map LambdaJava 8 之后你也可以用函数接口 Lambda 简化策略注册public class StrategyFactory { private static final MapString, FunctionOrderInfo, Double STRATEGY_MAP new HashMap(); static { STRATEGY_MAP.put(normal, order - order.getOriginalPrice()); STRATEGY_MAP.put(member, order - order.getOriginalPrice() * 0.95); STRATEGY_MAP.put(vip, order - order.getOriginalPrice() * 0.8); STRATEGY_MAP.put(enterprise, order - { double ratio queryContractRatio(order.getContractNo()); return order.getOriginalPrice() * ratio; }); } public static FunctionOrderInfo, Double getStrategy(String userLevel) { return STRATEGY_MAP.getOrDefault(userLevel, order - { throw new IllegalArgumentException(unsupported level); }); } }函数式写法适合策略逻辑非常简单的场景一旦策略体超过十行或者要依赖其他服务还是建议抽成完整的策略类否则Map里的 Lambda 会变成一坨谁也看不懂的“代码集中营”。4. 策略模式常见误用到底该不该用、和谁搭配4.1 策略模式 vs 工厂模式两者不是替代关系很多初学者会把策略模式和工厂模式搞混。其实它们的关注点完全不同工厂模式解决的是“对象怎么创建”的问题。调用方说“我要一个折扣策略”工厂负责把对应的策略对象 new 出来。策略模式解决的是“算法怎么替换”的问题。上下文说“我要用折扣策略计算出价格”策略对象负责执行算法。所以在实战里工厂往往是策略模式的搭档而不是对手。你完全可以用工厂来选择策略再用策略模式来执行算法。这也是 2.5 节那个组合为什么踩着这么多年的坑还没过时。4.2 策略模式 vs 状态模式别看走眼策略模式和状态模式的 UML 结构确实很像都是上下文持有一个策略/状态对象都是通过组合来改变行为。但它们解决的是完全不同的问题状态模式关注的是“状态内部转移”。状态对象自己决定下一个状态是谁上下文的状态会随着业务推进自动切换。策略模式关注的是“算法替换”。策略对象不关心下一个策略是谁切换策略是由外部调用方或工厂决定的。用一个例子区分订单状态机待支付、已支付、已发货、已完成适合状态模式因为状态之间有转移逻辑订单计费适合策略模式因为折扣算法之间没有依赖关系。判断标准很简单如果你的对象会“自动变形”那是状态模式如果只是“换一个做法”那是策略模式。4.3 什么时候真的别用策略模式策略模式虽好但也别拿到什么都往里套。我见过最夸张的案例是有人把一行a b都封装成了策略类一个项目新增十几个类文件。这是典型的过度设计。下面几种情况不建议用策略模式算法基本不变如果你的折扣规则一年到头就那两条写 if-else 更直观可维护性也不差。策略类之间没有公共接口价值如果两个“策略”的入参、出参都不同硬凑到一个接口下接口只会变得不伦不类。团队维护水平不够策略模式依赖多态如果团队里有新手看不懂接口、组合这些概念代码反而会更快腐化。设计模式是工具不是装饰品。当变化确实存在且变化频率较高时策略模式才真正有价值。5. 实战中的问题排查与避坑经验5.1 同一个策略对象要复用吗单例还是每次新建策略对象一般是无状态的除了一些配置数据所以天然适合单例。我在项目里通常把策略注册到Map中一次性创建后续就复用。这样省去了反复new的开销也方便测试时替换。但要注意如果策略对象内部持有可变状态比如计数、缓存那就不能共享否则多线程环境下会出大问题。我曾经遇到一个线上故障就是因为策略类里加了一个实例变量记录调用次数而策略对象在工厂里是单例的导致并发环境下计数错乱还影响到了后续订单的折扣计算。这件事之后我定了一条规矩无状态策略才允许单例有状态策略必须每次新建。5.2 策略类爆炸怎么办用内部类、Lambda 和模板方法压缩一个项目里几十个策略类确实会显得类很多。我常用的压缩方案有三种逻辑简单且只在一处使用用 Lambda /std::function替代独立类。策略之间有大段公共逻辑先抽一个抽象基类做模板方法具体策略只覆盖差异部分。策略按业务域聚合比如把所有“营销活动折扣”策略放到一个包promotion下把所有“用户等级折扣”放到member下别让策略类散落在项目各个角落。命名也很重要。策略类名直接使用业务语言如FirstOrderDiscount、FullReductionDiscount不要叫什么StrategyA、StrategyB。好的命名能让半年后的你一眼就知道这个策略是干什么的。5.3 策略切换的时机和上下文状态有一个很隐蔽的坑策略切换后上下文中已有的临时状态可能不能沿用。比如你在OrderCalculator里缓存了上一次计算的明细此时切换到新策略缓存中的计算结果可能是用旧策略算出来的如果直接复用就会出错。我的建议是策略切换之后必须清理或重建上下文中的缓存字段。如果你不想每次切换都担心可以考虑把策略对象设计成不可变对象并把上下文中的缓存收敛到策略内部而不是放在上下文里。这样策略一换旧缓存自然作废。5.4 测试策略模式时怎么 Mock策略模式的优点之一就是很好测。你不需要启动完整业务只需要针对某个具体策略类写单测即可。Test void testMemberDiscount() { DiscountStrategy strategy new MemberDiscount(); double result strategy.calculate(1000, someOrderInfo()); assertEquals(950, result, 0.001); }如果上下文依赖了外部服务比如EnterpriseDiscount要查合同那就把查询逻辑抽象成接口单测中注入一个 Mock 实现。记住策略模式让每个算法的测试边界变得清晰你要做的就是把外部依赖控制在策略内部的小接口上别让策略直接依赖具体 RPC 或 DAO 实现。这样可以很方便地替换成测试替身。5.5 大作业和面试里怎么讲出亮点如果你是在做设计模式大作业或者准备面试我建议你按这个顺序讲先抛出痛点用一个 if-else 不断膨胀的真实例子说明变化带来的维护成本。再画三个角色策略接口、具体策略、上下文说清楚依赖方向。现场写关键代码不用写全部写接口和上下文即可。最后讲一个扩展案例比如“如果我要新增一个企业协议折扣只需要加一个策略类注册到工厂客户端一行不改”。面试官其实不关心你会不会背定义他关心的是你有没有在真实场景里做过取舍。这时候你补一句“我不用策略模式的情况是规则很少且不变”比你背十遍“策略模式封装了一族算法”有用得多。6. 策略模式的进阶场景游戏开发与大型业务系统热搜词里出现了“设计模式与游戏完美开发”我顺便聊聊游戏开发里策略模式怎么用这也是一个很容易出彩的场景。6.1 游戏 AI 与角色行为策略游戏里的角色 AI 是策略模式的经典舞台。比如一个怪物有三种攻击方式近战、远程、范围毒爆。你在开发时就该定义IAttackStrategy接口再分别实现MeleeAttack、RangedAttack、PoisonAttack。怪物根据玩家距离、血量、技能冷却等条件动态切换攻击策略public class Monster { private IAttackStrategy attackStrategy; public void update() { if (player.isFar() skillOnCooldown) { setStrategy(new RangedAttack()); } else if (player.getHealth() 20) { setStrategy(new PoisonAttack()); } attackStrategy.attack(player); } }相比写一长串if-else控制攻击逻辑策略模式让每个行为变成了独立单元游戏策划调数值、加新行为时程序员不需要动核心战斗逻辑。这在大作业里绝对是降维打击级别的设计。6.2 大型业务系统里的规则引擎在电商、营销、金融这类后端系统里策略模式最常被用来做规则引擎的骨架。比如优惠券系统券模板分直减券、折扣券、满减券、叠加券每一种券下去就是一个策略。你把“选券”逻辑交给工厂“算券”逻辑交给策略用户下单时再把选出的策略列表注入上下文执行。这种设计的收益很直接新增一种券只需新增一个策略类。每个策略可以独立测试。规则之间通过编排器组合互不干扰。我在实际项目里还加过一个“策略链”的概念先顺序执行多个策略每个策略决定自己是否命中命中则改写订单金额。这就从策略模式自然过渡到了责任链模式。设计模式之间是可以组合的这比单一模式更有威力。6.3 稍作扩展策略注册表与注解驱动如果你不想每次加策略都改工厂可以考虑用注册表模式 反射/注解。Java 里可以用EnumMap建立策略枚举与策略对象的映射public enum DiscountType { NORMAL(NormalDiscount.class), MEMBER(MemberDiscount.class), VIP(VipDiscount.class); public final Class? extends DiscountStrategy clazz; DiscountType(Class? extends DiscountStrategy clazz) { this.clazz clazz; } }然后在 Spring 这类框架下你甚至可以直接注入一个MapString, DiscountStrategySpring 会自动把容器中所有策略实现注册进来Service public class StrategyRegister { private final MapString, DiscountStrategy strategyMap; public StrategyRegister(MapString, DiscountStrategy strategyMap) { this.strategyMap strategyMap; } }这样新增策略类时只需要加Component(enterprise)工厂代码都不用动。当然框架的魔法会掩盖一部分流程新手看起来可能有点懵所以我建议先用普通 Map 把原理吃透再上注解驱动提升开发效率。我在实际项目的体会是策略模式最大的价值不是“消除 if-else”这么简单而是它逼着你先想清楚“什么是稳定的什么是会变的”然后把会变的东西隔离起来。你写代码时每次觉得很痛苦、改动一处就牵连一片那大概率就是变化没有被封装好的信号。策略模式不一定是你唯一的选择但它绝对是一把称手的工具。如果你现在手上正好有一团乱麻的算法分支不妨今天就试着抽出其中一个接口再配上两个实现类你会发现代码突然就顺眼了很多。
返回列表