设计模式之策略模式

设计模式之策略模式

阅读指引:本文较长,可按需跳读。

  • 想快速理解 → 读「概念」「举例理解」「工作流程」;
  • 想看代码 → 读「代码示例」(含 UML、C++、Java、函数式轻量变体);
  • 想区分相似模式 → 读「策略模式与状态模式的对比」;
  • 想判断该不该用 → 读「优缺点」「一个反面例子」「应用场景」;
  • 准备落地 → 读「注意事项」与「策略模式不负责什么」。

概念

定义

策略模式(Strategy Pattern) 是一种行为型设计模式。它的核心思想是:

把一组可互换的算法分别封装起来,让它们可以互相替换,并且算法的变化独立于使用算法的客户端。

换句话说:当同一件事有多种做法,并且需要在运行时选择其中一种做法时,可以把每种做法封装成一个"策略",由上下文对象持有并调用。

结构

策略模式通常有三个角色:

  1. Strategy 策略接口
    • 定义所有策略必须实现的方法。
    • 例如:calculatePrice()、pay()、sort()。
  2. ConcreteStrategy 具体策略
    • 实现具体算法。
    • 例如:微信支付、支付宝支付、银行卡支付。
  3. Context 上下文
    • 持有一个策略接口引用。
    • 把客户端请求委托给当前策略执行。
    • 客户端可以在运行时切换策略。

举例理解

假设你现在需要去机场,可以选择:打车、坐地铁、自驾、坐机场大巴。

这些方式都能让你从家到机场。但怎么去中间的过程不相同:花费、时间、路线、舒适度等等都不一样。

面对多选择,你可能会下意识想到 if/else 或 switch:

如果选打车:
    计算打车路线、费用、时间
否则如果选地铁:
    计算地铁换乘、票价、时间
否则如果选自驾:
    计算自驾路线、停车费、时间
否则如果选机场大巴:
    计算大巴站点、票价、时间

相当于把所有的出行算法都放在了一个方法内。如果新增一种"拼车 + 地铁",就要新增一个判断分支,每种方式的计算逻辑也混在一起,越来越乱。

策略模式的解决办法: 把每种出行方式都封装成一个独立的"策略"。

  1. 策略接口:出行方式

    你可以理解为一个统一规则:任何出行方式都必须回答:从家到机场要多久、花多少钱、怎么走。

    出行方式 {
        规划路线(起点, 终点)
    }
    
  2. 具体策略:具体的出行方式

    • 打车策略:规划打车路线,算车费和时间
    • 地铁策略:规划换乘路线,算票价和时间
    • 自驾策略:规划自驾路线,算油费、停车费和时间
    • 机场大巴策略:规划大巴站点,算票价和时间

    它们都实现同一个"出行方式"接口。

  3. 上下文:出行规划器

    出行规划器 {
        当前出行方式;
    
        设置出行方式(方式) {
            当前出行方式 = 方式;
        }
    
        去机场() {
            当前出行方式.规划路线(家, 机场);
        }
    }
    

    出行规划器不关心打车具体怎么算、地铁怎么换乘,它只负责调用当前策略的"规划路线"。

  4. 客户端:你

    此时你只需要根据你的实际情况做出选择:时间充足想省钱选地铁;快迟到了切换成打车;行李很多选自驾;想便宜又不想挤地铁选机场大巴。

    如果早上你本来选择地铁:

    规划器.设置出行方式(new 地铁策略());
    规划器.去机场();
    

    结果发现时间来不及,于是运行时切换:

    规划器.设置出行方式(new 打车策略());
    规划器.去机场();
    

    规划器没有变,只是内部换了一种"去机场算法"。

策略模式就像你手机里的导航 App:目的地都是机场,但你可以选"最快路线"“少收费路线”“避开拥堵路线”。导航 App 不关心路线怎么算,它只调用当前选中的路线策略;你随时可以切换策略。


工作流程

策略模式的工作流程很简单,可以理解成:

  1. 先定一个统一规则:比如"去机场"这件事,不管怎么去,都要能完成"去机场"。

  2. 把每种做法分别做出来:打车是一种做法,地铁是一种做法,自驾是一种做法。每种做法各自管自己,互不混在一起。

  3. 找一个执行者,手里拿着"当前做法":这个执行者就像"出行规划器",它不关心具体怎么走,只负责按当前选中的方式去机场。

  4. 你选一种做法,交给执行者:比如你今天选地铁,就把"地铁做法"交给规划器。

  5. 执行者调用当前做法:你说"去机场",规划器就按手里的"地铁做法"执行。

  6. 想换做法时,直接替换:发现来不及了,你把规划器手里的"地铁做法"换成"打车做法"。规划器本身不用改,再执行时就按打车走。

    当然,策略模式能换的是"下一次调用用哪个策略",不是把正在执行到一半的算法中途打断换掉。 换策略发生在两次执行之间,不是执行到一半自动切换(你不能坐地铁坐到一半然后就能换策略到打车,这相当于另一种策略,或者需要在业务逻辑中分段处理)。

一句话概括:

