你的策略模式是 Map<String, Strategy>?那不过是最廉价的 if-else 替代品

每个策略模式教程最后都是一样的结论:把策略放进 Map,按 key 拉出来。"看,没有 if-else!"他们说,好像把条件逻辑塞进查找表就能让它消失一样。它不会消失。if-else 只是从业务代码搬到了你填充 Map 的地方。新增一个策略,你仍然得在某处注册它。Map 方案是策略模式的最小可行实现------它能用,但只解决了恰好一个问题,剩下的全忽略了。

当策略选择不是简单的字符串查找时会发生什么?当策略需要组合,或者基于多个条件选择,或者动态加载时?这时候大多数人的"策略模式"就碎了,因为他们从没学过模式的完整机制。

Map 方案能用------直到它不能用

教科书示例:支付策略。

java 复制代码
public interface PaymentStrategy {
    void pay(Order order);
}

@Service
public class AlipayStrategy implements PaymentStrategy { ... }
@Service
public class WechatStrategy implements PaymentStrategy { ... }

@Component
public class PaymentService {
    private final Map<String, PaymentStrategy> strategies;

    public PaymentService(List<PaymentStrategy> list) {
        this.strategies = list.stream()
            .collect(Collectors.toMap(
                s -> s.getClass().getSimpleName().replace("Strategy", "").toLowerCase(),
                s -> s
            ));
    }

    public void pay(String type, Order order) {
        strategies.get(type).pay(order);
    }
}

这是每篇博客都教的版本。Spring 注入所有实现,你按名映射,按 key 查找。干净,pay() 里没有 if-else。但它也是策略模式最浅的理解。

三个迟早会打你的问题:

问题1:策略发现本身仍是条件逻辑。 某处总要决定传哪个 key。如果决策来自 controller 读查询参数,还行。如果决策来自一个根据订单金额、用户历史、风险评分综合判断的服务------你的 Map 就废了。条件逻辑只是换了位置。

问题2:策略组合看不到。 高风险订单需要同时选支付策略和风控策略,一起选定。Map<String, Strategy> 每次只给你一个策略。组合需要不同的抽象------StrategyChain 或 StrategySelector------大多数人从来没建过。

问题3:不能不碰代码就加策略。 Map 在启动时填充。动态策略------插件、数据库规则、租户级配置------需要运行时可添加的注册表。Spring 的 List-injection 做不了。

策略模式实际提供的三个机制

策略模式有 Map 版本忽略的三个机制:

1. Context-driven selection

模式的 Context 对象不只是分发器。它是持有选择准则的实体。GoF 画这个模式时,Context 持有 Strategy 的引用,但也持有决定用哪个 Strategy 的状态。

在实践中这意味着 StrategySelector------不是 Map:

java 复制代码
public interface StrategySelector<T extends Strategy> {
    T select(StrategyContext context);
}

Spring 的 HandlerMapping 就是这个。它不是按字符串查 handler,而是评估 RequestCondition 对象------路径模式、HTTP 方法、header、参数------按匹配精度排序。选择逻辑本身也是策略(RequestMappingHandlerMapping vs SimpleUrlHandlerMapping),你可以插自己的。

java 复制代码
// Spring 的策略选择------不是 Map lookup
public abstract class AbstractHandlerMapping implements HandlerMapping {
    @Override
    public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
        Object handler = getHandlerInternal(request);
        // ...
    }
}

选择基于请求本身,不是硬编码的 key、不是枚举。Context 决定 Strategy。

2. 策略层次与组合

真实系统没有扁平的策略列表。它们有层次------主策略带备用策略、组合多个子策略的策略、包裹其他策略的策略。

Spring 的 ResourceLoader 是好例子。它不是一个资源加载策略,而是链:

java 复制代码
// DefaultResourceLoader → ClassPathResourceLoader
// FileSystemResourceLoader → file:// 协议
// UrlResourceLoader → http:// 协议
// 你可以注册自定义 Protocol 作为策略
public class DefaultResourceLoader implements ResourceLoader {
    private final Map<String, ProtocolResolver> protocolResolvers = new LinkedHashMap<>();
}

ProtocolResolver 是策略,但整体资源加载是一个组合策略,按顺序尝试每个 resolver。这不是 Map<String, Strategy>------是带优先级和备用的解析链。

3. 运行时策略注册

GoF 模式没说策略是固定的。它说 Context 可以随时换 Strategy。这意味着运行时支持 add/remove/list 的 StrategyRegistry:

java 复制代码
public interface StrategyRegistry<T extends Strategy> {
    void register(String name, T strategy);
    void unregister(String name);
    List<T> list();
}

这是插件架构的基础。多租户 SaaS 里,每个租户可能有不同的折扣策略,从配置加载。启动时填充的 Map 做不了------你需要可变注册表。

Spring 在哪里正确使用了策略模式

Spring 在大多数人从不看的地方用了策略模式:

HandlerMapping------为请求找 handler 的策略。多个实现共存,各有选择准则。DispatcherServlet 不选一个------遍历所有,用第一个返回 handler 的。策略选择由 Context 完成,不是 Map key。

ResourceLoader------加载资源的策略。ProtocolResolver 策略动态注册。策略由资源路径本身选择,不是显式 key。

PropertySource ------读配置的策略。每个 PropertySource 是策略(文件、环境变量、数据库、远程配置)。Spring Environment 按优先级聚合它们。Map<String, PropertySource> 会丢掉让整个机制运转的优先级排序。

怎么正确实现策略模式

想用策略模式超越 Map 方案,需要这些:

分离选择与分发:

java 复制代码
// 不是这样:
strategies.get(type).execute(context);

// 这样:
Strategy strategy = selector.select(context);
strategy.execute(context);

selector 封装选择逻辑。可以简单到 Map 查找,也可以复杂到评分算法。关键是:选择是独立于执行的独立关注点。

让策略注册可动态:

java 复制代码
@Component
public class StrategyRegistry {
    private final ConcurrentHashMap<String, Strategy> strategies = new ConcurrentHashMap<>();

    public void register(String name, Strategy strategy) {
        strategies.put(name, strategy);
    }

    public void unregister(String name) {
        strategies.remove(name);
    }
}

不依赖 Spring 的 List injection。建一个运行时可添加策略的注册表------插件、租户配置、外部来源的策略都需要这个。

为复杂选择建 StrategySelector:

java 复制代码
@Component
public class DiscountStrategySelector implements StrategySelector<DiscountStrategy> {
    private final List<DiscountStrategy> candidates;

    @Override
    public DiscountStrategy select(DiscountContext context) {
        return candidates.stream()
            .filter(s -> s.supports(context))
            .max(Comparator.comparingInt(DiscountStrategy::priority))
            .orElseThrow(() -> new NoStrategyException(context));
    }
}

这支持 Context 过滤、优先级排序和备用------Map<String, Strategy> 永远做不到的事。

真正的权衡

策略模式的威力不在消除 if-else。在于让策略选择、组合和演化独立于使用策略的代码。Map 版本实现了其中一个(演化------加个新类 Spring 就捡起来)。但牺牲了选择灵活性和组合。

如果你的策略选择确实是简单字符串查找------支付类型、渠道代码------Map 版本够了,别过度设计。但如果选择涉及 Context、优先级、组合或运行时注册,Map 版本会碎,你会发现加越来越多的条件逻辑到本该消除它的"策略"层。

策略模式是关注点分离------不只是策略之间的分离,还有选择、注册和执行之间的分离。Map 方案把三者压成一个结构。它能用,直到那个驱动你选策略模式的复杂度回来,你发现模式其实没解决它。

我最近在做一个小程序叫「爪爪代码冒险记」,用卡皮巴拉漫画讲设计模式------策略模式那期画的场景就是选支付方式时 Map 方案翻车的现场,比纯看代码有意思点,可以搜搜看。

相关推荐
长栎2 小时前
你写的 abstract class 里全是钩子方法——模板模式不是让你填空,是让你别越界
后端
ping某2 小时前
语法树,到底是一棵什么形状的树?
后端
_柳青杨2 小时前
一文吃透 Node.js 事件循环:从原理到 Node 20+ 重大变更
javascript·后端
Alson_Code2 小时前
人机协作项目文档--HITL-AgentScope
后端·aigc·ai编程
IT_陈寒2 小时前
Java 并行流把我坑惨了,这6小时加班值了
前端·人工智能·后端
葫芦和十三3 小时前
图解 MongoDB 03|CRUD 全链路:一条 find 怎么穿过 WiredTiger
后端·mongodb·agent
葫芦和十三11 小时前
图解 MongoDB 04|索引模型:每建一个索引,就是在 B+-tree 森林里多栽一棵
后端·mongodb·agent
用户479492835691512 小时前
claude Fable用不了?把Gpt 5.5pro接到你的claude code里
前端·后端
GetcharZp14 小时前
告别 Nginx 复杂配置!这款带 Web 面板的万能代理神器,让端口转发变得如此简单
后端