工厂方法模式(Factory Method Pattern)

前言

简单工厂模式虽然初步实现了对象创建与使用的分离,但其内部臃肿的 if-else 逻辑却成为了系统扩展时的痛点,严重违背了开闭原则(OCP)

如何优雅地打破这种“上帝类”的束缚?工厂方法模式(Factory Method Pattern) 给出了标准答案:利用多态,将对象的实例化彻底延迟到子类中进行。

参考博客:工厂方法设计模式

一、核心定义

简单工厂模式中,所有的创建逻辑都集中在一个“上帝工厂类”里(充满了 if-elseswitch-case)。

痛点:一旦新增产品(比如新增华为手机),就必须修改这个工厂类的源代码,这严重违背了开闭原则(Open-Closed Principle, OCP)——对扩展开放,对修改封闭。

工厂方法模式通过“多态”解决了这个问题:==将对象的实例化推迟到子类中进行。==

核心思想是:定义一个创建对象的接口(抽象工厂),但让实现这个接口的类(具体工厂)来决定实例化哪个类(具体产品)。

核心思想:不直接 new 对象,而是通过工厂子类来决定创建哪种产品,实现创建与使用的解耦

作用 说明
解耦 客户端代码只依赖抽象接口,不依赖具体产品类,降低耦合度
开闭原则 新增产品只需新增子类工厂,不需要修改已有代码
单一职责 将产品创建逻辑集中在工厂类中,业务代码不关心对象如何创建
灵活扩展 支持运行时动态切换产品族,适应不同平台/环境的需求

适用场景

  • 不确定运行时需要哪种具体对象
  • 希望通过子类来扩展产品创建逻辑
  • 需要将创建逻辑与业务逻辑分离

二、标准体系结构图 (UML)

工厂方法模式包含四个核心角色:

  1. 抽象产品(Product):定义产品的接口规范。
  2. 具体产品(Concrete Product):实现抽象产品接口的具体类。
  3. 抽象工厂(Abstract Factory):声明返回抽象产品对象的工厂方法。
  4. 具体工厂(Concrete Factory):实现工厂方法,返回具体的实例。

三、 场景推演:手机代工厂

基础代码实现(以手机代工厂为例):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 1. 抽象产品:手机
public interface Phone {
void make();
}

// 2. 具体产品:苹果手机与小米手机
public class IPhone implements Phone {
@Override
public void make() { System.out.println("组装一台 iPhone"); }
}
public class MiPhone implements Phone {
@Override
public void make() { System.out.println("组装一台 小米手机"); }
}

// 3. 抽象工厂:手机制造标准
public interface PhoneFactory {
Phone createPhone(); // 将生产细节推迟到具体工厂
}

// 4. 具体工厂:专厂专办
public class AppleFactory implements PhoneFactory {
@Override
public Phone createPhone() { return new IPhone(); }
}
public class XiaomiFactory implements PhoneFactory {
@Override
public Phone createPhone() { return new MiPhone(); }
}

客户端调用:

1
2
3
4
5
6
7
8
9
10
11
public class Client {
public static void main(String[] args) {
// 想买小米手机,找小米专属工厂
PhoneFactory xiaomiFactory = new XiaomiFactory();
Phone myPhone = xiaomiFactory.createPhone();
myPhone.make();

// 【优势体现】:如果明天要造华为手机,只需新增 HuaweiPhone 和 HuaweiFactory 类,
// 原有代码一行都不用改,完美符合开闭原则!
}
}

四、实战案例:跨平台对话框按钮

4.1 需求分析

业务场景:开发一个跨平台的对话框组件,根据当前运行环境(Windows / Web)自动渲染不同风格的按钮:

  • Windows 环境:渲染原生 Swing 窗口按钮(带 GUI 面板、标签、退出按钮)
  • Web 环境:渲染 HTML 按钮(输出 <button> 标签)

痛点:如果用 if-else 在业务代码里判断环境并直接 new 不同的按钮对象,会导致:

  1. 客户端代码与所有具体按钮类强耦合
  2. 每新增一种平台按钮就要修改业务逻辑,违反开闭原则
  3. 创建逻辑散落在各处,难以维护

解决方案:使用工厂方法模式,将按钮的创建委托给各平台的工厂子类。

**实战代码:**https://github.com/likerhood/CodeDesignWork

运行结果:

image-20260415122620533

4.2 架构图

4.2.1 面条代码架构图

image-20260415213256031

4.2.2 工厂方法架构图

image-20260415213512542

4.3 类图对比

4.3.1 面条代码类图

4.3.2 工厂方法类图

4.4 时序图

4.4.2 面条代码类图

4.4.2 工厂方法类图

4.5 代码比较

4.5.1 工厂方法代码

代码结构