定统一规则 → 做多种做法 → 执行者拿着当前做法 → 你选一个交给它 → 它按这个做法办事 → 想换就换。


代码示例

UML

下面用"家到机场"的例子,画出策略模式涉及的所有类,并标注关系。

implements

implements

implements

implements

holds / delegates

creates / uses

creates

creates

creates

creates

«interface»

TravelStrategy

+goToAirport() : void

TaxiStrategy

+goToAirport() : void

SubwayStrategy

+goToAirport() : void

DriveStrategy

+goToAirport() : void

AirportBusStrategy

+goToAirport() : void

«Context»

TravelPlanner

-strategy : TravelStrategy

+TravelPlanner(strategy : TravelStrategy)

+setStrategy(strategy : TravelStrategy) : void

+goToAirport() : void

Client

+main(args : String[]) : void

类/接口角色在"家到机场"里的含义
TravelStrategy策略接口 Strategy统一规则:任何出行方式都必须能完成"去机场"
TaxiStrategy具体策略 ConcreteStrategy打车去机场的算法
SubwayStrategy具体策略 ConcreteStrategy坐地铁去机场的算法
DriveStrategy具体策略 ConcreteStrategy自驾去机场的算法
AirportBusStrategy具体策略 ConcreteStrategy坐机场大巴去机场的算法
TravelPlanner上下文 Context出行规划器,持有当前策略并委托执行
Client客户端 Client你,负责选策略、创建规划器、触发执行、需要时切换策略

图中最重要的三条线:

  • 具体策略 → 策略接口:实现关系,负责"有多种做法";
  • 上下文 → 策略接口:聚合关系,负责"拿着当前做法并委托执行";
  • Client → 具体策略:依赖关系。这里 Client 依赖具体策略是合理的,因为"选哪个策略"正是 Client 的职责。如果项目里选择逻辑复杂,可以用工厂、注册表或 DI 容器把这条依赖从 Client 转移到基础设施层,让 Client 只依赖工厂接口。

而上下文与具体策略之间没有直接依赖线,这正是策略模式解耦的核心。

C++ 实现

// ============================================================
// 策略模式(Strategy Pattern)完整示例
// 场景:从家去机场,有多种出行方式,可在运行时切换。
//
// 对应 UML:
//   TravelStrategy          —— 策略接口(抽象基类)
//   TaxiStrategy            —— 具体策略
//   SubwayStrategy          —— 具体策略
//   DriveStrategy           —— 具体策略
//   AirportBusStrategy      —— 具体策略
//   TravelPlanner           —— 上下文(Context),持有策略并委托
//   Client(main)            —— 客户端,创建策略、注入、切换
// ============================================================

#include <iostream>
#include <memory>
#include <string>
#include <utility>
#include <vector>

// ------------------------------------------------------------
// 1. 策略接口 Strategy
//
// 说明:
//   这里把 goToAirport() 声明为 const,表达"示例中的策略是
//   无状态的,算法不修改策略对象本身"。
//   如果某个策略需要维护自身状态(例如记录调用次数、缓存),
//   应去掉 const,并重新评估其能否被多上下文共享。
// ------------------------------------------------------------
class TravelStrategy {
public:
    // 虚析构函数:保证通过基类指针删除派生类对象时,
    // 派生类的析构函数能被正确调用,避免资源泄漏。
    virtual ~TravelStrategy() = default;

    // 纯虚函数:统一契约,每种出行方式都必须实现"去机场"。
    virtual void goToAirport() const = 0;
};

// ------------------------------------------------------------
// 2. 具体策略 ConcreteStrategy:打车
// ------------------------------------------------------------
class TaxiStrategy : public TravelStrategy {
public:
    void goToAirport() const override {
        std::cout << "【打车】叫车、走机场高速,预计 40 分钟,花费约 80 元。"
                  << std::endl;
    }
};

// ------------------------------------------------------------
// 3. 具体策略 ConcreteStrategy:地铁
// ------------------------------------------------------------
class SubwayStrategy : public TravelStrategy {
public:
    void goToAirport() const override {
        std::cout << "【地铁】步行到站、换乘 2 次,预计 60 分钟,花费约 8 元。"
                  << std::endl;
    }
};

// ------------------------------------------------------------
// 4. 具体策略 ConcreteStrategy:自驾
// ------------------------------------------------------------
class DriveStrategy : public TravelStrategy {
public:
    void goToAirport() const override {
        std::cout << "【自驾】走机场高速、停车 3 天,预计 35 分钟,油费+停车约 120 元。"
                  << std::endl;
    }
};

// ------------------------------------------------------------
// 5. 具体策略 ConcreteStrategy:机场大巴
// ------------------------------------------------------------
class AirportBusStrategy : public TravelStrategy {
public:
    void goToAirport() const override {
        std::cout << "【机场大巴】步行到站点、等车 15 分钟,预计 70 分钟,花费约 25 元。"
                  << std::endl;
    }
};

