策略模式完全指南:从 if/else 选算法到 Spring 注入与 AB 测试

"我有一组算法,根据条件选择用哪个" —— 这是策略模式的天然场景。排序算法、压缩算法、定价规则、推荐策略、支付方式 —— 处处都是它。策略模式可能是 23 个模式里最"代码量小、收益却大"的一个。这篇文章把策略模式从动机讲到 Lambda 实现、Spring 注入、AB 测试,讲清楚它和状态、模板方法的边界。

问题:if/else 选算法

class Sorter {
    public void sort(int[] arr, String algorithm) {
        if (algorithm.equals("bubble")) {
            // 100 行冒泡排序
        } else if (algorithm.equals("quick")) {
            // 60 行快排
        } else if (algorithm.equals("merge")) {
            // 80 行归并
        } else {
            throw new IllegalArgumentException();
        }
    }
}

问题:

  • 一个方法 200+ 行,每加一种算法都要改这个方法。
  • 测试时只想测快排,但你的测试要面对整个 if 链。
  • 不同算法的参数不同 —— 快排可能要 pivot 策略,归并要 buffer size,if 链根本不好传。

策略模式说:把每种算法封装成一个独立类,用相同接口暴露

策略模式的标准结构

// 1. 策略接口
interface SortStrategy {
    void sort(int[] arr);
}

// 2. 具体策略
class BubbleSort implements SortStrategy {
    public void sort(int[] arr) { /* 冒泡实现 */ }
}
class QuickSort implements SortStrategy {
    public void sort(int[] arr) { /* 快排实现 */ }
}
class MergeSort implements SortStrategy {
    public void sort(int[] arr) { /* 归并实现 */ }
}

// 3. Context:持有策略,转发调用
class Sorter {
    private SortStrategy strategy;
    public Sorter(SortStrategy s) { this.strategy = s; }
    public void setStrategy(SortStrategy s) { this.strategy = s; }
    public void sort(int[] arr) { strategy.sort(arr); }
}

// 客户端
Sorter sorter = new Sorter(new QuickSort());
sorter.sort(new int[]{3, 1, 4, 1, 5, 9, 2, 6});

// 换策略
sorter.setStrategy(new MergeSort());
sorter.sort(arr2);

每个算法独立,加新算法 = 加新类。Sorter 客户端的代码不用变。

实战 1:支付策略

interface PaymentStrategy {
    PaymentResult pay(Order order);
}

class AlipayStrategy implements PaymentStrategy {
    private final AlipayClient client;
    public PaymentResult pay(Order o) { /* 调用支付宝 */ }
}
class WechatStrategy implements PaymentStrategy {
    public PaymentResult pay(Order o) { /* 调用微信 */ }
}
class CreditCardStrategy implements PaymentStrategy {
    public PaymentResult pay(Order o) { /* 调用 Stripe */ }
}

class CheckoutService {
    public void checkout(Order order, PaymentStrategy strategy) {
        validate(order);
        PaymentResult r = strategy.pay(order);
        if (r.isSuccess()) markPaid(order);
        else handleFail(order, r);
    }
}

// 客户端按用户选择决定策略
PaymentStrategy strategy = switch (user.preferredPayment()) {
    case "alipay" -> new AlipayStrategy();
    case "wechat" -> new WechatStrategy();
    case "card"   -> new CreditCardStrategy();
};
checkout.checkout(order, strategy);

这里同时演示了"Spring 注入"的妙处:

// Spring 自动收集所有 PaymentStrategy 实现到 Map
@Service
public class CheckoutService {
    private final Map<String, PaymentStrategy> strategies;

    public CheckoutService(List<PaymentStrategy> list) {
        this.strategies = list.stream()
            .collect(Collectors.toMap(PaymentStrategy::name, Function.identity()));
    }

    public void checkout(Order o, String channel) {
        PaymentStrategy s = strategies.get(channel);
        if (s == null) throw new IllegalArgumentException("unknown channel");
        s.pay(o);
    }
}

// 新增支付商:加一个 @Component 的策略类即可,旧代码零改动
@Component
public class PaypalStrategy implements PaymentStrategy {
    public String name() { return "paypal"; }
    public PaymentResult pay(Order o) { ... }
}

这是 Spring 项目里策略模式的典型用法 —— 用接口 + Map + 名称查找,实现"按业务参数选策略"。框架自动注入,加新策略零侵入。

实战 2:定价策略

interface PricingStrategy {
    BigDecimal calculate(Cart cart);
}

