Featured image of post 设计模式漫谈:四种「工厂」到底有什么区别

设计模式漫谈:四种「工厂」到底有什么区别

简单工厂、静态工厂、工厂方法、抽象工厂——一篇讲清四者的边界、关系与工程取舍

很多人聊"工厂模式"时会下意识地说"工厂模式嘛,就是 XxxFactory.create()"——可一旦有人追问"那简单工厂、静态工厂、工厂方法、抽象工厂到底有啥区别?",往往就答不上来了。

这四个词长得像,但实际上只有两个属于 GoF 23 种正统设计模式,剩下两个是工程实践里大家叫顺嘴的"非正式说法"。今天我们就把它们一次性捋清楚。


先从一个让人头大的例子说起

假设我们要做一个发送通知的功能,有三种渠道:短信、邮件、推送。最直觉的写法是:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class OrderService {
    public void notifyUser(String channel, String userId, String content) {
        if ("sms".equals(channel)) {
            SmsSender sender = new SmsSender("ak", "sk");
            sender.send(userId, content);
        } else if ("email".equals(channel)) {
            EmailSender sender = new EmailSender("smtp.xx.com", 25);
            sender.send(userId, content);
        } else if ("push".equals(channel)) {
            PushSender sender = new PushSender("appKey");
            sender.send(userId, content);
        }
    }
}

这种写法的麻烦之处在于:

  1. 创建逻辑散落各处:哪天 SmsSender 的构造参数变了,所有 new SmsSender(...) 的地方都要改。
  2. 业务和"造对象"耦合OrderService 本不该知道 SmsSenderak/sk 怎么填。
  3. 新增渠道要改老代码:再来一个微信渠道,又得加一个 else if,老代码越改越脏,违反开闭原则(OCP)。
  4. 不好测:业务方法里硬编码了 new,单元测试想替换成 mock 都难。

工厂模式要解决的就是这些痛点——把"用什么对象"和"对象怎么造"拆开


一、简单工厂(Simple Factory)

它是什么

把"造对象"的逻辑收拢到一个工厂类的一个方法里,调用方只传"类型标识",不关心具体实现。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class NotificationSenderFactory {

    public static Sender create(String channel) {
        switch (channel) {
            case "sms":   return new SmsSender("ak", "sk");
            case "email": return new EmailSender("smtp.xx.com", 25);
            case "push":  return new PushSender("appKey");
            default:      throw new IllegalArgumentException("unknown channel: " + channel);
        }
    }
}

业务代码立刻清爽:

1
2
Sender sender = NotificationSenderFactory.create(channel);
sender.send(userId, content);

它的优缺点

优点

  • 创建逻辑集中到一个地方,构造参数变了只改一处。
  • 业务代码只面向 Sender 接口编程,不依赖具体实现。

缺点

  • 新增类型要改 create 方法——违反开闭原则。
  • 工厂方法里塞满 switch,类型多了之后会很臃肿。

一个常见的误解

很多书会把"简单工厂"列成一种设计模式,但 GoF 23 种里其实没有它。它更像是一种"惯用写法",是迈向真正工厂模式的过渡形态。


二、静态工厂方法(Static Factory Method)

“静态工厂"这个词在中文社区有点歧义,常见有两层含义:

  1. 狭义:上面简单工厂里那个方法本身是 static 的,所以也叫静态工厂——这层含义没什么新东西。
  2. 广义(更值得讲的一层)Effective Java 第 1 条提倡的——用类内部的静态工厂方法替代 public 构造器

第二种含义长什么样

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public final class Color {

    private final int r, g, b;

    private Color(int r, int g, int b) {   // 构造器私有
        this.r = r; this.g = g; this.b = b;
    }

    public static Color of(int r, int g, int b) {
        return new Color(r, g, b);
    }

    public static Color fromHex(String hex) {
        int rgb = Integer.parseInt(hex.substring(1), 16);
        return new Color((rgb >> 16) & 0xFF, (rgb >> 8) & 0xFF, rgb & 0xFF);
    }

    public static Color black() {
        return BLACK;   // 可以缓存常用实例
    }
    private static final Color BLACK = new Color(0, 0, 0);
}

JDK 里到处都是这个套路:

  • Integer.valueOf(int):缓存了 -128~127 的实例
  • List.of(...) / Map.of(...):返回不可变实现
  • Optional.of(x) / Optional.empty()
  • Files.newInputStream(path):返回 InputStream 接口,实现类对外不可见

为什么推荐它

相比 new 构造器,静态工厂方法有四个真正有价值的优点:

  1. 方法可以有名字——Color.fromHex("#FF0000")new Color(255, 0, 0) 表达力强得多。一个类想要"多种构造方式"时,构造器只能靠参数列表区分,名字不够用。
  2. 不必每次都创建新对象——可以缓存、可以复用。Integer.valueOf 和单例都是这个原理。
  3. 可以返回子类型甚至接口类型——隐藏具体实现,调用方只面向接口编程。Files.newInputStream 就是个例子。
  4. 返回类型可以根据参数动态决定——EnumSet.of(...) 会根据元素数量返回 RegularEnumSetJumboEnumSet,调用方不感知。

它的代价

  • 类如果没有 public 构造器,就不能被子类继承——这其实有时是好事(强制组合优于继承)。
  • 静态工厂方法和普通静态方法在 IDE 里不易区分,所以业界约定了一组命名习惯:of / valueOf / from / getInstance / newInstance / create。看到这些名字,老司机就知道这是个工厂。