// ------------------------------------------------------------
// 6. 上下文 Context:出行规划器
//
// 设计取舍:
//   - 这里用 shared_ptr 是为了演示场景 4 的"策略共享"。
//     若上下文独占策略,unique_ptr 更轻、语义更清晰,是更常见的选择;
//   - 用 shared_ptr<const TravelStrategy> 也可以,能在类型层面表达
//     "上下文不修改策略",但本示例保留非 const 版本以便与
//     "策略可能有状态"的扩展场景兼容;
//   - 本示例保留了"未选择策略"的容错分支,属于"容忍式"设计;
//     Java 版则选择 fail-fast(构造时拒绝 null),两种都是合理选择,
//     详见 Java 版注释。
// ------------------------------------------------------------
class TravelPlanner {
public:
    explicit TravelPlanner(std::shared_ptr<TravelStrategy> strategy)
        : strategy_(std::move(strategy)) {}

    // 运行时切换策略,只影响下一次调用。
    void setStrategy(std::shared_ptr<TravelStrategy> strategy) {
        strategy_ = std::move(strategy);
    }

    // 对外统一入口:委托给当前策略。
    void goToAirport() const {
        if (!strategy_) {
            std::cout << "【提示】还没有选择出行方式,无法去机场。" << std::endl;
            return;
        }
        // 动态分派:实际执行哪个 goToAirport(),取决于 strategy_ 指向的真实对象。
        strategy_->goToAirport();
    }

private:
    // 上下文只依赖策略接口,不依赖任何具体策略。
    std::shared_ptr<TravelStrategy> strategy_;
};

// ------------------------------------------------------------
// 7. 客户端 Client
// ------------------------------------------------------------
int main() {
    std::cout << "========== 场景 1:默认选地铁 ==========" << std::endl;

    TravelPlanner planner(std::make_shared<SubwayStrategy>());
    planner.goToAirport();

    std::cout << "\n========== 场景 2:发现地铁来不及,运行时切换打车 ==========" << std::endl;
    // 注意:上一次策略已执行完,不是"执行到一半中断再切换"。
    planner.setStrategy(std::make_shared<TaxiStrategy>());
    planner.goToAirport();

    std::cout << "\n========== 场景 3:用有序容器按名称选择策略 ==========" << std::endl;

    // 选择说明:
    //   这里用 std::vector 保持"注册顺序",是为了演示输出稳定。
    //   实际注册表用 std::unordered_map 也没问题,只是遍历顺序不稳定,
    //   会对示例输出造成干扰。如果确实需要按 key 有序,
    //   用 std::map;如果需要 O(1) 查找且不关心顺序,用 std::unordered_map。
    std::vector<std::pair<std::string, std::shared_ptr<TravelStrategy>>> registry{
        {"taxi",       std::make_shared<TaxiStrategy>()},
        {"subway",     std::make_shared<SubwayStrategy>()},
        {"drive",      std::make_shared<DriveStrategy>()},
        {"airportBus", std::make_shared<AirportBusStrategy>()},
    };

    for (const auto& [name, strategy] : registry) {
        std::cout << "选择出行方式 [" << name << "]:" << std::endl;
        planner.setStrategy(strategy);
        planner.goToAirport();
    }

    std::cout << "\n========== 场景 4:策略共享 ==========" << std::endl;

    // 注意:"无状态"指策略对象自身不保存可变数据;
    // 如果策略方法内部依赖了共享的可变外部对象(如静态缓存、共享配置),
    // 共享仍需单独评估线程安全,不能仅凭"类里没有成员变量"就判定安全。
    auto sharedTaxi = std::make_shared<TaxiStrategy>();

    TravelPlanner plannerA(sharedTaxi);
    TravelPlanner plannerB(sharedTaxi);

    std::cout << "规划器 A:";
    plannerA.goToAirport();

    std::cout << "规划器 B:";
    plannerB.goToAirport();

    return 0;
}

Java 实现

// ============================================================
// 策略模式(Strategy Pattern)完整示例(Java 版)
// ============================================================

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;

// ------------------------------------------------------------
// 1. 策略接口 Strategy
// ------------------------------------------------------------
interface TravelStrategy {
    // 统一契约:每种出行方式都必须实现"去机场"。
    void goToAirport();
}

// ------------------------------------------------------------
// 2. 具体策略:打车
// ------------------------------------------------------------
class TaxiStrategy implements TravelStrategy {
    @Override
    public void goToAirport() {
        System.out.println("【打车】叫车、走机场高速,预计 40 分钟,花费约 80 元。");
    }
}

// ------------------------------------------------------------
// 3. 具体策略:地铁
// ------------------------------------------------------------
class SubwayStrategy implements TravelStrategy {
    @Override
    public void goToAirport() {
        System.out.println("【地铁】步行到站、换乘 2 次,预计 60 分钟,花费约 8 元。");
    }
}

// ------------------------------------------------------------
// 4. 具体策略:自驾
// ------------------------------------------------------------
class DriveStrategy implements TravelStrategy {
    @Override
    public void goToAirport() {
        System.out.println("【自驾】走机场高速、停车 3 天,预计 35 分钟,油费+停车约 120 元。");
    }
}

