工厂模式
从 new 说起
上一节的策略模式解决了「行为的灵活切换」问题,但有一个细节被轻轻带过——对象是怎么创建的?回顾 MallardDuck 的构造器:
1 2 3 4 | public MallardDuck() { flyBehavior = new FlyWithWings(); // 直接 new 具体类 quackBehavior = new Quack(); } |
这里的 new FlyWithWings() 就是一个硬编码的创建决策。在这个简单场景中没什么问题,但当创建逻辑变得复杂——需要根据参数、配置或运行环境来决定创建哪种对象时,散落在各处的 new 就成了维护噩梦。
本节探讨的就是:如何将对象的创建从使用中分离出来。这一思路催生了三种递进的工厂模式——简单工厂、工厂方法和抽象工厂。
简单工厂模式
问题场景
考虑一个支付系统,需要根据用户选择的支付方式执行不同的支付逻辑。最直觉的写法:
1 2 3 4 5 6 7 8 9 10 11 | public void pay(String type) { if (type.equalsIgnoreCase("cash")) { // 现金支付处理代码 } else if (type.equalsIgnoreCase("creditcard")) { // 信用卡支付处理代码 } else if (type.equalsIgnoreCase("voucher")) { // 代金券支付处理代码 } else { // ... } } |
这段代码有两个严重问题:一是所有支付逻辑纠缠在一个方法中,每增加一种支付方式就要修改这个方法;二是创建逻辑和业务逻辑混在一起,难以独立修改。
重构:分离创建与使用
第一步,将不同的支付方式抽象为独立的类:
1 2 3 4 5 6 7 8 9 10 11 12 13 | // 抽象产品 public abstract class AbstractPay { public abstract void pay(); } // 具体产品 public class CashPay extends AbstractPay { public void pay() { /* 现金支付处理 */ } } public class CreditcardPay extends AbstractPay { public void pay() { /* 信用卡支付处理 */ } } |
第二步,用一个专门的工厂类来负责创建:
1 2 3 4 5 6 7 8 9 10 11 | public class PayMethodFactory { public static AbstractPay getPayMethod(String type) { if (type.equalsIgnoreCase("cash")) { return new CashPay(); } else if (type.equalsIgnoreCase("creditcard")) { return new CreditcardPay(); } // ... return null; } } |
客户端只需调用 PayMethodFactory.getPayMethod("cash") 即可获得所需的支付对象,不需要知道具体类的名字,也不需要了解创建细节。这就是简单工厂模式的核心思想——将对象的创建和对象的使用分离。
模式定义
简单工厂模式
简单工厂模式(Simple Factory Pattern)又称静态工厂方法(Static Factory Method)模式,属于类创建型模式。定义一个专门的工厂类,根据传入的参数返回不同类的实例,被创建的实例通常具有共同的父类。
结构
classDiagram
class Product {
<<abstract>>
}
class ConcreteProductA
class ConcreteProductB
class Factory {
+factoryMethod(String arg)$ Product
}
Product <|-- ConcreteProductA
Product <|-- ConcreteProductB
Factory ..> ConcreteProductA : creates
Factory ..> ConcreteProductB : creates
三个角色各司其职:
- Factory(工厂):包含创建逻辑的核心类,提供静态工厂方法,根据参数决定创建哪种具体产品
- Product(抽象产品):所有具体产品的公共父类或接口,定义产品的通用接口
- ConcreteProduct(具体产品):工厂实际创建的对象,每个具体产品实现自己的业务逻辑
要点分析
简单工厂模式的设计决策有几个值得注意的地方。
工厂方法是静态方法,客户端通过类名直接调用(PayMethodFactory.getPayMethod("cash")),使用方便。在实际开发中,传入的参数还可以保存在 XML 配置文件中,这样修改支付方式时无须修改任何 Java 源代码——只需改配置文件:
1 2 3 4 | <?xml version="1.0"?> <config> <className>CashPay</className> </config> |
简单工厂模式的核心要点是:当你需要什么,只需要传入一个正确的参数,就可以获取你所需要的对象,而无须知道其创建细节。
实例:OA 系统权限管理
某 OA 系统根据用户登录时的账号密码进行身份验证,验证通过后取出用户的权限等级(以整数形式存储),根据不同等级创建不同的用户对象(普通用户、管理者、管理员),不同等级拥有不同的操作权限。
classDiagram
direction LR
class Employee {
<<abstract>>
+diffOperation()* void
}
class User {
+sameOperation() void
+diffOperation() void
}
class Manager {
+diffOperation() void
}
class Administrator {
+diffOperation() void
}
class UserFactory {
+getUser(int permission)$ User
}
Employee <|-- User
Employee <|-- Manager
Employee <|-- Administrator
UserFactory ..> Employee : creates
UserFactory 根据权限等级参数创建不同的用户对象,客户端只需传入权限整数,无需知道具体有哪些用户类。
JDK 中的简单工厂
简单工厂模式在 JDK 中随处可见:
1 2 3 4 5 6 | // java.text.DateFormat —— 根据参数返回不同风格的日期格式化器 DateFormat fmt = DateFormat.getDateInstance(DateFormat.SHORT); // java.security —— 根据算法名称返回对应的密钥生成器和密码器 KeyGenerator keyGen = KeyGenerator.getInstance("DESede"); Cipher cipher = Cipher.getInstance("DESede"); |
这些静态方法都是典型的简单工厂:传入一个参数(风格、算法名等),返回一个具体子类的实例。
优缺点
| 维度 | 内容 |
|---|---|
| 优点 | 实现了创建与使用的责任分割;客户端无须知道具体产品类名,只需知道参数;结合配置文件可在不修改代码的情况下更换产品 |
| 缺点 | 工厂类集中了所有创建逻辑,是系统的单点故障;增加新产品必须修改工厂方法,违反开闭原则;静态方法无法通过继承形成等级结构 |
简单工厂模式最大的硬伤是违反开闭原则——每添加一种新产品,就必须修改工厂类的条件判断逻辑。当产品类型较多时,工厂逻辑会变得臃肿不堪。
适用场景
- 工厂负责创建的对象种类较少,不会导致工厂逻辑过于复杂
- 客户端只知道传入的参数,不关心创建细节
简化变体
在某些情况下,可以省去独立的 Factory 类,将静态工厂方法直接写在抽象产品类中——抽象产品同时扮演自己子类的工厂:
classDiagram
direction LR
class Product {
+factoryMethod(String arg)$ Product
}
class ConcreteProductA
class ConcreteProductB
Product <|-- ConcreteProductA
Product <|-- ConcreteProductB
这种简化减少了一个类,但代价是产品类同时承担了创建职责,适用于结构简单的场景。
工厂方法模式
简单工厂的瓶颈
回到按钮工厂的场景。简单工厂可以根据参数返回圆形按钮、矩形按钮、菱形按钮等。但如果要增加一种椭圆形按钮,除了新建 EllipseButton 类,还必须打开工厂类修改其 if-else 逻辑。每次扩展都要修改已有代码——这正是开闭原则所禁止的。
问题的根源在于:简单工厂用一个类集中了所有产品的创建,工厂与每一种具体产品都耦合在一起。
解决思路很自然:不再用一个工厂类统管所有创建,而是为每种产品提供一个专门的工厂。先定义一个抽象的按钮工厂类,再为每种按钮各定义一个具体工厂子类。当出现新的按钮类型时,只需增加一个新的具体工厂类——无须修改任何已有代码。
模式定义
工厂方法模式
工厂方法模式(Factory Method Pattern)定义创建产品对象的接口,但将实际的实例化延迟到工厂子类中完成。工厂父类负责定义创建产品对象的公共接口,工厂子类负责生成具体的产品对象。
别名:虚拟构造器(Virtual Constructor)、多态工厂(Polymorphic Factory)
工厂方法模式是简单工厂模式的进一步抽象和推广。由于使用了面向对象的多态性,它保持了简单工厂的优点,同时克服了其违反开闭原则的缺点。
结构
classDiagram
direction LR
class Product {
<<abstract>>
}
class ConcreteProduct
class Factory {
<<abstract>>
+factoryMethod()* Product
}
class ConcreteFactory {
+factoryMethod() Product
}
Product <|-- ConcreteProduct
Factory <|-- ConcreteFactory
ConcreteFactory ..> ConcreteProduct : creates
四个角色:
- Product(抽象产品):定义产品的通用接口
- ConcreteProduct(具体产品):实现抽象产品接口的具体类
- Factory(抽象工厂):声明工厂方法,返回类型为抽象产品
- ConcreteFactory(具体工厂):实现工厂方法,返回具体产品实例
与简单工厂的关键区别:工厂本身也有了抽象层。核心工厂类不再负责所有产品的创建,仅仅给出具体工厂必须实现的接口,具体创建工作交给子类。
支付案例的演进
用工厂方法模式继续重构支付系统:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | // 抽象工厂 public abstract class PayMethodFactory { public abstract AbstractPay getPayMethod(); } // 具体工厂——每种支付方式一个工厂 public class CashPayFactory extends PayMethodFactory { public AbstractPay getPayMethod() { return new CashPay(); } } public class CreditcardPayFactory extends PayMethodFactory { public AbstractPay getPayMethod() { return new CreditcardPay(); } } |
客户端代码:
1 2 3 4 5 6 | PayMethodFactory factory; AbstractPay payMethod; factory = new CashPayFactory(); // 仍有具体类名 payMethod = factory.getPayMethod(); payMethod.pay(); |
注意这里客户端代码中仍然出现了具体的工厂类名 CashPayFactory。为了彻底消除这一依赖,可以引入配置文件 + 反射机制。
配置文件与反射
在实际开发中,不直接用 new 创建工厂对象,而是将具体工厂类的类名写入 XML 配置文件,通过 Java 反射机制动态创建对象。
1 2 3 4 | <?xml version="1.0"?> <config> <className>CashPayFactory</className> </config> |
工具类 XMLUtil 负责读取配置并通过反射创建对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | public class XMLUtil { public static Object getBean() { // 1. 解析 XML 配置文件 DocumentBuilderFactory dFactory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = dFactory.newDocumentBuilder(); Document doc = builder.parse(new File("config.xml")); // 2. 获取类名 NodeList nl = doc.getElementsByTagName("className"); Node classNode = nl.item(0).getFirstChild(); String cName = classNode.getNodeValue(); // 3. 通过反射创建实例 Class c = Class.forName(cName); Object obj = c.newInstance(); return obj; } } |
Java 反射
反射(Reflection)是指在程序运行时获取已知名称的类或已有对象的相关信息的机制,包括类的方法、属性、超类等信息,还包括实例的创建和类型判断。核心 API 是 Class.forName(className) 获取 Class 对象,再调用 newInstance() 创建实例——即通过一个类名字符串得到类的实例。
修改后的客户端代码彻底消除了对具体工厂类的依赖:
1 2 3 4 5 6 7 | PayMethodFactory factory; AbstractPay payMethod; // 从配置文件读取工厂类名,通过反射创建 factory = (PayMethodFactory) XMLUtil.getBean(); payMethod = factory.getPayMethod(); payMethod.pay(); |
现在要更换支付方式,只需修改 config.xml 中的类名——整个 Java 源代码无须任何改动。这才是工厂方法模式在实际工程中的完整形态。
实例:日志记录器
某系统需要支持多种日志记录方式(文件记录、数据库记录等),且用户可以根据需求动态选择。
classDiagram
class Log {
<<abstract>>
+writeLog()* void
}
class FileLog {
+writeLog() void
}
class DatabaseLog {
+writeLog() void
}
class LogFactory {
<<abstract>>
+createLog()* Log
}
class FileLogFactory {
+createLog() Log
}
class DatabaseLogFactory {
+createLog() Log
}
Log <|-- FileLog
Log <|-- DatabaseLog
LogFactory <|-- FileLogFactory
LogFactory <|-- DatabaseLogFactory
FileLogFactory ..> FileLog : creates
DatabaseLogFactory ..> DatabaseLog : creates
如果需要新增一种日志记录方式(比如发送到远程服务器),只需新建 RemoteLog 和 RemoteLogFactory 两个类,无须修改任何已有代码。这正是工厂方法模式符合开闭原则的体现。
优缺点
优点:
- 客户端无须知道具体产品类名,只需关心所需产品对应的工厂
- 基于工厂和产品的多态性设计是工厂方法模式的关键——所有具体工厂类都有同一抽象父类
- 新增产品时无须修改任何已有代码(抽象工厂、抽象产品、客户端、其他具体工厂和产品),只需添加一个具体工厂和具体产品,完全符合开闭原则
缺点:
- 每增加一种产品,就要增加一个具体产品类和一个具体工厂类,类的个数成对增加,增加了系统复杂度和编译运行开销
- 引入抽象层增加了系统的抽象性和理解难度,实现时可能需要用到 DOM 解析和反射等技术
工厂方法模式退化后可以演变成简单工厂模式——当抽象工厂与具体工厂合并为一个类,并将工厂方法设计为静态方法时,就回到了简单工厂。
适用场景
- 客户端不知道它所需要的对象的类,只需知道对应的工厂
- 希望通过子类来指定创建哪个对象,利用多态性和里氏代换原则实现扩展
- 将创建任务委托给多个工厂子类之一,具体工厂类的类名可存储在配置文件或数据库中
抽象工厂模式
从一个产品到一族产品
工厂方法模式中,每个具体工厂只负责创建一种产品。但现实中,一个工厂往往需要生产一整族相关产品。比如海尔工厂既生产海尔电视机,也生产海尔空调——它们属于不同的产品类型,但属于同一个「品牌家族」。
为了理解这一需求,需要先分清两个概念:
产品等级结构与产品族
产品等级结构(Product Hierarchy)是产品的继承结构。例如抽象类「电视机」及其子类「海尔电视机」「TCL 电视机」构成一个产品等级结构。
产品族(Product Family)是同一个工厂生产的、位于不同产品等级结构中的一组产品。例如海尔工厂生产的「海尔电视机 + 海尔空调」就是一个产品族——电视机和空调分属不同的产品等级结构,但都出自海尔。
用一个二维矩阵来直观理解:
| Button | Text | |
|---|---|---|
| Windows | WindowsButton | WindowsText |
| Unix | UnixButton | UnixText |
| Linux | LinuxButton | LinuxText |
横轴是产品等级结构(Button 和 Text 各自构成一个继承结构),纵轴是产品族(同一行的产品由同一个工厂生产、被设计为一起使用)。
工厂方法模式针对的是一列(一个产品等级结构),而抽象工厂模式针对的是一行(一个产品族)。当系统需要的不是单一产品,而是多个位于不同产品等级结构中属于同一产品族的产品时,就需要抽象工厂模式。
模式定义
抽象工厂模式
抽象工厂模式(Abstract Factory Pattern)提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。
Provide an interface for creating families of related or dependent objects without specifying their concrete classes.
别名:Kit。使用频率:高。
抽象工厂模式是所有形式的工厂模式中最为抽象和最具一般性的一种形态。
结构
classDiagram
class AbstractFactory {
<<abstract>>
+createProductA()* AbstractProductA
+createProductB()* AbstractProductB
}
class ConcreteFactory1 {
+createProductA() AbstractProductA
+createProductB() AbstractProductB
}
class ConcreteFactory2 {
+createProductA() AbstractProductA
+createProductB() AbstractProductB
}
class AbstractProductA {
<<abstract>>
}
class ConcreteProductA1
class ConcreteProductA2
class AbstractProductB {
<<abstract>>
}
class ConcreteProductB1
class ConcreteProductB2
AbstractFactory <|-- ConcreteFactory1
AbstractFactory <|-- ConcreteFactory2
AbstractProductA <|-- ConcreteProductA1
AbstractProductA <|-- ConcreteProductA2
AbstractProductB <|-- ConcreteProductB1
AbstractProductB <|-- ConcreteProductB2
ConcreteFactory1 ..> ConcreteProductA1 : creates
ConcreteFactory1 ..> ConcreteProductB1 : creates
ConcreteFactory2 ..> ConcreteProductA2 : creates
ConcreteFactory2 ..> ConcreteProductB2 : creates
四个角色:
- AbstractFactory(抽象工厂):声明一组创建产品的方法,每个方法对应一个产品等级结构
- ConcreteFactory(具体工厂):实现所有创建方法,生产同一产品族的全部产品
- AbstractProduct(抽象产品):每个产品等级结构的公共接口
- ConcreteProduct(具体产品):由具体工厂创建的具体产品对象
注意与工厂方法模式的关键区别:抽象工厂中有多个创建方法(createProductA()、createProductB()),每个具体工厂负责创建一整族产品。
代码示例
抽象工厂类包含多个创建方法,每个方法对应一种产品:
1 2 3 4 | public abstract class AbstractFactory { public abstract AbstractProductA createProductA(); public abstract AbstractProductB createProductB(); } |
具体工厂类实现所有创建方法,确保同一工厂创建的产品属于同一产品族:
1 2 3 4 5 6 7 8 | public class ConcreteFactory1 extends AbstractFactory { public AbstractProductA createProductA() { return new ConcreteProductA1(); // 产品族 1 的 A 产品 } public AbstractProductB createProductB() { return new ConcreteProductB1(); // 产品族 1 的 B 产品 } } |
实例一:电器工厂
海尔工厂生产海尔电视机和海尔空调,TCL 工厂生产 TCL 电视机和 TCL 空调。同品牌的电器构成一个产品族,同类型的电器构成一个产品等级结构。
classDiagram
class EFactory {
<<abstract>>
+produceTelevision()* Television
+produceAirConditioner()* AirConditioner
}
class HaierFactory {
+produceTelevision() Television
+produceAirConditioner() AirConditioner
}
class TCLFactory {
+produceTelevision() Television
+produceAirConditioner() AirConditioner
}
class Television {
<<abstract>>
+play()* void
}
class HaierTelevision {
+play() void
}
class TCLTelevision {
+play() void
}
class AirConditioner {
<<abstract>>
+changeTemperature()* void
}
class HaierAirConditioner {
+changeTemperature() void
}
class TCLAirConditioner {
+changeTemperature() void
}
EFactory <|-- HaierFactory
EFactory <|-- TCLFactory
Television <|-- HaierTelevision
Television <|-- TCLTelevision
AirConditioner <|-- HaierAirConditioner
AirConditioner <|-- TCLAirConditioner
HaierFactory ..> HaierTelevision : creates
HaierFactory ..> HaierAirConditioner : creates
TCLFactory ..> TCLTelevision : creates
TCLFactory ..> TCLAirConditioner : creates
选择 HaierFactory 就能获得海尔全系列产品,选择 TCLFactory 就能获得 TCL 全系列产品——抽象工厂模式保证了客户端始终使用同一产品族中的对象,不会出现「海尔电视 + TCL 空调」这样的混搭。
实例二:数据库操作工厂
某系统自定义了数据库连接对象 Connection 和语句对象 Statement,可针对不同数据库(Oracle、MySQL)提供不同的实现。
classDiagram
class DBFactory {
<<abstract>>
+createConnection()* Connection
+createStatement()* Statement
}
class OracleFactory {
+createConnection() Connection
+createStatement() Statement
}
class MySQLFactory {
+createConnection() Connection
+createStatement() Statement
}
class Connection {
<<abstract>>
}
class OracleConnection
class MySQLConnection
class Statement {
<<abstract>>
}
class OracleStatement
class MySQLStatement
DBFactory <|-- OracleFactory
DBFactory <|-- MySQLFactory
Connection <|-- OracleConnection
Connection <|-- MySQLConnection
Statement <|-- OracleStatement
Statement <|-- MySQLStatement
OracleFactory ..> OracleConnection : creates
OracleFactory ..> OracleStatement : creates
MySQLFactory ..> MySQLConnection : creates
MySQLFactory ..> MySQLStatement : creates
通过配置文件切换数据库类型时,只需更换具体工厂,Connection 和 Statement 会自动匹配——不会出现 Oracle 连接搭配 MySQL 语句的错误组合。
优缺点
优点:
- 隔离具体类的生成:客户端不需要知道什么被创建,更换一个具体工厂就可以改变整个系统的行为
- 保证产品族的一致性:当一个产品族中的多个对象被设计为一起工作时,抽象工厂能保证客户端始终只使用同一产品族中的对象
- 增加新的产品族很方便:增加一个具体工厂和一组具体产品即可,无须修改已有系统,符合开闭原则
缺点:
- 增加新的产品等级结构困难:如果要在电器工厂中新增「电冰箱」这种产品类型,就必须修改抽象工厂接口(添加
createRefrigerator()方法),并修改所有的具体工厂子类——这严重违反了开闭原则
开闭原则的倾斜性
抽象工厂模式以一种倾斜的方式支持开闭原则:
- 增加产品族(新增一行):容易。只需新增一个具体工厂类和对应的具体产品类,不影响已有代码
- 增加产品等级结构(新增一列):困难。需要修改抽象工厂接口及所有具体工厂的实现
在使用抽象工厂模式时,需要事先规划好产品等级结构,因为后续增加新的产品类型代价很高。
适用场景
- 系统不应依赖于产品如何被创建、组合和表达的细节
- 系统中有多于一个产品族,而每次只使用其中某一产品族
- 属于同一产品族的产品被设计为一起使用,系统设计中需要体现这一约束
- 所有产品以同样的接口出现,客户端不依赖于具体实现
三种工厂模式的关系
三种工厂模式并非各自独立,而是一条渐进式抽象的演化链:
flowchart LR
A["简单工厂"] -->|"工厂也抽象化"| B["工厂方法"]
B -->|"工厂生产多种产品"| C["抽象工厂"]
C -->|"只剩一个产品等级结构"| B
B -->|"抽象工厂与具体工厂合并<br>工厂方法变为静态"| A
classDef simple fill:#fff3e0,stroke:#ef6c00,stroke-width:2px
classDef method fill:#e8f5e8,stroke:#2e7d32,stroke-width:2px
classDef abstr fill:#e3f2fd,stroke:#1565c0,stroke-width:2px
class A simple
class B method
class C abstr
退化关系:当抽象工厂模式中每个具体工厂类只创建一种产品(只有一个产品等级结构)时,它退化为工厂方法模式;当工厂方法模式中抽象工厂与具体工厂合并为一个类,且工厂方法变为静态方法时,它退化为简单工厂模式。
| 简单工厂 | 工厂方法 | 抽象工厂 | |
|---|---|---|---|
| 工厂类数量 | 1 个 | 多个(一个抽象 + 多个具体) | 多个(一个抽象 + 多个具体) |
| 产品等级结构 | 1 个 | 1 个 | 多个 |
| 新增产品 | 修改工厂类 | 新增工厂类 + 产品类 | 新增工厂类 + 一族产品类 |
| 新增产品等级结构 | - | - | 需修改所有工厂类 |
| 开闭原则 | 违反 | 符合 | 倾斜(族易,等级难) |
| 核心机制 | 静态方法 + 条件判断 | 多态 + 继承 | 多态 + 继承 + 组合 |
小结
本节沿着「如何将对象创建与使用分离」这条主线,从简单工厂出发,逐步演进到工厂方法和抽象工厂:
- 简单工厂:用一个工厂类的静态方法根据参数创建对象,实现了创建与使用的分离,但违反开闭原则
- 工厂方法:将工厂也抽象化,每种产品对应一个具体工厂,通过多态实现扩展,完全符合开闭原则
- 抽象工厂:一个工厂负责创建一整族相关产品,保证产品族的一致性,但增加新的产品等级结构困难
三种模式都属于创建型模式,核心思想一脉相承:将「使用哪个类」的决策从客户端代码中移出,交给专门的工厂来处理。配合配置文件和反射机制,可以在完全不修改源代码的情况下切换具体产品——这是面向对象设计中「封装变化」和「面向接口编程」两大原则在对象创建领域的集中体现。
设计工具箱更新:
| 层次 | 内容 |
|---|---|
| OO 基础 | 抽象、封装、多态、继承 |
| OO 原则 | 封装变化、面向接口编程、组合优于继承 |
| OO 模式 | 策略模式、简单工厂模式、工厂方法模式、抽象工厂模式 |