← 返回题目列表

策略模式中如何设计策略工厂或策略注册表?

高频 中等 第 13 / 26 题 更新于 2026/07/28
策略模式策略工厂注册表Spring

简化版

策略工厂或注册表负责根据业务类型找到对应策略。常见实现是用 Map<类型, 策略> 保存映射,启动时注册所有策略,调用时按类型获取;这样可以避免主流程里写大量条件分支。

详细版

策略工厂的核心职责是“选择策略”,不要把策略执行逻辑塞进去。

常见设计方式有三种:

  1. 手动注册:适合策略少、项目简单的场景。
  2. 枚举注册:适合类型稳定、需要集中声明业务码的场景。
  3. 容器自动注册:在 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,工厂就能自动收集。

五、策略注册表的工程注意点

策略注册表看起来简单,但真实项目要注意几个坑:

  1. 重复 type:两个策略返回同一个类型时,启动阶段就应该失败。
  2. 未知 type:不要静默返回空,应该明确报错或走默认策略。
  3. 策略状态:如果策略 Bean 是单例,不要保存请求级可变状态。
  4. 类型来源:外部参数要先校验,不能让任意字符串直接驱动敏感逻辑。
  5. 策略选择复杂度:如果选择条件很多,可以把选择规则单独抽出来。

一个好的策略工厂应该很薄,只做选择,不做具体业务。

六、注册、查找与生命周期治理

注册表含 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,运行时按类型取出执行;工厂要处理重复注册、未知类型、默认策略和无状态问题,但不要替具体策略干活。