class RegularPricing implements PricingStrategy {
    public BigDecimal calculate(Cart c) { return c.subtotal(); }
}

class VipPricing implements PricingStrategy {
    public BigDecimal calculate(Cart c) {
        return c.subtotal().multiply(new BigDecimal("0.9"));   // 9 折
    }
}

class FlashSalePricing implements PricingStrategy {
    public BigDecimal calculate(Cart c) {
        return c.items().stream()
            .map(i -> i.isFlashSale() ? i.flashPrice() : i.regularPrice())
            .reduce(BigDecimal.ZERO, BigDecimal::add);
    }
}

class CouponPricing implements PricingStrategy {
    private final Coupon coupon;
    public CouponPricing(Coupon c) { this.coupon = c; }
    public BigDecimal calculate(Cart c) {
        return coupon.apply(c.subtotal());
    }
}

// 多策略组合(责任链)
class StackedPricing implements PricingStrategy {
    private final List<PricingStrategy> strategies;
    public BigDecimal calculate(Cart c) {
        Cart proxy = c;
        for (PricingStrategy s : strategies) {
            // 让上一个策略的结果作为下一个的输入
        }
        return computed;
    }
}

定价场景里策略多到爆 —— VIP 折扣、闪购、优惠券、满减、积分抵扣...每加一个促销活动就是加一个策略。如果都堆在一个 if/else 里,维护就崩溃了。

实战 3:压缩策略

interface CompressionStrategy {
    byte[] compress(byte[] data);
    byte[] decompress(byte[] data);
}

class GzipStrategy implements CompressionStrategy {
    public byte[] compress(byte[] data) { /* GZIP */ }
    public byte[] decompress(byte[] data) { /* GZIP */ }
}
class ZstdStrategy implements CompressionStrategy { ... }
class LZ4Strategy implements CompressionStrategy { ... }

// 上传服务根据数据特征选策略
public class Uploader {
    public void upload(byte[] data) {
        CompressionStrategy s = data.length > 10_000_000
            ? new ZstdStrategy()    // 大文件用压缩比好的
            : new LZ4Strategy();    // 小文件用速度快的
        byte[] compressed = s.compress(data);
        send(compressed);
    }
}

用 Lambda 简化策略

当策略接口只有一个方法,Lambda 直接代替策略类:

// 普通版本
List<User> users = ...;
users.sort(new Comparator<User>() {
    public int compare(User a, User b) { return a.getAge() - b.getAge(); }
});

// Lambda 版本
users.sort((a, b) -> a.getAge() - b.getAge());

// 或更简洁
users.sort(Comparator.comparingInt(User::getAge));

JDK 集合 API 里到处是策略模式 —— Comparator 是排序策略,Predicate 是过滤策略,Function 是变换策略。Lambda 让"一次性策略"几乎零成本。

各语言里的策略

Python:函数当策略

def by_age(u): return u.age
def by_name(u): return u.name

users.sort(key=by_age)        # 按年龄
users.sort(key=by_name)       # 按名字

# 或匿名 lambda
users.sort(key=lambda u: u.age)

JavaScript:函数即策略

const strategies = {
    bubble: (arr) => { /* ... */ },
    quick:  (arr) => { /* ... */ },
    merge:  (arr) => { /* ... */ },
};

const sort = strategies[config.algorithm];
sort(myArray);

Go:函数类型 + 接口都可

type PricingStrategy func(cart Cart) float64

func regular(c Cart) float64 { return c.Subtotal() }
func vip(c Cart) float64     { return c.Subtotal() * 0.9 }

var pricing PricingStrategy = vip
total := pricing(cart)

// 或用接口形式,适合复杂策略
type Pricer interface { Calculate(Cart) float64 }

策略 + 状态 + 模板方法

这三个常被混淆,分清:

  • 策略:Context 持有一个 Strategy,客户端外部决定选哪个,运行时可换。重点"算法可替换"。
  • 状态:对象的内部状态决定它的行为,状态自己跳转。重点"对象状态机"。
  • 模板方法:用继承,父类定流程,子类填具体步骤。重点"流程固定步骤可变"。

策略和状态结构上一样,意图差别在"谁决定切换"。策略和模板方法的差别在"用组合还是用继承"。

策略选择的自动化

1. 根据数据特征自动选

Java 的 Arrays.sort 内部就在做这件事 —— 小数组用插入排序,大数组用双轴快排或归并,看数据量自动切换。

2. AB 测试