三、工厂方法(Factory Method)—— GoF 正统

简单工厂的最大问题是:新增类型要改 switch工厂方法就是为了解决这个问题——把工厂本身也抽象化

类关系

每种产品配一个工厂,工厂自己也面向接口编程。

代码长什么样

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public interface Sender {
    void send(String userId, String content);
}

public interface SenderFactory {
    Sender create();
}

public class SmsSenderFactory implements SenderFactory {
    @Override public Sender create() {
        return new SmsSender("ak", "sk");
    }
}

public class EmailSenderFactory implements SenderFactory {
    @Override public Sender create() {
        return new EmailSender("smtp.xx.com", 25);
    }
}

调用方持有一个 SenderFactory,至于背后是哪种工厂,由配置或 IoC 容器注入:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class OrderService {
    private final SenderFactory factory;          // 依赖抽象

    public OrderService(SenderFactory factory) {
        this.factory = factory;
    }

    public void notifyUser(String userId, String content) {
        Sender sender = factory.create();
        sender.send(userId, content);
    }
}

关键收益

新增一种"微信渠道"时:

  • 加一个 WeChatSender 实现类
  • 加一个 WeChatSenderFactory 实现类
  • 老代码一行不动

这就是开闭原则的真正落地。代价是类的数量翻倍,所以只在"扩展性收益 > 类爆炸成本"时才值得用。


四、抽象工厂(Abstract Factory)—— GoF 正统

工厂方法解决的是"一种产品有多种实现”。当一个场景里同时有多种产品,且这些产品需要"配套使用"时,就需要抽象工厂。

经典场景:跨平台 UI

假设我们做一个跨平台 GUI 库,要支持 Windows 和 Mac,每个平台都有自己的 ButtonCheckbox

产品 \ 平台WindowsMac
ButtonWindowsButtonMacButton
CheckboxWindowsCheckboxMacCheckbox

约束:Windows 程序里不能混入 Mac 风格的控件,反之亦然。

类关系如下:

抽象工厂就是把"造一整套"的能力封装到一个工厂里:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public interface UIFactory {
    Button   createButton();
    Checkbox createCheckbox();
}

public class WindowsUIFactory implements UIFactory {
    @Override public Button   createButton()   { return new WindowsButton(); }
    @Override public Checkbox createCheckbox() { return new WindowsCheckbox(); }
}

public class MacUIFactory implements UIFactory {
    @Override public Button   createButton()   { return new MacButton(); }
    @Override public Checkbox createCheckbox() { return new MacCheckbox(); }
}

调用方只拿 UIFactory,造出来的所有控件天然属于同一族:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class App {
    private final UIFactory ui;

    public App(UIFactory ui) { this.ui = ui; }

    public void render() {
        Button b   = ui.createButton();      // 一定是同一个平台
        Checkbox c = ui.createCheckbox();
        // ...
    }
}

抽象工厂 vs 工厂方法

最容易混淆的就是这两兄弟,记住一句话就够:

工厂方法关心"一种产品的多种实现",抽象工厂关心"多种产品的成套实现"。

抽象工厂的痛点是——产品族里加一个新成员(比如要加个 Slider)时,所有具体工厂都得改。这是它结构性的代价。


横向对比

模式是否 GoF解决的问题新增类型成本典型场景
简单工厂创建逻辑收拢改工厂方法(违反 OCP)类型少、且不常变的场景
静态工厂方法替代构造器加一个静态方法几乎所有库的对象创建
工厂方法把"造哪个"也抽象掉新增产品类 + 新增工厂类(OCP)插件化、可扩展的运行时切换
抽象工厂一族产品配套使用新增成员要改所有工厂跨平台、多环境、多套配套实现

工程上怎么选

不要看见"工厂"就上工厂方法/抽象工厂。多数业务场景里,越简单越好

  • 单纯想让对象创建更优雅、更有表达力:静态工厂方法(offrom)就够了,不需要任何工厂类。
  • 类型种类有限、变化频率低:简单工厂足够。承担违反 OCP 的代价换简洁,是合理的工程取舍。
  • 类型会持续扩展、且希望"加新类型不动老代码":用工厂方法,配合 IoC 容器或 SPI 机制。
  • 要支持"多套互不相容的实现族":才考虑抽象工厂。它的复杂度不低,没真实需求别上。

一个常被忽略的现实是——Spring/Guice 这类 IoC 容器,本身就是一个超级抽象工厂。在用 Spring 的项目里,绝大多数"工厂模式"的需求其实可以通过 @Bean + @QualifierMap<String, XxxStrategy> 注入直接解决,不必手写工厂类。


小结

把四个工厂的本质用一句话压缩:

工厂模式的本质,是把"用什么"和"怎么造"拆开。

四种形态只是抽象程度不同:

  • 简单工厂:拆得最浅,调用方还要自己传"类型标识"。
  • 静态工厂方法:把"造"封装进类自己,对外只暴露语义化的命名。
  • 工厂方法:把"造哪个"也抽象成接口,新增类型不再修改老代码。
  • 抽象工厂:把"造一整套"抽象成接口,保证产品族内部一致性。

工程上永远从最简单的开始,业务复杂度真实驱动你时再向上升级——这才是设计模式正确的用法。

使用 Hugo 构建
主题 StackJimmy 设计