策略模式中如何设计策略工厂或策略注册表?
简化版
策略工厂或注册表负责根据业务类型找到对应策略。常见实现是用 Map<类型, 策略> 保存映射,启动时注册所有策略,调用时按类型获取;这样可以避免主流程里写大量条件分支。
详细版
策略工厂的核心职责是“选择策略”,不要把策略执行逻辑塞进去。
常见设计方式有三种:
- 手动注册:适合策略少、项目简单的场景。
- 枚举注册:适合类型稳定、需要集中声明业务码的场景。
- 容器自动注册:在 Spring 项目中注入所有策略实现,构建 Map。
一个比较常见的结构是:
interface PayStrategy {
String type();
void pay(Order order);
}
然后工厂维护:
Map<String, PayStrategy> strategyMap;
调用时:
PayStrategy strategy = factory.get(payType);
strategy.pay(order);
策略工厂要注意默认策略、未知类型异常、重复注册、策略是否有状态等问题。
完整版教学
一、策略工厂解决什么问题
策略模式把不同算法拆成了多个类,但还剩一个问题:谁来决定用哪个策略?
如果调用方这样写:
if ("wechat".equals(type)) {
return new WechatPayStrategy();
}
if ("balance".equals(type)) {
return new BalancePayStrategy();
}
那只是把执行逻辑拆开了,选择逻辑仍然散落在业务代码里。策略工厂的作用就是把这部分选择集中起来。
它负责:
- 维护类型与策略的映射;
- 处理未知类型;
- 处理默认策略;
- 屏蔽策略创建细节;
- 防止调用方直接依赖具体策略类。
二、手动注册适合简单场景
最直接的写法是手动构建 Map:
public class PayStrategyFactory {
private final Map<String, PayStrategy> strategies = new HashMap<>();
public PayStrategyFactory() {
strategies.put("wechat", new WechatPayStrategy());
strategies.put("balance", new BalancePayStrategy());
}
public PayStrategy get(String type) {
PayStrategy strategy = strategies.get(type);
if (strategy == null) {
throw new IllegalArgumentException("unsupported pay type: " + type);
}
return strategy;
}
}
这种方式简单清晰,缺点是新增策略要修改工厂。对于小项目或策略数量很少的业务,这个成本可以接受。
三、枚举注册适合类型稳定的业务
有些业务类型本身需要集中管理,比如支付类型、通知渠道、导出格式。可以用枚举维护业务码和描述:
public enum PayType {
WECHAT("wechat"),
BALANCE("balance");
private final String code;
}
枚举适合表达稳定的业务类型,但不要把大量复杂执行逻辑直接塞进枚举。枚举可以负责类型定义,策略类负责行为实现。
如果业务码来自数据库或运营配置,枚举就不一定合适,配置化注册会更灵活。
四、Spring 中常见的自动注册方式
在 Spring 项目中,可以注入所有策略实现:
@Component
public class PayStrategyFactory {
private final Map<String, PayStrategy> strategyMap;
public PayStrategyFactory(List<PayStrategy> strategies) {
this.strategyMap = strategies.stream()
.collect(Collectors.toMap(PayStrategy::type, s -> s));
}
public PayStrategy get(String type) {
PayStrategy strategy = strategyMap.get(type);
if (strategy == null) {
throw new IllegalArgumentException("unsupported pay type");
}
return strategy;
}
}
每个策略作为 Bean 存在:
@Component
public class WechatPayStrategy implements PayStrategy {
public String type() {
return "wechat";
}
public void pay(Order order) {
// 微信支付逻辑
}
}
新增策略时,只要新增实现类并声明为 Bean,工厂就能自动收集。
五、策略注册表的工程注意点
策略注册表看起来简单,但真实项目要注意几个坑:
- 重复 type:两个策略返回同一个类型时,启动阶段就应该失败。
- 未知 type:不要静默返回空,应该明确报错或走默认策略。
- 策略状态:如果策略 Bean 是单例,不要保存请求级可变状态。
- 类型来源:外部参数要先校验,不能让任意字符串直接驱动敏感逻辑。
- 策略选择复杂度:如果选择条件很多,可以把选择规则单独抽出来。
一个好的策略工厂应该很薄,只做选择,不做具体业务。
六、注册、查找与生命周期治理
注册表含 12 个策略时,HashMap 平均查找接近 O(1);相比 12 段顺序判断,扩展点和冲突检测更清晰。
startup: discover -> validate unique keys -> immutable map; request: key -> lookup -> execute
这个推演把模式带来的收益和成本放在同一条调用链上。设计评审时既要确认结果正确,也要确认额外层次没有改变原有契约。
七、边界、代价与验证
| 检查维度 | 应确认的内容 |
|---|---|
| 正确性 | 注册表只负责“key 到策略”的映射,不应吞进每个策略的业务规则。 |
| 适用边界 | 启动期要检测重复 key,运行期要处理未知 key;注册完成后用不可变映射可减少并发风险。 |
| 测试证据 | 覆盖正常路径、异常路径、空值或未知类型,并断言调用次数与顺序 |
| 运行成本 | 记录额外对象、调用层数、延迟、线程或内存开销,不用模式名称代替测量 |
模式落地后要用单元测试验证独立职责,用集成测试验证对象装配和真实调用入口。涉及并发、远程或异步时,还要补充竞态、超时、重复执行与资源释放测试。
易错点:注册表只负责“key 到策略”的映射,不应吞进每个策略的业务规则。
八、常见误区与追问
- 误区:Spring 注入 Map 后重复策略 key 会自动按业务期望解决。 Bean 名或自定义 key 的冲突必须显式校验,不能依赖覆盖顺序。
- 误区:用了策略模式,系统里就不该再出现任何条件判断。 策略消除的是反复扩张的业务算法分支;选择策略、校验输入和兜底仍可能需要有限判断。
- 误区:示例代码能运行,就代表模式边界设计正确。 能运行只证明一条路径,仍要检查异常、并发、顺序、生命周期以及新增实现时是否修改稳定代码。
- 追问:注册表需要每次请求重新扫描 Bean 吗? 不需要;通常启动时构建并校验一次,请求期只做查找。
- 追问:策略对象可以保存本次请求的可变状态吗? 容器单例策略通常不应保存请求状态,应把订单、用户等数据作为参数传入,否则并发调用会相互污染。
- 追问:怎样验证这套设计没有改变原有语义? 先做基线行为测试,再对新增层验证返回值、异常类型、调用次数和顺序;性能敏感处还要比较改造前后的量化指标。
九、加强记忆
策略工厂和注册表的作用是“集中选策略”。最常见实现是启动时把策略放进 Map,运行时按类型取出执行;工厂要处理重复注册、未知类型、默认策略和无状态问题,但不要替具体策略干活。