// ------------------------------------------------------------
// 5. 具体策略:机场大巴
// ------------------------------------------------------------
class AirportBusStrategy implements TravelStrategy {
    @Override
    public void goToAirport() {
        System.out.println("【机场大巴】步行到站点、等车 15 分钟,预计 70 分钟,花费约 25 元。");
    }
}

// ------------------------------------------------------------
// 6. 上下文 Context:出行规划器
//
// 设计取舍(与 C++ 版对照):
//   - Java 版选择 fail-fast:构造时和 setStrategy 时都拒绝 null,
//     因为在本示例中"没有策略的规划器"是无效对象,越早失败越容易定位;
//   - C++ 版选择"容忍式":允许持有空策略,goToAirport 时打提示,
//     演示另一种常见设计——上下文可以处于"未选择策略"的中间态。
//   两种都合理,取决于业务是否允许"规划器存在但尚未选策略"。
// ------------------------------------------------------------
class TravelPlanner {
    private TravelStrategy strategy;

    public TravelPlanner(TravelStrategy strategy) {
        this.strategy = Objects.requireNonNull(strategy, "strategy 不能为 null");
    }

    public void setStrategy(TravelStrategy strategy) {
        this.strategy = Objects.requireNonNull(strategy, "strategy 不能为 null");
    }

    // 对外统一入口:委托给当前策略执行。
    public void goToAirport() {
        strategy.goToAirport();
    }
}

// ------------------------------------------------------------
// 7. 客户端 Client
// ------------------------------------------------------------
public class StrategyDemo {
    public static void main(String[] args) {
        System.out.println("========== 场景 1:默认选地铁 ==========");
        TravelPlanner planner = new TravelPlanner(new SubwayStrategy());
        planner.goToAirport();

        System.out.println("\n========== 场景 2:运行时切换打车 ==========");
        planner.setStrategy(new TaxiStrategy());
        planner.goToAirport();

        System.out.println("\n========== 场景 3:用 Map 按字符串选择策略 ==========");

        // 用 LinkedHashMap 保证遍历顺序与注册顺序一致,方便输出演示。
        Map<String, TravelStrategy> registry = new LinkedHashMap<>();
        registry.put("taxi", new TaxiStrategy());
        registry.put("subway", new SubwayStrategy());
        registry.put("drive", new DriveStrategy());
        registry.put("airportBus", new AirportBusStrategy());
        // 兜底策略:应对"用户输入拼错、配置缺失、版本不一致"的情况。
        registry.put("default", new SubwayStrategy());

        // 模拟用户输入,其中 "walk" 是未注册的 key,用来演示兜底。
        String[] userInputs = {"taxi", "subway", "drive", "airportBus", "walk"};
        for (String input : userInputs) {
            System.out.println("选择出行方式 [" + input + "]:");
            // 关键:不能假设 registry 一定命中,必须有兜底。
            TravelStrategy strategy = registry.getOrDefault(input, registry.get("default"));
            planner.setStrategy(strategy);
            planner.goToAirport();
        }

        System.out.println("\n========== 场景 4:策略共享 ==========");
        // "无状态"指策略对象自身不保存可变数据;
        // 若策略内部依赖共享可变对象,仍需评估线程安全。
        TravelStrategy sharedTaxi = new TaxiStrategy();
        TravelPlanner plannerA = new TravelPlanner(sharedTaxi);
        TravelPlanner plannerB = new TravelPlanner(sharedTaxi);

        System.out.print("规划器 A:");
        plannerA.goToAirport();
        System.out.print("规划器 B:");
        plannerB.goToAirport();
    }
}

函数式轻量变体

当策略接口只有一个无参(或少参)方法、且策略无状态、逻辑简单、不需要被 DI 管理时,不必新建接口和多个类——用 JDK / C++ 自带的函数式接口即可,结构更轻。

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Supplier;

// 上下文:直接持有 Runnable / Supplier 作为"策略"
class FnTravelPlanner {
    private Runnable strategy;
    FnTravelPlanner(Runnable s) { this.strategy = s; }
    void setStrategy(Runnable s) { this.strategy = s; }
    void goToAirport() { strategy.run(); }
}

public class FnStrategyDemo {
    public static void main(String[] args) {
        // 用 Map 注册"函数式策略",key 是名字,value 是一段行为
        Map<String, Runnable> registry = new LinkedHashMap<>();
        registry.put("taxi",   () -> System.out.println("【打车】叫车、走机场高速,预计 40 分钟,约 80 元。"));
        registry.put("subway", () -> System.out.println("【地铁】步行到站、换乘 2 次,预计 60 分钟,约 8 元。"));
        registry.put("drive",  () -> System.out.println("【自驾】走机场高速、停车 3 天,预计 35 分钟,油费+停车约 120 元。"));

        FnTravelPlanner planner = new FnTravelPlanner(registry.get("subway"));
        for (Map.Entry<String, Runnable> e : registry.entrySet()) {
            System.out.println("选择出行方式 [" + e.getKey() + "]:");
            planner.setStrategy(e.getValue());
            planner.goToAirport();
        }
    }
}

