设计模式 03 · 工厂方法模式

上一篇单例管的是"只造一个",这一篇的工厂,管的是另一个问题:当一个东西有很多种、到底该造哪一种? 还记得开篇那坨支付代码里的 if (payType.equals("wechat")) ... else if ("alipay") ... 吗?每接一个新渠道就得回去加一个分支------这正是工厂模式要收拾的烂摊子。

先澄清一个几乎所有初学者都会绕晕的地方:工厂相关的模式其实有三个 ,而且名字很像------简单工厂工厂方法抽象工厂 。它们是层层递进的关系,不是三选一。简单工厂是最朴素的起点(严格说它不算 GoF 二十三式,但绕不开),工厂方法是本篇的主角,抽象工厂是下一篇的内容。这一篇我们把前两者一次讲透,你会清楚地看到:它们不是三种并列的技巧,而是同一个问题在不同"变化压力"下的三次升级

这篇文章按这条线索展开:先说清楚"new 一个对象"这个再普通不过的动作,为什么也是一种需要治理的耦合;然后用简单工厂把散落各处的 new 收拢到一个地方,再指出它撞上开闭原则的天花板;接着引出工厂方法 ------它把"决定造哪个"这件事从一个大 if-else 里解放出来,下放给一个个专门的子类工厂;最后讲清楚它的四个角色、到底什么场景才值得上工厂,以及它在 JDK/框架里的真实身影。全程还是那套订单支付的例子。

