周二早上,闹钟响的时候,天刚蒙蒙亮。
小王眼睛都没睁开,伸手把闹钟按掉,脑子里还是那五步:起床、洗漱、早餐、通勤、打卡。
流程没变,变的是今天的第一件事------他拉开冰箱,能当早饭的一样没有。
昨天加班到十点,回家路上该买的一样没买。冷藏室剩半盒不知道哪天的牛奶,冷冻层连速冻饺子都没给他留一袋。
他愣了两秒,很自然地换了方案:不做了,到公司楼下买包子。
洗漱、出门、骑车到地铁站、坐地铁。出站,走到公司楼下,在那家连锁包子店买了两个雪菜包,付钱时手机往柜台那个蓝色圆环上一碰,还立减了几块。拎着袋子往楼里走,进闸机打卡,到工位坐下才拆开吃。
省下的几块钱不值什么,但一大早撞上这种事,心情会好上半分钟。
五步一步没少。变的只是第三格:早餐这一步,从"出门前开火"换成了"到楼下买"。买在通勤末尾,吃挪到了工位------位置其实也跟着挪了,这个后面再说。

楼下两个雪菜包,把第三格填上了
同一个位置,换一种做法。
今天聊的就是这件事在代码里的写法:策略模式(Strategy)------把"怎么做"抽成一个可以替换的零件,用的时候插上去,不合用就换一个。系列第四篇。
第三篇讲的是"填空":空留在父类,答案写在子类,编译期就定死了。今天讲的是"换件":零件不在子类里,在运行时插。两件事看起来都是"做法不同",灵活度差着一整级。
01 先把所有做法塞进一个 if
先看一个不太聪明的写法。要是今天这点变化直接写进 breakfast(),一开始大概是这样:
java
class WangMorning extends MorningRoutine {
protected void breakfast() {
if (noFood()) {
buyBaozi(); // 冰箱空了:楼下买两个
} else if (lateForWork()) {
buyOnigiri(); // 起晚了:便利店饭团
} else if (onDiet()) {
yogurtAndCereal(); // 想减肥:酸奶麦片
} else {
fryEggAndToast(); // 常态:煎蛋 + 吐司
}
}
}
四种吃法,四段分支
这段代码能跑,也不难懂。问题在位置:它把"变化"塞进了最该稳定的地方。
breakfast() 就是模板方法里那个空。第三篇说过,骨架之所以值钱,正因为它是稳定的。现在每多一种吃法,这个空就得再动一次。
更麻烦的是条件会互相影响。冰箱空了又起晚了,先看哪个?想减肥又没食材,又该走哪条?每加一个条件,原来排好的顺序就得重新想一遍。四种吃法,三种条件,能凑出八种组合------不是因为你爱吃,是因为组合在长。
判断和执行挤在同一处,代码就只能靠分支生长。
02 把做法摘出来,做成能插的零件
策略模式只做一件事:把"怎么做"从主流程里摘出来,单独定一个接口。
java
// 一个零件该长什么样:能填饱就行
interface Breakfast {
void eat();
}
class CookAtHome implements Breakfast {
public void eat() { System.out.println("煎蛋 + 吐司,开火十分钟"); }
}
class BuyBaozi implements Breakfast {
public void eat() { System.out.println("楼下买两个雪菜包"); }
}
class BuyOnigiri implements Breakfast {
public void eat() { System.out.println("便利店拿个饭团"); }
}
三个零件,同一个接口
主流程这边,原来那个空,换成一个插槽:
java
class MorningRoutine {
private Breakfast breakfast; // ← 插槽
public void setBreakfast(Breakfast b) { // 插什么,运行时说了算
this.breakfast = b;
}
public final void run() {
wakeUp();
wash();
breakfast.eat(); // ← 只认"能吃",不认"谁做的"
commute(); // 今天没变,照旧锁在这儿
clockIn();
}
private void wakeUp() { System.out.println("起床,按掉闹钟"); }
private void wash() { System.out.println("刷牙洗脸"); }
private void commute() { System.out.println("电动车到地铁站,再坐地铁"); }
private void clockIn() { System.out.println("打卡"); }
}
run() 的顺序没动,早餐那一行换成了 breakfast.eat()
注意 run() 的顺序一步没改。变的只有早餐那一行:从调 breakfast() 变成调 breakfast.eat()。同一个 breakfast,从"必须由子类实现的方法",变成了"随时能换的字段"。
顺带一句:这个字段在真实代码里得给个默认值,或者在调 run() 之前判个空------不然一跑就是 NPE。示例只留了主线。
今天冰箱空了,用的人只多这一行:
java
MorningRoutine wang = new MorningRoutine();
wang.setBreakfast(new BuyBaozi()); // 换零件,骨架不动
wang.run();
换个零件,调用处只改这一行
小李想吃饭团,用的还是这个 MorningRoutine,换一行的事。没有子类,没有重写,没有第二份骨架。
加一种吃法,只加一个类。

