记得刚入行那会儿,我特别怕碰到一种需求: “小王啊,咱们的消息推送,除了发短信,能不能再加个邮件推送?过两天可能还得加个微信通知。”
我当时的做法特别直接,在业务代码里写了一堆 if-else。
只要类型是 SMS,我就 new SMSProvider();是 Email,我就 new EmailProvider()。
结果后来渠道越来越多,那个主函数的代码长得像老太婆的裹脚布。 更要命的是,每次加新渠道,我都得颤颤巍巍地去改那段核心逻辑,生怕手一抖,把原来的短信功能给搞挂了。
这其实是我们很多人的通病:把“造东西”和“用东西”这两件事,混在一起做了。
真正的高手,懂得把“麻烦”外包出去
工厂方法模式(Factory Method)的核心逻辑,其实就一句话: 把对象的创建过程剥离出来,交给一个专门的“工厂”去处理,业务逻辑只负责“提要求”。
这就好比你去咖啡店点单。 你只需要跟服务员说“我要一杯拿铁”,你不需要知道咖啡豆磨了多少秒,牛奶加热到了多少度,也不需要知道是哪位咖啡师做的。
如果哪天店里推出了新品“燕麦拿铁”,老板只需要培训咖啡师(工厂)就行了,你作为顾客(业务方),点单的方式完全不用变。
在代码里,这意味着我们要把那个到处 new 的动作,收拢到一个地方。
看看代码是怎么“减肥”的
咱们还是拿那个“消息推送”的例子来拆解。
如果不使用模式,我们的代码通常是这样的:
class MessageService {
send(type, content) {
// 这种写法在初期很常见,但隐患很大
if (type === 'sms') {
const service = new SMSService();
service.send(content);
} else if (type === 'email') {
const service = new EmailService();
service.send(content);
}
// 如果明天要加钉钉通知,你还得回来改这里
}
}
这种写法的痛点在于:你的 MessageService 既要负责发消息的逻辑,又要负责创建服务的逻辑。违背了“单一职责原则”。
现在,我们试着用工厂模式重构一下:
// 1. 先定义好各种具体的服务
class SMSService { send(c) { console.log('短信:', c) } }
class EmailService { send(c) { console.log('邮件:', c) } }
// 2. 造一个工厂,专门负责“生产”这些服务
class NotificationFactory {
static create(type) {
const services = {
'sms': SMSService,
'email': EmailService
};
const ServiceClass = services[type];
if (!ServiceClass) throw new Error('不支持的类型');
return new ServiceClass();
}
}
// 3. 业务逻辑变得极其清爽
class MessageService {
send(type, content) {
// 我不管你是怎么创建的,我只管找工厂要
const service = NotificationFactory.create(type);
service.send(content);
}
}
两种写法的直观对比
容易出问题的写法:
核心业务逻辑里塞满了 if-else 和 new。
后果:每次新增功能都要修改核心代码,测试范围无限扩大,很容易引入新 Bug。
更稳健的工厂写法:
核心业务只依赖一个 create 接口。
后果:如果要加一个“钉钉通知”,你只需要在工厂里注册一下,原来的 MessageService 代码连一个标点符号都不用动。这就是传说中的“对扩展开放,对修改关闭”。
给你的 3 条行动建议
-
识别“变化点”:检查一下你的代码,哪里有大量的
if-else是为了判断“创建哪个对象”的?那里就是植入工厂模式的最佳地点。 -
别搞过度设计:如果是简单的两个类切换,用个简单的函数封装(简单工厂)就够了。别一上来就搞抽象类、接口继承那一套复杂的标准工厂模式,JS 没那么教条。
-
配置化思维:配合配置文件使用工厂模式简直是神器。比如把
type写在 JSON 配置里,这样连代码都不用重新编译,改个配置就能切换系统的行为。
我以前总觉得把 new 拿出来单独写个类是多此一举。
后来项目维护了两年才发现,这种“多此一举”让我在面对老板突如其来的需求变更时,能从容地喝完手里的咖啡,而不是通宵改 Bug。