取舍对照:

维度传统策略类函数式策略
适用方法数多方法接口单方法接口
是否需要 DI 管理常需要(Spring Bean)通常不需要
是否需要状态可有状态无状态为主
独立测试类可独立测试lambda 内联,测试需包装
可读性类名即文档,一目了然短逻辑清晰,长逻辑易失控
类爆炸风险高低

经验小谈:逻辑超过 5~10 行、需要复用、需要被 Spring 管理、需要独立测试,就退回类实现;否则函数式接口更轻。


策略模式与状态模式的对比

策略模式和状态模式在代码结构上几乎一模一样:都是"上下文持有抽象接口,具体实现类实现接口,上下文委托调用"。所以很多人会混淆。但它们的意图、切换控制方、对象间关系完全不同。

一、主要区别

  • 策略模式:同一件事有多种算法,由外部(客户端)选择用哪一种。
  • 状态模式:对象的行为随内部状态改变,状态之间可以自动转移。

用"去机场"类比:

  • 策略模式:你主动选打车还是地铁,出行方式之间互不知道彼此。
  • 状态模式:你走到"地铁站内"“检票口”"站台"不同位置,行为自动变化,且位置之间会自然流转。

二、核心差异

对比维度策略模式状态模式
核心意图封装可互换的算法让行为随内部状态改变
切换由谁控制通常由客户端主动选择通常由上下文或状态自身触发转移
具体实现之间是否知道彼此通常互不知道,彼此独立通常知道彼此,能触发转移到另一个状态
是否存在自动转移通常没有通常有
上下文是否感知状态变化不感知,只持有当前策略感知,状态转移是它的核心逻辑
关注点"怎么做"可替换"现在是什么状态"决定行为
典型变化新增一种算法新增一种状态及转移规则
客户端职责负责选策略、注入、切换通常只发起动作,不关心内部状态转移

最关键的一条:

策略模式是"我选一种做法";状态模式是"对象处于某种状态,行为随之改变,状态还会自己流转"。

三、用"去机场"分别实现,对比更直观

策略模式版本(客户端主动换):

TravelPlanner planner = new TravelPlanner(new SubwayStrategy());
planner.goToAirport();                       // 坐地铁
planner.setStrategy(new TaxiStrategy());     // 客户端主动换
planner.goToAirport();                       // 打车
  • SubwayStrategy 不知道 TaxiStrategy 存在;
  • 没有"自动从地铁转到打车"的逻辑;
  • 换的是下一次调用的算法。

状态模式版本(状态自身转移):

TravelContext context = new TravelContext(new AtHomeState());
context.move();   // 在家 → 出门
context.move();   // 出门 → 地铁站
context.move();   // 地铁站 → 站台
context.move();   // 站台 → 机场

class AtHomeState implements TravelState {
    public void move(TravelContext ctx) {
        System.out.println("出门,前往地铁站");
        ctx.setState(new ToSubwayState());   // 状态自己触发转移
    }
}
  • 状态转移由状态自身或上下文触发;
  • 每个状态知道下一个状态是谁;
  • 客户端只调用 move(),不关心内部怎么转。

四、判断用哪个

问题选策略选状态
切换动作由谁发起?客户端主动选对象内部状态变化驱动
实现类之间要知道彼此吗?互不知道需要知道下一个状态
变化的核心是什么?算法会增、会换,"做同一件事"不变对象有明确状态阶段,行为随阶段变

五、常见误区

  • 误区 1:状态模式就是"策略模式的自动版"。 不准确。状态模式的重点不是"自动",而是行为由内部状态决定,状态之间构成转移关系。有些状态转移也由外部事件触发,但转移逻辑仍在状态/上下文内部。
  • 误区 2:用了 setStrategy() 就是状态模式。 不是。只要有"客户端主动替换实现"的动作,本质仍是策略模式。
  • 误区 3:两者不能共存。 可以。订单有状态流转(状态模式),每个状态下又有多种支付方式可选(策略模式)。

六、典型使用场景对照

策略模式状态模式
支付方式:微信、支付宝、银行卡订单状态:待支付、已支付、已发货、已完成
折扣计算:无折扣、VIP、SVIP电梯状态:停止、上行、下行、开门
排序算法:快排、归并、堆排交通灯:红、绿、黄
压缩算法:zip、rar、7z播放器:播放、暂停、停止
出行方式:打车、地铁、自驾行程阶段:在家、路上、安检、登机

一句话区分:策略模式是"我挑一个工具用";状态模式是"我到哪一步,就做哪一步的事,而且会自己走到下一步"。


策略模式的优缺点

优点

1. 符合开闭原则,扩展性好。 新增一种算法,通常只需新增一个实现策略接口的类,再在注册表或工厂里登记一下,已有的策略类和上下文都不需要修改。

2. 消除大量条件分支,代码更清晰。 把原本堆在一个方法里的 if-else 或 switch 拆成独立的策略类,每个类只负责一种算法,职责单一。选择逻辑可集中到 Map、工厂或配置里。

