工厂方法模式中产品接口和工厂接口应该如何设计?
简化版
产品接口应该描述调用方真正需要使用的能力,工厂接口应该只暴露创建产品所需的稳定方法。设计时要避免把具体产品细节泄漏到抽象接口里,否则工厂方法的解耦价值会被削弱。
详细版
工厂方法一般有两层抽象:
interface MessageSender {
void send(Message message);
}
interface MessageSenderFactory {
MessageSender createSender();
}
MessageSender 是产品接口,面向业务动作;MessageSenderFactory 是工厂接口,面向创建动作。具体实现可以是 EmailSender、SmsSender、EmailSenderFactory、SmsSenderFactory。
产品接口不要暴露只有某个具体产品才有的方法,例如不要在 MessageSender 里强行放 setEmailServer()。这会让其它产品实现无意义方法,也让调用方重新依赖具体细节。
工厂接口也不要过度膨胀。如果创建参数很多,可以考虑把参数封装成配置对象;如果一个工厂要创建一组相关产品,可能已经接近抽象工厂模式。
完整版教学
一、产品接口先从调用方视角出发
产品接口不是把所有具体类的方法取并集,而是抽象调用方稳定需要的能力。比如发送消息时,调用方关心 send,不关心邮件服务器、短信供应商或重试 SDK 的细节。
好的产品接口应该满足三个条件:语义稳定、方法数量克制、不会偏向某个具体实现。
二、工厂接口只表达创建意图
工厂接口通常只有一个核心方法,例如 create()、createSender()、newParser()。方法名要体现创建出来的是什么,而不是暴露内部构造步骤。
interface ParserFactory {
Parser createParser(ParserOptions options);
}
如果创建依赖参数,参数最好是稳定的配置对象,而不是不断增长的方法参数列表。
三、返回抽象而不是具体类
工厂方法的返回类型应该尽量是产品接口或抽象类:
Parser createParser();
如果返回 JsonParser 这种具体类,调用方仍然被具体实现绑住,后续替换为 FastJsonParser、JacksonParser 的成本就会上升。
四、警惕抽象被具体细节污染
常见错误是为了适配某个实现,把具体配置方法塞进公共接口。比如 MessageSender 里出现 setSmsTemplateId(),邮件发送器就会很尴尬。
更好的做法是把具体配置放在具体工厂内部、配置对象里,或者让不同产品拆分成不同抽象层级。
五、从调用方和构造方分别收缩两个接口
设计消息发送器时,业务只需要 send(message),创建方才需要 endpoint、凭证和重试配置。把 setSmsTemplateId 塞进公共产品接口,或让业务层逐项传 SDK 细节,都会让抽象向某个实现倾斜。
OrderService -> MessageSender.send(message)
MessageSenderFactory -> create(SenderOptions)
EmailFactory 内部读取 SMTP 配置
SmsFactory 内部读取供应商 SDK 配置
两者都返回 MessageSender
OrderService 不强转具体 Sender
具体配置变化只影响对应 Factory/Product
这段协作图要回答两个不同问题:产品由谁构造,具体工厂又由谁选择。工厂方法可以把产品构造从业务流程中移走,但应用入口、配置层或容器仍然必须决定使用哪个工厂;声称模式“消灭了所有分支”并不准确。
六、模式边界与扩展成本
| 维度 | 本题结论 |
|---|---|
| 稳定抽象 | 产品接口表达稳定业务能力,Creator 接口表达最小且稳定的创建输入。 |
| 主要变化点 | 具体 SDK、认证参数、初始化步骤与产品实现。 |
| 新增一种产品的动作 | 新增产品和工厂,只要满足两个接口即可接入。 |
| 不应放进工厂的职责 | 具体产品运行时专属方法不应污染公共产品接口。 |
| 更轻或更合适的方案 | 确实没有公共语义时拆成不同产品接口,不要强造一个万能抽象。 |
是否符合开闭原则不能只数新增了多少类,而要看高风险旧代码是否仍被反复修改。若每次扩展仍要修改一个中心 switch、产品接口又泄漏具体类型,即使类名叫 Factory,也没有获得工厂方法的主要收益。
七、用三个测试验证设计没有走样
- 契约测试:对每个具体工厂执行同一组产品接口测试,确认返回值满足抽象契约。
- 扩展测试:加入一个假产品和假工厂,记录为了接入它修改了哪些旧文件。
- 未知类型测试:若存在注册表或参数路由,明确缺失 key 的异常类型和降级策略。
- 生命周期测试:连续创建两次,确认产品应当复用还是隔离,避免无意变成单例或重复昂贵连接。
- 依赖测试:具体构造参数应由工厂显式获得,业务类不应通过全局容器偷偷查找。
- 并发测试:注册表若支持运行期写入,必须定义发布、覆盖和读取的一致性;只读注册表则尽量启动期冻结。
- 可观测性测试:记录命中的工厂 key 与具体实现,排查配置选择错误时无需猜测。
记忆钩子:产品接口回答“拿到后做什么”,工厂接口回答“创建时需要什么”。
八、常见误区与追问
- 误区:产品接口应包含所有具体类方法的并集。 这会制造无意义实现并泄漏具体细节。
- 误区:工厂方法必须无参数。 可以接收稳定的配置/上下文对象,关键是隐藏具体构造。
- 误区:返回 Object 最灵活。 它丢失编译期契约,迫使调用方强转。
- 追问:创建参数很多怎么办? 封装成语义明确的 Options,或重新审视工厂职责是否过宽。
- 追问:具体配置放哪里? 由具体工厂持有/注入,或通过专属配置对象传入,避免进入产品公共接口。
- 追问:何时接近抽象工厂? 一个工厂接口开始成套创建多个相关产品等级时。
九、加强记忆
产品接口回答“调用方拿到产品后能做什么”,工厂接口回答“调用方怎样拿到产品”。只要抽象里出现大量具体实现的词,就说明边界可能泄漏了。