// 50% 用户用策略 A,50% 用策略 B
PaymentStrategy strategy = AbTest.assign(user.getId(), "payment_v2")
    ? new NewPaymentStrategy()
    : new OldPaymentStrategy();

3. 基于运行时指标自适应选

压缩策略可以根据"上一批数据的压缩率"动态切 —— 重复度高的内容上 gzip,熵高的上 zstd。

策略的代价

1. 类数量增加

10 个策略 = 10 个类,代码体量上升。对一次性的简单策略,Lambda 比类轻得多。

2. 客户端必须知道有哪些策略

客户端要在"创建 Context 时决定用哪个策略" —— 这意味着客户端要知道策略种类。补救:用工厂、配置文件、注解、注入容器把策略选择本身也封装起来。

3. 策略间状态共享困难

策略之间通常无状态。如果两个策略需要共享一些状态(例如"上一次的中间结果"),要么放进 Context,要么策略之间通过 Context 通信。

策略模式的常见坑

坑 1:策略接口设计过死。 接口太"瘦"导致策略只能拿到固定参数,没法实现需要更多上下文的高级策略。改进:接口接受一个上下文对象,而不是一堆参数,这样未来加字段不破坏接口。

坑 2:策略接口设计过胖。 接口有 10 个方法,每个策略实现都要写 10 个,大多数都是空。把接口拆细,或用默认方法提供默认行为。

坑 3:策略选择硬编码。 if (channel.equals("alipay")) new AlipayStrategy() —— 加新策略要改这个 if。改用 Map / 工厂 / Spring 注入。

坑 4:策略里塞业务逻辑。 策略应该只是"算法本身",和业务编排无关。"扣库存 + 通知 + 写日志 + 算价格"塞到 PricingStrategy 里是错的 —— 这些应该在 Context 里编排,策略只负责算价格。

识别策略场景

  1. 一组相关但行为不同的算法
  2. 需要在运行时切换
  3. 客户端代码里有 if/switch 选算法。
  4. 不同算法的参数不同,塞进一个方法签名很别扭。

实战 4:推荐算法的策略热切换

推荐系统经常需要"同时跑多种召回策略,在线对比效果"。把每种召回做成一个策略,中心服务统一调度:

interface RecallStrategy {
    String name();
    List<Item> recall(User user, int limit);
    double weight();
}

@Component public class HotItemsRecall implements RecallStrategy { /* 热度召回 */ }
@Component public class CFRecall implements RecallStrategy { /* 协同过滤 */ }
@Component public class EmbeddingRecall implements RecallStrategy { /* 向量召回 */ }
@Component public class GraphRecall implements RecallStrategy { /* 图召回 */ }

@Service
public class RecommendService {
    private final List<RecallStrategy> strategies;

    public List<Item> recommend(User user) {
        // 并行跑所有策略
        Map<Item, Double> scoreMap = new HashMap<>();
        strategies.parallelStream().forEach(s -> {
            for (Item it : s.recall(user, 100)) {
                scoreMap.merge(it, s.weight(), Double::sum);
            }
        });

        return scoreMap.entrySet().stream()
            .sorted(Map.Entry.<Item, Double>comparingByValue().reversed())
            .limit(50)
            .map(Map.Entry::getKey)
            .collect(toList());
    }
}

这种"策略池 + 加权融合"是推荐工程的标配。要上新算法,加一个 @Component 类即可,旧代码不动。这就是策略模式在数据驱动业务里的高级用法。

写在最后

策略模式可能是设计模式里"最朴素但最高 ROI"的一个。它做的事极简单 —— 把"选算法"从客户端代码里抽出来变成接口 —— 但带来的灵活度极高:加算法零侵入、客户端代码干净、测试容易隔离、运行时可切换。这种"把变化点对象化"的思想,是面向对象设计的精髓。

给一个工程信号:当你写 if/switch 选算法时,停一秒,考虑用策略模式。在 Java 里 Lambda 让策略几乎零成本,在 Python/JS 里函数即策略,门槛比任何模式都低。一旦养成习惯,你的代码会从"过程式判断分支"自然演化成"声明式策略组合",可读性、可扩展性、可测试性同步提升。这就是为什么策略模式被认为是入门面向对象设计最值得学的模式之一。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

状态模式完全指南:从 if 地狱到订单状态机的优雅演化

2026-5-15 15:35:28

技术教程

模板方法模式完全指南:从 Spring JdbcTemplate 到 Servlet 与框架设计

2026-5-15 15:35:29

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索