3. 算法可以独立复用和测试。 每个策略类都是独立单元,可单独编写单元测试,也可被多个上下文复用。

4. 支持运行时动态切换。 客户端可以在两次调用之间替换上下文持有的策略引用,从而改变下一次执行所用的算法。

5. 上下文与具体算法解耦。 上下文只依赖策略接口,不依赖任何具体策略类,符合依赖倒置原则。

6. 算法实现和选择逻辑分离。 "用哪种算法"和"算法怎么实现"是两件事,互不纠缠,便于分别演进。

缺点

1. 类数量会增加。 每种算法一个策略类,算法一多,类的数量就明显上升。算法很简单时,引入策略模式反而让结构变重。

2. 客户端需要了解策略差异。 客户端要负责选择策略,就必须知道有哪些策略、各自适合什么场景。选择逻辑复杂时,需额外引入工厂、注册表、配置中心分担。

3. 策略之间难以自然共享状态或逻辑。 策略类通常彼此独立,公共逻辑要么重复,要么抽到基类或工具类里。算法之间有复杂共享状态或相互依赖时,策略模式表达起来较别扭。

4. 可能造成过度设计。 算法种类少、几乎不变化、分支也简单时,硬套策略模式会引入不必要的接口和类。模式的价值在于隔离变化,没有变化时引入模式就是负担。

5. 上下文与策略的通信需要设计。 数据怎么传、传多少,需要提前设计。传少了策略不够用,传多了策略与上下文耦合变紧。

6. 无法中断正在执行的算法。 策略模式切换的是"下一次调用用哪个策略",不能把正在执行的算法中途替换掉(详见「注意事项」第 11 条)。

7. 策略对象生命周期需要管理。 策略是有状态还是无状态、能否共享、由谁创建和销毁,都需要考虑。多线程环境下要额外注意。


一个反面例子

“不要为了模式而模式”,这里给一个具体的对照:

// 场景:只有 2 种支付方式,且未来一年大概率不会新增
// 写法 A:直接 if-else —— 1 个方法,10 行
String pay(String type, long amount) {
    if ("wechat".equals(type)) return wechatPay(amount);
    if ("alipay".equals(type)) return alipayPay(amount);
    throw new IllegalArgumentException("unknown pay type: " + type);
}

// 写法 B:引入策略模式 —— 1 接口 + 2 实现 + 1 上下文 + 1 注册表
// 收益:新增渠道时不改入口;
// 成本:文件数从 1 变 5,新人需先理解 5 个类才能改一行逻辑。

判断标准:

  • 算法数量 ≥ 3 且预计会增长 → 策略模式值得;
  • 算法几乎不变、数量少、分支简单 → if-else 更直接;
  • 算法简单但数量多、需要运行时选择 → 策略模式 + 注册表;
  • 算法简单、无状态、方法单一 → 函数式策略 是折中方案。

应用场景

以下场景是策略模式常用的地方,但未必只能用策略模式。判断时看两条:
① 同一行为是否存在多种可互换的实现;
② 是否需要运行时选择其中一种。
如果流程骨架固定、只有某一步可变,优先考虑模板方法;如果策略创建逻辑复杂,需配合工厂。

主流框架使用策略模式的场景

Java 标准库的 Comparator。 Comparator 接口本身就是策略抽象,compare() 是策略方法;调用 Collections.sort(list, comparator) 或 list.sort(comparator) 时,排序算法保持不变,但"怎么比较"被替换成了你传入的策略。按姓名排、按年龄排、按价格排,只需换一个 Comparator 实现,排序主流程一行都不用改。

Spring 的 PlatformTransactionManager。 Spring 事务管理的核心接口就是策略接口,DataSourceTransactionManager、JpaTransactionManager、JtaTransactionManager 是不同的事务策略实现。上层代码只依赖 PlatformTransactionManager,具体用哪种事务管理器由配置或运行环境决定。

Spring MVC 的 HandlerMapping 与 ViewResolver。 DispatcherServlet 作为上下文,根据配置选择不同的 HandlerMapping 来定位请求处理器;ViewResolver 同样通过策略模式支持 JSP、Thymeleaf、Freemarker 等多种视图技术。

Spring 的 InstantiationStrategy。 这是 Spring 内部用于创建 Bean 实例的策略接口,SimpleInstantiationStrategy 和 CglibSubclassingInstantiationStrategy 分别代表"直接反射实例化"和"CGLIB 子类化实例化"两种算法。

前端与 JavaScript 生态。 表单校验是前端最典型的策略模式应用:把"非空"“最小长度”“手机号格式”"邮箱格式"等规则各自封装成独立校验策略,调用方按顺序执行规则集。动画缓动函数(linear、easeIn、easeOut)本质上也是可互换的缓动策略,React 中不同状态对应不同 reducer 的设计同样带有策略模式的影子。

其他场景

支付渠道路由。 微信支付、支付宝、银联、云闪付等渠道在"支付"这个统一动作下,下单、签名、回调验签的算法各不相同。把每个渠道封装成独立策略,支付主流程只依赖统一的支付接口。