目录

  1. [从一坨 if-else 说起:new 也是一种耦合](#从一坨 if-else 说起:new 也是一种耦合)
  2. [简单工厂:先把 new 收拢到一处](#简单工厂:先把 new 收拢到一处)
  3. 简单工厂的天花板:它违反了开闭原则
  4. 工厂方法:把"造什么"下放给子类
  5. 工厂方法的四个角色与骨架
  6. 到底什么时候才值得上工厂
  7. 现实身影,以及和其他模式的界限

一、从一坨 if-else 说起:new 也是一种耦合

我们接着开篇的支付场景往下走。第一篇里已经用开闭原则把支付渠道抽象成了一个 Payment 接口,每个渠道是一个实现:

java 复制代码
public interface Payment {
    void pay(double amount);
}
class WechatPayment implements Payment { public void pay(double a){ /*...*/ } }
class AlipayPayment implements Payment { public void pay(double a){ /*...*/ } }
class UnionPayment  implements Payment { public void pay(double a){ /*...*/ } }

抽象是抽好了,但"到底该 new 哪个实现"这件事,还散落在业务代码里。订单服务里大概是这么写的:

java 复制代码
public class OrderService {
    public void pay(String payType, double amount) {
        Payment payment;
        // 根据类型,亲自 new 出对应的实现
        if (payType.equals("wechat")) {
            payment = new WechatPayment();
        } else if (payType.equals("alipay")) {
            payment = new AlipayPayment();
        } else if (payType.equals("unionpay")) {
            payment = new UnionPayment();
        } else {
            throw new IllegalArgumentException("不支持的支付方式");
        }
        payment.pay(amount);
    }
}

这段代码有个容易被忽视的问题:OrderService 本来只关心"发起支付"这个业务动作,现在却被迫认识了 WechatPaymentAlipayPaymentUnionPayment 每一个具体实现类。 面向接口编程喊了半天,结果在 new 这一步又把具体类给引进来了------因为 new 关键字后面跟的,永远是一个具体类,而不是接口 。你没法 new 一个接口。

这就带来两个坏处。其一,创建逻辑和业务逻辑搅在了一起 :哪天支付宝的构造需要多传一个参数、或者要先做个初始化,你得回 OrderService 里改。其二,这坨 new 的判断会到处复制 :除了下单,退款、查询、对账......只要哪里需要根据类型拿到 Payment,同样的 if-else 就得再抄一遍,改一处要改很多处。

问题的根源是:"创建一个对象"这件事本身,也是一种会变化、需要被隔离的职责。 而隔离职责的第一步,就是把它从业务里拎出来,交给一个专门的角色------工厂。

二、简单工厂:先把 new 收拢到一处

简单工厂(Simple Factory) 的想法非常朴素:既然到处都在根据类型 new 对象,那就把这段逻辑抽到一个专门的类里,谁要对象找它要,别自己 new 这个专门的类,就叫工厂。

java 复制代码
public class PaymentFactory {
    // 一个静态方法,输入类型,输出对应的 Payment
    public static Payment create(String payType) {
        switch (payType) {
            case "wechat":   return new WechatPayment();
            case "alipay":   return new AlipayPayment();
            case "unionpay": return new UnionPayment();
            default:
                throw new IllegalArgumentException("不支持的支付方式: " + payType);
        }
    }
}

于是 OrderService 一下子清爽了:

java 复制代码
public class OrderService {
    public void pay(String payType, double amount) {
        Payment payment = PaymentFactory.create(payType);  // 不再自己 new
        payment.pay(amount);
    }
}

变化很明显:OrderService 从此只认识 Payment 接口和 PaymentFactory,不再认识任何一个具体的支付实现类。 想知道微信支付怎么造、支付宝构造要不要传参,那是工厂内部的事,业务侧完全不用管。而且退款、对账那些地方,现在也都统一找 PaymentFactory.create() 拿对象,创建逻辑收敛到了唯一一处------要改构造方式,只改工厂。

这一步的收益是实打实的,所以简单工厂在实际项目里用得非常广,是最接地气的一种"模式"。它把"创建"和"使用"分了家,这正是所有工厂模式的共同内核。

一个命名提醒 :简单工厂也叫"静态工厂方法",因为它常用一个静态方法对外提供。它不属于 GoF 的二十三个正统设计模式------GoF 认为它更像一种编程习惯而非模式。但它是理解后面工厂方法、抽象工厂的必要台阶,所以务必先拿下它。

简单工厂看着已经不错了,但它有一个绕不过去的硬伤,而这个硬伤,恰恰踩在我们开篇立下的那条"总纲"上。

三、简单工厂的天花板:它违反了开闭原则

再回头盯着 PaymentFactory.create() 那个 switch。现在产品说,要新接入一个"数字人民币"支付。你得怎么做?

你必须回到 PaymentFactory 这个类里,撬开 create 方法,加一个 case:

java 复制代码
case "dcep": return new DcepPayment();   // ← 又得回来改这个方法

看出来了吗?这违反了开闭原则 ------每增加一种产品,都要去修改工厂这段已经写好、已经测试过、正在稳定运行的代码。第一篇讲开闭原则时批过的那个 if-else 毛病,其实并没有消失,只是从 OrderService 里搬到了 PaymentFactory 里。工厂把创建逻辑集中了,这是进步;但"集中"也意味着所有新增产品的修改压力,全压在了这一个 switch,它会越长越大,最终变成一个谁都不想碰、一碰就要回归测试所有支付渠道的雷区。

这就是简单工厂的天花板:它解决了"创建逻辑散落"的问题,却没解决"新增产品要改代码"的问题。 对于渠道基本稳定、极少新增的场景,简单工厂完全够用,别过度设计;但如果这个产品维度会频繁扩展,我们就需要一种能让"新增产品时不改任何旧代码"的结构。

顺着开闭原则的老思路想:要应对"新增"这种变化,就得把"变化的部分"做成可扩展的抽象。 简单工厂的问题在于,它把"决定造哪个产品"的逻辑,硬编码成了一个集中的 switch。那能不能把这个"决定权"本身也抽象出去、下放出去?这就是工厂方法的核心思路。

四、工厂方法:把"造什么"下放给子类

工厂方法模式(Factory Method) 的定义是:定义一个用于创建对象的接口(工厂接口),但把"到底实例化哪个类"这件事,推迟到子类去决定。 一句话------不再用一个大工厂配一个大 switch,而是每种产品配一个自己的小工厂。

具体怎么做?先把"工厂"也抽象成一个接口:

java 复制代码
// 抽象工厂接口:只声明"我能造一个 Payment",但不说造哪个
public interface PaymentFactory {
    Payment create();
}

然后,每一种支付渠道,都对应一个自己的工厂实现类,各自只负责造自己那一种产品:

java 复制代码
public class WechatPaymentFactory implements PaymentFactory {
    public Payment create() { return new WechatPayment(); }
}
public class AlipayPaymentFactory implements PaymentFactory {
    public Payment create() { return new AlipayPayment(); }
}
public class UnionPaymentFactory implements PaymentFactory {
    public Payment create() { return new UnionPayment(); }
}

OrderService 现在依赖的是工厂接口,由外部把具体用哪个工厂注入进来(这正是第一篇讲的依赖倒置):

java 复制代码
public class OrderService {
    private final PaymentFactory factory;   // 只依赖工厂接口
    public OrderService(PaymentFactory factory) { this.factory = factory; }

    public void pay(double amount) {
        Payment payment = factory.create();  // 造哪个,由注入的工厂决定
        payment.pay(amount);
    }
}

// 用的时候,决定注入哪个工厂
new OrderService(new WechatPaymentFactory());   // 这个订单走微信
new OrderService(new AlipayPaymentFactory());   // 那个订单走支付宝

关键的变化在这里:现在要新增"数字人民币",你要做的是------新建一个 DcepPayment 类,再新建一个 DcepPaymentFactory 类,然后就完了。 你没有修改任何一行已有的代码,PaymentFactory 接口没动、其他三个工厂没动、OrderService 没动。这就真正做到了"对扩展开放、对修改关闭"。

对比一下就看清了这次升级的本质:

  • 简单工厂 :一个工厂类,内部一个 switch 决定造什么。新增产品 → 那个 switch(违反开闭)。
  • 工厂方法 :一个工厂接口,每个产品配一个工厂子类。新增产品 → 一个产品类 + 一个工厂类(符合开闭)。

代价当然也有:类的数量变多了------每来一个产品,就多一个配套的工厂类。这是工厂方法的固有成本,后面第六节会专门说这笔账划不划算。

五、工厂方法的四个角色与骨架

把上面的代码抽象一下,工厂方法模式里其实有四个固定角色,认准它们,你在任何代码里都能一眼认出这个模式:

角色 本例中是谁 职责
抽象产品(Product) Payment 接口 定义产品的公共行为
具体产品(Concrete Product) WechatPayment 产品的具体实现,是被创建的目标
抽象工厂(Creator) PaymentFactory 接口 声明工厂方法 create(),返回抽象产品
具体工厂(Concrete Creator) WechatPaymentFactory 实现工厂方法,决定造哪个具体产品

它们的关系是两条平行的继承体系 :一边是产品家族(Payment → 各种 Payment),一边是工厂家族(PaymentFactory → 各种 Factory),而且一个具体工厂,精确对应一个具体产品。用一张图看这个"平行结构"最清楚:

这张图里最该记住的,就是那条"一一对应、并排扩展 "的关系:产品和工厂成对出现,新增时并排加一对新的,老的纹丝不动。这也解释了工厂方法名字的由来------那个 create() 就是所谓的"工厂方法",它被声明在抽象层、实现在子类,把"实例化哪个类"的决策权,从调用方转移给了子类

有一种常见变体值得一提:抽象工厂(Creator)不一定是纯接口,也可以是个抽象类 ,里面除了抽象的工厂方法,还能放一些所有工厂共用的逻辑。比如工厂方法造出产品后,统一记一条日志、做一次校验------这些公共动作写在抽象类里,子类只负责实现那个真正 new 对象的工厂方法。这种"模板 + 工厂方法"的组合在框架里很常见。

六、到底什么时候才值得上工厂

讲到这儿,必须泼一盆和开篇一脉相承的冷水:工厂方法不是越用越好,它是有成本的,滥用就是过度设计。

我们把这三层的适用场景摊开说清楚,你才好对号入座:

  • 什么都不做(直接 new) :如果一个对象就一种实现、创建也简单,那就老老实实 new,别整任何工厂。给一个只有一种实现的类套上工厂接口,是最典型的过度设计,除了让读代码的人多跳转几次,毫无收益。
  • 简单工厂 :当同一个接口有多种实现 ,且创建逻辑想收拢到一处、避免业务里到处 new 时,用简单工厂。它最轻量,代价只是"新增产品要改 switch"。如果你的产品种类基本稳定、极少新增(比如支付渠道其实一年也加不了一个),简单工厂就是性价比最高的选择,别硬上工厂方法。
  • 工厂方法 :当产品维度会频繁扩展 、你希望新增产品时完全不碰旧代码,或者创建过程本身比较复杂(要组装、要配置、要依赖别的对象),值得为每个产品配一个工厂时,才升级到工厂方法。它用"类数量增多"换来了"完美的开闭"。

一句话决策:先问这个对象值不值得用工厂(有没有多实现),再问值不值得用工厂方法(会不会频繁新增)。 两个问题都为"是",工厂方法才划算;第一个就为"否",连简单工厂都不用上。

这里再强调一遍第一篇的判断力问题:模式是用来解决真实存在的变化压力的。你得先看到"这个维度确实在不停加新东西"这个事实,才谈得上用工厂方法去接住它。为想象中根本不会发生的扩展预先铺一堆工厂类,和不用模式导致代码烂,是同样糟糕的两种极端。

七、现实身影,以及和其他模式的界限

工厂方法在 JDK 和主流框架里遍地都是,认出它们能帮你巩固理解:

  • Collection.iterator() :这是教科书级的工厂方法。Collection 是抽象工厂(Creator),iterator() 是工厂方法,Iterator 是抽象产品。ArrayListLinkedList 各自实现 iterator(),返回自己专属的迭代器实现------每个具体集合(具体工厂)造出自己专属的迭代器(具体产品),严丝合缝地对应工厂方法的结构。
  • java.util.Calendar.getInstance() :根据地区、时区返回不同的 Calendar 子类实例,是简单工厂(静态工厂方法)的典型。
  • 日志框架 LoggerFactory.getLogger()Spring 的 BeanFactory :名字里就带 Factory,思想都是"把对象的创建交给专门的工厂,使用方只面向抽象"。其中 Spring 的 BeanFactory 是整个容器的基石,它把"创建和管理 Bean"这件事做到了极致。

最后划清两条容易混淆的界限:

  • 工厂方法 vs 抽象工厂(下一篇) :工厂方法的一个工厂只造一种 产品(一个 create());抽象工厂的一个工厂要造一整族、多种 互相关联的产品(比如同时造 Order + Invoice + Shipment)。可以先记一句:工厂方法管"一个产品的多种实现",抽象工厂管"一族产品的整套搭配"。 下一篇细讲。
  • 工厂方法 vs 建造者(第 5 篇) :两者都在管"对象创建",但关注点不同。工厂方法关心的是"造哪一个 "(在多个类型里选一个);建造者关心的是"怎么一步步造好一个"(一个复杂对象的分步装配)。前者的产品通常是不同的类,后者通常是同一个类的复杂实例。

小结 。这一篇讲的是"创建对象"这件事该如何被治理。核心是一条清晰的演进线:直接 new 把具体类耦合进了业务;简单工厂 把创建收拢到一处、让业务只面向接口,但新增产品仍要改那个 switch,撞上了开闭原则的天花板;工厂方法 再进一步,把"决定造哪个"的决策权下放给一个个工厂子类,做到了新增产品时完全不改旧代码。记住那张"产品与工厂平行、成对扩展"的结构图,也记住那句冷水------先确认真有多实现、真会频繁扩展,工厂方法才值得上,否则简单工厂甚至直接 new 就够了 。下一篇我们把工厂再往上抬一层:当你要造的不是一个产品,而是一整族互相搭配的产品时,就轮到抽象工厂登场了。

相关推荐
啦啦啦啦啦zzzz7 小时前
设计模式:桥接模式和组合模式
c++·设计模式·组合模式·桥接模式
饼干哥哥1 天前
字节Seedance2.5终于上线,这次在收割谁?
人工智能·设计模式·前端框架
董员外1 天前
RAG 系统进化论(六):GraphRAG(基于知识图谱的 RAG),从相似文本走向实体关系
人工智能·后端·设计模式
鬼鬼鬼1 天前
从 Prompt 到 Harness:企业级 Agent 工程的完整演进之路
设计模式·架构·ai编程
啦啦啦啦啦zzzz1 天前
设计模式:原型模式
c++·设计模式·原型模式
董员外1 天前
RAG 系统进化论(五):Corrective RAG 与 Self-RAG,让系统发现并纠正错误
人工智能·后端·设计模式
workflower1 天前
高质量数据集的类型
人工智能·机器学习·设计模式·自然语言处理·机器人
啦啦啦啦啦zzzz2 天前
工具:动态类工厂和用配置文件存储属性
c++·设计模式·工具·动态工厂
Nontee2 天前
设计模式:模板方法与策略,从“每个字都认识“到能说清它们在干嘛
java·数据库·设计模式