插槽只有一个,零件可以有很多个
这里有个对不上的地方,得说清楚。
现实里换早餐方案,你换的只是今天的一个念头,成本几乎是零。代码里换策略,前提是你先有一个大家都认的接口,还得有人负责造零件、挑零件。
策略模式省掉的是"改主流程"的成本,不是"写代码"的成本。每加一种吃法,你照样得写一个新类、补一条测试。
03 零件能换,可谁来选
这是策略模式最容易被忽略的一环。
如果那串判断还留在调用方,你只是把 if-else 从类里搬到了类外------河没通,只是换了个地方堵。
选择少、条件稳,就放在调用方,一行就够:
java
Breakfast b = hasFood ? new CookAtHome() : new BuyBaozi();
只有两个选项,别兴师动众
选择会变、选项还在长,就交给一个专门造零件的地方------第二篇的工厂,正好在这儿接班:
java
class BreakfastFactory {
static Breakfast forMorning(boolean hasFood, boolean late) {
if (!hasFood) return new BuyBaozi();
if (late) return new BuyOnigiri();
return new CookAtHome();
}
}
// 调用处只剩一句
wang.setBreakfast(BreakfastFactory.forMorning(hasFood, late));
判断归工厂,执行归插槽
这就是工厂和策略总黏在一起的原因:一个负责"今天用哪个",一个负责"用起来都一样"。
但别误会------工厂里的判断并没有消失,只是集中到了一处。以后加吃法,改的是这一个点,不是散在十个调用方里的十份判断。
同样一条流程,两种分工
04 到公司,优惠规则又改了
工位上的包子还没吃完,需求评审就来了。产品把文档投到屏幕上,满屏都是优惠规则。
上周是满 100 减 20,这周加会员价,下周上新人券。"就加几条规则,很快的"------这话你听过。
如果下单逻辑是这么写的:
java
// 每来一条规则,这段就要动一次
if (type == FULL_REDUCTION) {
total = total.subtract(new BigDecimal("20"));
} else if (type == MEMBER) {
total = total.multiply(new BigDecimal("0.9"));
} else if (type == NEW_USER) {
total = total.subtract(new BigDecimal("15"));
}
规则越多,这段越长,碰它的人越多
改的人不一定清楚别人那几个分支的前置条件。改错一次,就是线上资损。
换成策略:
java
interface Discount {
BigDecimal apply(BigDecimal amount);
}
class FullReduction implements Discount { // 满 100 减 20
public BigDecimal apply(BigDecimal amount) {
return amount.compareTo(new BigDecimal("100")) >= 0
? amount.subtract(new BigDecimal("20"))
: amount;
}
}
class MemberPrice implements Discount { // 会员 9 折
public BigDecimal apply(BigDecimal amount) {
return amount.multiply(new BigDecimal("0.9")); // 实际还要 setScale 定精度
}
}
一条规则一个类,各自对各自的账
结账那边只剩一行:
java
BigDecimal total = discount.apply(subtotal);
下周的新人券?写个新类,这行不动
这套东西在 JDK 里满地都是,只是名字不叫策略模式。
排序算法不动,比较规则是你从外面传进去的(Comparator);第三篇提过的 Comparable 则是把规则长在元素自己身上。同样在排序,一个从外面插,一个从里面长。
线程池构造时选的那个拒绝策略更直白------AbortPolicy 直接抛异常,CallerRunsPolicy 让提交任务的线程自己跑。插哪个,行为就是哪个,类名里就写着"Policy"。
05 什么时候,别给自己开个插槽
策略不是越多越好。它的门槛很明确:做法会变,而且变得不止一种。
如果你就一种做法,或者十年没变过------别造接口。为了"以后可能要扩展"先开一个插槽,是拿今天的复杂度去赌一个不一定会来的明天。
策略一多,选择逻辑必须有归处。要么收进一个工厂,要么收进一处配置。散在各个调用点,你灭掉的那堆 if-else 会以十份的形式长回来。
策略是"选一个",不是"叠一堆"。满 100 减 20、再打 9 折、再叠新人券------三种优惠同时生效,那是装饰器或者责任链的活。硬塞进策略,一笔订单只能吃到一种优惠;业务要的是三种一起上,需求评审那一关就过不去。
策略换的是做法,不是顺序。今天早饭换了买法,五步还是五步;吃的时间被挤到工位,那是生活顺手挪的位置,不是策略的功劳。代码里真要挪这一步,改的是 run() 的顺序,换零件解决不了。
最后一句可能最要紧:**策略模式消灭不了 if-else。**它只是把"判断在哪"和"执行在哪"分开。总得有人判断。
判断归判断,执行归执行。
06 包子吃完,活儿开始
回到工位。早上那两个雪菜包在工位吃完了,塑料袋揉成一团躺在垃圾桶里,需求文档打开,第一行写着:优惠规则调整。
他笑了一下,新建一个类。
这大概是策略模式最像生活的地方:它不负责让你每天都有早饭吃,只负责让你在冰箱空了的时候,不用把整条早上的流程推倒重来。
跟前面三篇对一句:单例管"造几个",工厂管"怎么造",模板方法管"事情怎么走",策略管"这一步用什么做法"。前三篇管的是"造几个""怎么造""按什么顺序走",这一篇管的是同一个位置换个做法。
带走一句话:策略 = 把"怎么做"抽成一个接口,让主流程只认得一个插槽。
骨架不动,零件可换。
参考资料
- 📕 《设计模式:可复用面向对象软件的基础》(GoF 原著)------策略模式的原始定义与"上下文---策略"结构
- 📗 **《Head First 设计模式》**第 1 章------用鸭子讲策略,本文"早餐换做法"类比的同乡
- 📘 Java 官方文档 · Comparator :docs.oracle.com/en/java/jav...
- 📙 Java 官方文档 · ThreadPoolExecutor 拒绝策略 :docs.oracle.com/en/java/jav...