每个策略模式教程最后都是一样的结论:把策略放进 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 方案翻车的现场,比纯看代码有意思点,可以搜搜看。