如何用工厂方法模式重构大量 if-else 创建逻辑?
简化版
重构大量 if-else 时,先抽象产品接口,再把每个分支的创建逻辑拆成具体工厂,最后用配置、注册表或依赖注入完成工厂选择。重点是逐步迁移并补测试,避免一次性改动把创建逻辑和业务行为都搅在一起。
详细版
可以按五步做:
- 找出所有分支返回对象的共同能力,抽象产品接口;
- 把每个
if分支中的构造细节移到具体工厂; - 定义统一工厂接口,例如
HandlerFactory.create(); - 用
Map<String, HandlerFactory>替换中心分支; - 为旧类型补回归测试,再逐个迁移新类型。
重构前:
Handler create(String type) {
if ("email".equals(type)) return new EmailHandler(config);
if ("sms".equals(type)) return new SmsHandler(client);
if ("push".equals(type)) return new PushHandler(token);
throw new IllegalArgumentException(type);
}
重构后:
HandlerFactory factory = factories.get(type);
if (factory == null) throw new IllegalArgumentException(type);
return factory.create();
这样中心代码只负责查找工厂,具体构造由各自工厂维护。新增产品时新增工厂并注册,旧分支不必继续膨胀。
完整版教学
一、先确认分支是不是同一种变化
不是所有 if-else 都适合用工厂方法。有些分支只是简单条件判断,没有对象创建变化;有些分支返回的对象能力完全不同,强行抽象会制造虚假的公共接口。
适合重构的信号是:多个分支都在创建同一抽象下的不同实现,而且新增类型越来越频繁。
二、抽象产品接口
先从调用方使用对象的方式提炼接口:
interface Handler {
void handle(Request request);
}
接口只保留稳定行为,不把某个具体分支的配置细节塞进来。
三、拆分具体工厂
每个复杂分支可以拆成一个工厂:
class EmailHandlerFactory implements HandlerFactory {
public Handler create() {
return new EmailHandler(/* dependencies */);
}
}
如果构造依赖很多,工厂本身可以通过构造器注入依赖,避免产品创建代码散落在业务流程里。
四、用注册表替换中心分支
中心创建器保留统一入口,但不再知道每个产品怎么创建:
class HandlerFactoryRegistry {
private final Map<String, HandlerFactory> factories;
Handler create(String type) {
HandlerFactory factory = factories.get(type);
if (factory == null) throw new IllegalArgumentException(type);
return factory.create();
}
}
这一步让新增产品变成“新增工厂 + 注册映射”,而不是改一段高风险分支。
五、迁移时要保护行为
重构创建逻辑很容易改变异常类型、默认值、依赖初始化顺序和对象生命周期。迁移前最好补齐旧分支测试,至少覆盖已支持的类型、未知类型、关键配置和并发使用场景。
例如旧代码遇到未知 type 时抛 UnsupportedChannelException,重构后就不应意外变成 NullPointerException;旧产品每请求新建,迁移后也不能无意注册成共享单例。把这些可观察行为写进测试,才能证明只是移动创建职责,而没有悄悄改变业务。
六、用第四种渠道验证重构后的修改面
已有 email、sms、push 三种 Handler 时,新增 webhook 是最直接的设计压力测试。理想结果是新建 WebhookHandler 和对应工厂,注册一次即可;旧三种 Handler 的构造路径与业务分发代码都不修改。
启动期: 注册 email -> EmailHandlerFactory
启动期: 注册 sms -> SmsHandlerFactory
启动期: 注册 push -> PushHandlerFactory
新增: 注册 webhook -> WebhookHandlerFactory
请求 type=webhook -> registry.get
factory.create(context) -> WebhookHandler
业务层只调用 Handler.handle(request)
这段协作图要回答两个不同问题:产品由谁构造,具体工厂又由谁选择。工厂方法可以把产品构造从业务流程中移走,但应用入口、配置层或容器仍然必须决定使用哪个工厂;声称模式“消灭了所有分支”并不准确。
七、模式边界与扩展成本
| 维度 | 本题结论 |
|---|---|
| 稳定抽象 | Handler 表达所有渠道稳定处理能力,HandlerFactory 表达创建所需上下文。 |
| 主要变化点 | 渠道专属依赖、配置、构造校验和产品类型数量。 |
| 新增一种产品的动作 | 新增 Handler、Factory 和注册项;旧处理流程保持封闭。 |
| 不应放进工厂的职责 | 渠道执行流程、请求路由后的业务判断和跨渠道编排。 |
| 更轻或更合适的方案 | 若 Handler 本身无状态且由 Spring 管理,可直接注入 Map<String, Handler>,不必每次创建。 |
是否符合开闭原则不能只数新增了多少类,而要看高风险旧代码是否仍被反复修改。若每次扩展仍要修改一个中心 switch、产品接口又泄漏具体类型,即使类名叫 Factory,也没有获得工厂方法的主要收益。
八、用三个测试验证设计没有走样
- 契约测试:对每个具体工厂执行同一组产品接口测试,确认返回值满足抽象契约。
- 扩展测试:加入一个假产品和假工厂,记录为了接入它修改了哪些旧文件。
- 未知类型测试:若存在注册表或参数路由,明确缺失 key 的异常类型和降级策略。
- 生命周期测试:连续创建两次,确认产品应当复用还是隔离,避免无意变成单例或重复昂贵连接。
- 依赖测试:具体构造参数应由工厂显式获得,业务类不应通过全局容器偷偷查找。
- 并发测试:注册表若支持运行期写入,必须定义发布、覆盖和读取的一致性;只读注册表则尽量启动期冻结。
- 可观测性测试:记录命中的工厂 key 与具体实现,排查配置选择错误时无需猜测。
记忆钩子:重构不是把 if 换成 Map,而是让每个分支的构造知识有独立归属。
九、常见误区与追问
- 误区:把 if-else 改成反射类名就完成重构。 反射字符串削弱类型安全,也没有处理依赖和生命周期。
- 误区:注册表中应存产品实例而不是工厂。 取决于产品是否可复用;有请求状态时必须存创建能力。
- 误区:所有条件分支都适合工厂化。 只有同一抽象下的创建变化才适合。
- 追问:注册表如何避免运行期并发修改? 优先启动期构造不可变 Map;动态插件需要并发结构和明确覆盖规则。
- 追问:如何渐进迁移旧分支? 先补字符化测试,再让旧入口委托注册表,逐种渠道迁移。
- 追问:未知 type 应怎样处理? 显式抛业务异常或使用明确默认工厂,不能让空指针偶然决定行为。
十、加强记忆
用工厂方法重构 if-else 的顺序是先抽象产品,再拆具体工厂,最后用注册表或容器选择工厂。不要一开始就删分支,先让新旧行为对齐,重构才稳。