优惠与折扣计算。 电商结算中经常同时存在无优惠、满减、折扣码、会员价、阶梯价、秒杀等多种计价规则。每一种规则是一个价格策略,结算引擎根据活动配置或用户身份选择对应策略执行。

风控与审核策略。 同一笔交易或同一个用户,可以根据风险等级、用户类型、金额区间选择不同的审核策略:白名单跳过、基础校验、人工复核,或者规则引擎与机器学习模型之间的切换。

文件解析与导出。 同一个"读取文件"的动作,面对 Excel、CSV、PDF、Word 时解析算法完全不同;导出时也有多种格式选择。

注意:如果"打开→读取→解析→关闭"的流程骨架固定、只有"解析"这一步可变,这更接近模板方法;只有连流程都可变时才是策略模式。

登录与认证方式。 密码登录、短信验证码、扫码、OAuth、Passkey 等认证方式共享"认证"这个统一语义,但校验流程各异。

缓存淘汰与压缩算法选择。 缓存容器需要支持 LRU、LFU、FIFO、TTL 等不同的淘汰策略,容器本身负责存储和读取,淘汰逻辑委托给当前策略。文件上传场景中按类型选择 gzip、brotli 或不压缩,也是同样的结构。

消息处理与数据汇聚。 系统监听多种类型的消息,商品消息、价格消息、库存消息需要补充不同的数据后写入同一个存储。

注意:这类场景往往是策略 + 观察者 + 责任链的组合,策略只负责"补充数据"这一步,消息分发由观察者/责任链承担。

排序、推荐与搜索匹配。 列表需要支持按最新、热度、距离、价格、个性化推荐等多种排序方式,搜索需要支持精确匹配、模糊匹配、拼音匹配、语义匹配等。

游戏 AI 行为切换。 游戏角色可以在巡逻、追击、防御、逃跑等行为之间切换,每个行为是一种决策策略。


注意事项

必读:接口与契约设计

1. 策略接口要稳定,参数要一次设计到位。 策略接口是所有具体策略共同遵守的契约,一旦上线后被大量实现类依赖,再改方法签名成本很高。设计时要考虑清楚:方法需要哪些输入、是否需要返回值、是否需要抛出异常、是否要支持上下文数据。

2. 策略接口的粒度要合适,不要做成"万能接口"。 一个策略接口只应表达一类可互换的算法。如果把支付、退款、对账都塞进同一个接口,就会出现大量空实现或抛 UnsupportedOperationException 的实现类。

3. 策略是否需要返回值、是否需要上下文,要提前想清楚。 如果一开始设计成无返回值,后续需要结果时就得改接口。反过来,如果把整个上下文对象直接传给策略,策略可能反过来修改上下文状态,造成耦合和副作用。

4. 策略接口要有清晰的异常约定。 每个策略可能抛出不同异常,调用方需要统一处理。接口层应约定好异常类型:是抛受检异常、运行时异常,还是返回结果对象。

必读:选择逻辑的位置

5. "选哪个策略"的逻辑不要塞进上下文。 上下文的职责是持有并委托策略,不是判断该用哪个策略。选择逻辑应放在客户端、工厂、注册表或配置中心里。

6. 选择逻辑本身也要防止膨胀。 把 if-else 从业务方法搬到客户端,不等于消灭了分支。选择条件复杂时,应进一步用 Map 注册表、工厂方法、配置映射或规则引擎来承载。

7. 要处理"找不到策略"的情况。 按字符串或枚举从注册表取策略时,key 拼错、配置缺失、版本不一致都可能导致取到 null。必须有明确的兜底策略:是抛异常、用默认策略,还是返回错误结果(见 Java 版场景 3 的 getOrDefault 示例)。

8. 默认策略要显式设计,不要靠运气。 默认策略应明确声明并测试,而不是在取不到策略时临时 new 一个,导致行为不可预期。

选择逻辑的演进路径(按规模从轻到重):

阶段选择方式适用规模新增策略要改哪里
1客户端 if-else2~3 个策略加分支
2Map 注册表中等数量(4~10)加一行注册
3工厂 / 枚举 + 注册表需要统一创建和兜底加注册 + 枚举项
4Spring DI(Map<String, Strategy> 注入)项目级加 Bean 即可
5配置中心 / 规则引擎策略由配置或规则驱动改配置,不改代码

不必一步到位,按规模演进即可。跨级升级往往是过度设计。

必读:状态、共享与并发

9. 尽量让策略无状态,无状态策略才能安全共享。 策略对象如果内部保存可变状态,多个上下文共享同一个实例时就会互相干扰。无状态策略可以做成单例或 Spring Bean;有状态策略则要考虑每次创建新实例,或把状态放到方法参数、上下文或外部存储里。

10. 多线程环境下要确认策略是否线程安全。 即使策略本身无成员变量,如果它依赖的成员对象不是线程安全的,或策略方法里用了共享的可变静态变量,仍然会有并发问题。"无状态"只描述策略对象自身,不保证其依赖链安全。