1
2
3
4
5
6
7
8
9
10
11
com.likerhood.design
├── buttons/ # 产品层
│ ├── IButton.java # 产品接口
│ └── imp/
│ ├── HtmlButtons.java # 具体产品 - HTML按钮
│ └── WindowsButtons.java # 具体产品 - Windows按钮
├── factory/ # 工厂层
│ ├── DialogFactory.java # 抽象工厂(含工厂方法)
│ └── imp/
│ ├── HtmlDialogFactory.java # 具体工厂 - 生产HTML按钮
│ └── WindowsDialogFactory.java # 具体工厂 - 生产Windows按钮

产品接口 IButton:定义按钮的统一行为契约

1
2
3
4
public interface IButton {
void render(); // 渲染按钮
void onClick(); // 点击事件
}

抽象工厂 DialogFactory:定义工厂方法 + 业务模板

1
2
3
4
5
6
7
8
9
10
public abstract class DialogFactory {
// 业务方法:渲染对话窗口(模板方法)
public void renderWindow() {
IButton okButton = createButton(); // 调用工厂方法
okButton.render();
}

// 工厂方法:由子类实现,决定创建哪种按钮
public abstract IButton createButton();
}

具体工厂:各自创建对应平台的按钮

1
2
3
4
5
6
7
8
9
10
11
12
13
// HTML 工厂
public class HtmlDialogFactory extends DialogFactory {
public IButton createButton() {
return new HtmlButtons();
}
}

// Windows 工厂
public class WindowsDialogFactory extends DialogFactory {
public IButton createButton() {
return new WindowsButtons();
}
}

客户端:运行时根据环境选择工厂

1
2
3
4
5
6
7
8
9
10
11
public static void config() {
if (System.getProperty("os.name").equals("Windows 11")) {
dialogFactory = new WindowsDialogFactory();
} else {
dialogFactory = new HtmlDialogFactory();
}
}

public static void runBusinessLogic() {
dialogFactory.renderWindow(); // 客户端只依赖抽象,不关心具体产品
}

4.5.2 面条代码(if-else 硬编码)

如果不用工厂方法模式,最直接的做法就是把所有创建逻辑和业务逻辑混在一起,用 if-else 判断环境后直接 new 具体对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Dialog {

public void renderWindow() {
String os = System.getProperty("os.name");

if (os.equals("Windows 11")) {
// 直接 new 具体产品类
WindowsButtons button = new WindowsButtons();
button.render();
} else {
// 直接 new 另一个具体产品类
HtmlButtons button = new HtmlButtons();
button.render();
}
}
}

客户端调用:

1
2
3
4
5
6
public class Client {
public static void main(String[] args) {
Dialog dialog = new Dialog();
dialog.renderWindow();
}
}

看起来代码更少,但问题巨大。

如果需求变化了呢?

假设现在要新增 Mac 按钮Linux 按钮,代码会变成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class Dialog {

public void renderWindow() {
String os = System.getProperty("os.name");

if (os.equals("Windows 11")) {
WindowsButtons button = new WindowsButtons();
button.render();
} else if (os.equals("Mac OS X")) {
MacButtons button = new MacButtons(); // 改动1
button.render();
} else if (os.equals("Linux")) {
LinuxButtons button = new LinuxButtons(); // 改动2
button.render();
} else {
HtmlButtons button = new HtmlButtons();
button.render();
}
}

// 假设还有另一个业务方法也需要按钮...
public void showAlert() {
String os = System.getProperty("os.name");

// 又要重复一遍完全相同的 if-else!
if (os.equals("Windows 11")) {
WindowsButtons button = new WindowsButtons();
button.onClick();
} else if (os.equals("Mac OS X")) {
MacButtons button = new MacButtons();
button.onClick();
} else if (os.equals("Linux")) {
LinuxButtons button = new LinuxButtons();
button.onClick();
} else {
HtmlButtons button = new HtmlButtons();
button.onClick();
}
}
}

每新增一个平台,所有用到按钮的地方都要改,if-else 到处重复。


总结

角色 本案例对应类 职责
抽象产品 IButton 定义产品的统一接口
具体产品 HtmlButtons / WindowsButtons 实现不同平台的按钮逻辑
抽象工厂 DialogFactory 声明工厂方法 + 封装业务模板
具体工厂 HtmlDialogFactory / WindowsDialogFactory 实现工厂方法,创建对应产品
客户端 DialogFactoryTest 选择工厂,调用业务方法

工厂方法模式的关键点

  • DialogFactory.renderWindow() 中调用了抽象方法 createButton(),具体创建哪种按钮由子类工厂决定——这就是”将实例化延迟到子类“的精髓。

  • 客户端只与 DialogFactoryIButton 两个抽象打交道,完全不感知具体产品类的存在,实现了真正的面向接口编程