11. 切换策略只影响下一次调用,不要误解为中断当前执行。 setStrategy() 替换的是上下文持有的引用,已经进入旧策略方法的那次调用会继续执行完。如果业务需要"执行到一半改变做法",必须在业务层显式分段。

12. 策略对象的生命周期要明确。 策略由谁创建、由谁销毁、能否缓存、是否每次调用新建,需要统一约定。频繁 new 会增加 GC 压力;长期缓存有状态策略又可能带来状态残留。

必读:上下文与策略的通信

13. 传给策略的数据要最小够用。 策略需要的数据应通过参数显式传入,而不是让策略去反查上下文。传整个上下文虽然方便,但会让策略依赖上下文的具体结构。

14. 策略不应反向修改上下文的核心状态。 需要回传结果时,应通过返回值或结果对象表达,而不是让策略直接改上下文。确需回调时,应定义清晰的回调接口。

15. 策略之间的公共逻辑要合理抽取。 多个策略有重复代码时,可以抽到抽象基类、工具类或默认方法里,但不要为了复用把不相关的策略强行塞进继承体系。

进阶:工程落地(Spring / DI / 序列化)

16. 与工厂、注册表或依赖注入配合使用。 策略模式本身只解决"持有并委托",不解决"如何创建和查找策略"。实际项目中通常用工厂、Map 注册表或 Spring 容器来管理策略实例和名称映射。

17. Spring 中要注意 Bean 名称冲突和注入方式。 多个策略实现同一个接口时,按类型注入会因候选 Bean 过多而失败。通常需要给每个策略指定 Bean 名称,用 Map<String, Strategy> 注入,或配合自定义注解和工厂按名称查找。注入方式要在项目内统一。

18. 策略的标识要稳定且可配置。 用字符串作为策略 key 时,要统一命名规范。key 最好来自枚举或常量,而不是散落的魔法字符串。对外暴露的 key 还要考虑兼容性,重命名可能影响配置中心、数据库存量数据和外部调用方。

19. 序列化与远程调用场景要额外考虑。 如果策略对象需要跨进程传输或持久化,通常不建议直接序列化策略对象,而是序列化策略标识,由接收方根据标识重新选择策略,避免实现类变更导致反序列化失败。

进阶:可维护性与测试

20. 不要为了模式而模式。 如果只有两三种算法、几乎不变化、分支也很简单,直接写 if-else 或 switch 更直观(见「一个反面例子」)。

21. 警惕策略类爆炸。 可以通过合并真正相近的策略、用函数式接口替代简单策略、用配置驱动参数化策略来缓解。不是所有差异都值得单独建类。

22. 策略命名要表达"算法差异",而不是"实现细节"。 比如 WechatPayStrategy、FullReductionDiscountStrategy,而不是 PaymentStrategyImpl1、DiscountHandlerA。

23. 文档中要写清楚每个策略的适用条件和约束。 客户端选择策略时需要知道各策略的差异:适用场景、性能特征、副作用、是否线程安全、失败时行为如何。

24. 每个策略都要独立测试,上下文也要测试委托行为。 策略类应单独覆盖正常路径、边界条件和异常路径。上下文则要测试"是否正确委托给当前策略"以及"切换后是否使用新策略"。

25. 异常处理策略要统一。 不同策略抛出的异常如果类型各异,调用方难以统一处理。可以在策略接口层约定异常转换规则,或者在上下文委托处统一包装。要避免某个策略静默吞掉异常。

26. 策略的失败回退要有明确设计。 某些策略失败后可以降级到默认策略,某些失败必须直接报错。回退逻辑应显式设计并测试,不能靠调用方临时 try-catch 再换一个策略。


策略模式不负责什么

把职责误加给上下文,是策略模式被用坏的最常见原因。明确边界,比记住定义更重要:

职责谁负责不该由谁负责
选择哪个策略客户端、工厂、注册表、配置中心上下文(上下文只持有并委托)
策略实例的创建与销毁工厂、DI 容器上下文(除非它是工厂本身)
中断正在执行的算法业务层显式分段策略模式(只换下一次调用)
策略之间的通信与状态共享额外设计(参数、结果对象、共享状态容器)策略接口(应保持最小契约)
策略的兜底与降级选择逻辑所在的层(客户端/注册表/配置)策略实现类(不应互相兜底)

一句话:策略模式只做一件事——把"用什么算法"从"怎么调用算法"里分离出来。其余的事情,交给工厂、注册表、DI 容器、配置中心或业务层。


总结

  • 统一入口不变,具体算法可换;换的是下一次怎么做,不能把正在做的事中途变掉。
  • 接口定契约、具体类实现算法、上下文持有接口并委托执行、客户端负责选择与切换。
  • 是否值得用,看两条:算法是否多样且会变;是否需要运行时选择。二者缺一,先写分支。
  • 落地时记住三件事:选择逻辑外置、策略尽量无状态、明确策略模式不负责什么。
上一篇: 设计模式之状态模式
LiaoYuan.Dai
博客等级 码龄7年 247粉丝 21原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值