Java 设计模式心法之第5篇 - 工厂方法 (Factory Method) - 定义对象创建的契约
上一章我们探讨了如何保证对象的“唯一性”(单例模式)。但更多时候,我们需要根据不同的情境创建不同类型的对象实例。如果直接在代码中用 new 硬编码具体类名,当需要新增类型或改变创建逻辑时,修改将如同“拆东墙补西墙”,极易违反开闭原则。本文将带你深入理解创建型模式中的“授权代表”——工厂方法模式。我们将揭示它如何通过定义一个创建对象的“标准流程”(接口),并将具体的“生产任务”(实例化)授权给“分厂”(子类)来完成,从而实现创建过程的灵活性与解耦。掌握工厂方法,是构建可扩展、易维护系统的关键一步。
一、问题的提出:当“标准化生产”遭遇“个性化需求”
想象一家大型制造企业,它生产多种不同型号的产品,比如汽车。如果总部(客户端代码)直接负责每一辆具体型号汽车(如“轿车”、“SUV”、“卡车”)的完整制造流程(new Car(), new Suv(), new Truck()),会发生什么?
- 总部负担过重: 总部需要了解所有型号汽车的详细制造工艺(具体类的构造细节)。
- 难以扩展: 当需要增加新的车型(如“电动皮卡”)时,必须修改总部的生产调度代码(增加新的
if-else或switch判断),这违反了开闭原则 (OCP),风险高且不优雅。 - 缺乏灵活性: 如果不同地区的“分厂”(子系统或模块)希望在标准流程基础上,对特定车型的制造过程进行微调或使用特定零件,总部集权式的生产方式难以满足这种“本地化”需求。
直接使用 new 进行对象创建,就像是这种“总部集权式生产”。它简单直接,但在面对对象类型多样化、未来可能扩展、创建逻辑复杂或需要子类定制化等场景时,就会显得捉襟见肘,导致代码僵化、耦合紧密、难以维护。我们需要一种更灵活、更符合开闭原则的创建机制。
二、授权的艺术:工厂方法模式的核心定义与意图
工厂方法模式 (Factory Method Pattern) 提供了一种优雅的解决方案。它不再由“总部”直接生产所有产品,而是定义了一套标准的“生产合同”或“建厂规范”(一个用于创建对象的接口或抽象方法),并将实际的“产品制造”任务(调用哪个具体类的构造器来实例化对象)授权给各个“分厂”(子类)去完成。
GoF 的经典意图描述是:“定义一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类。”
这句话点明了工厂方法模式的精髓:
- 定义创建接口 (Define an interface for creating an object): 在父类(或接口)中声明一个抽象的“工厂方法”,这个方法负责返回产品对象。它规定了“必须生产一个产品出来”的契约。
- 子类决定实例 (Let subclasses decide which class to instantiate): 具体的生产任务由子类来实现这个工厂方法。每个子类根据自己的职责,决定到底
new出哪一个具体的产品类实例并返回。 - 延迟实例化 (Defer instantiation to subclasses): 父类只定义创建的“框架”和“规范”,实际的“实例化动作”被推迟到了子类中执行。
核心角色:
- Product (产品接口/抽象类): 定义了工厂方法所创建的对象的共同接口。所有具体产品都必须实现这个接口。
- ConcreteProduct (具体产品类): 实现 Product 接口,是工厂方法实际创建的目标对象。
- Creator (创建者抽象类/接口): 声明了工厂方法 (
factoryMethod()),该方法返回一个 Product 类型的对象。Creator 也可以包含一些依赖于 Product 对象的通用业务逻辑。 - ConcreteCreator (具体创建者类): 实现了 Creator 中的抽象工厂方法,负责实际创建并返回一个具体的 ConcreteProduct 实例。每个 ConcreteCreator 通常对应一个 ConcreteProduct。
三、解耦的威力:工厂方法模式的适用场景
工厂方法模式在以下场景中特别有用:
- 编译时无法预知具体对象类型: 当一个类不知道它所必须创建的对象的确切类时,例如,需要根据配置文件、用户输入或运行时条件来决定创建哪种对象。
- 希望子类指定创建对象: 当一个类希望由它的子类来指定它所创建的对象的时候。这是最典型的应用场景,父类定义框架,子类填充细节(包括创建的对象类型)。
- 将创建对象的职责委托给辅助子类: 当你需要将创建对象的复杂逻辑或权限控制封装起来,由专门的“工厂子类”负责,主类只负责使用。
- 框架设计中的扩展点: 框架通常定义了标准的处理流程(模板方法)和对象接口,但将具体对象的创建留给使用者(通过继承框架类并实现工厂方法)来定制。许多框架(如 JDBC、
java.util.Calendar的某些用法)都体现了工厂方法的思想。
四、招式详解:工厂方法模式的 Java 实现
我们通过一个简单的日志记录器示例来演示工厂方法模式的实现。假设我们需要根据不同的场景(如文件记录、数据库记录)创建不同类型的日志记录器。
1. 定义产品接口 (Product):
/**
* 产品接口:定义了所有日志记录器都需要实现的功能
*/
interface Logger {
void log(String message);
}
2. 创建具体产品类 (ConcreteProduct):
/**
* 具体产品A:文件日志记录器
*/
class FileLogger implements Logger {
@Override
public void log(String message) {
System.out.println("【文件日志】:" + message);
// ... 实际写入文件的逻辑 ...
}
}
/**
* 具体产品B:数据库日志记录器
*/
class DatabaseLogger implements Logger {
@Override
public void log(String message) {
System.out.println("【数据库日志】:" + message);
// ... 实际写入数据库的逻辑 ...
}
}
// 如果未来需要控制台日志记录器,只需添加新类:
class ConsoleLogger implements Logger {
@Override
public void log(String message) {
System.out.println("【控制台日志】:" + message);
}
}
3. 定义创建者抽象类 (Creator):
/**
* 创建者抽象类:定义了创建 Logger 的工厂方法(抽象)
* 也可以包含一些使用 Logger 的通用业务逻辑
*/
abstract class LoggerFactory {
// 工厂方法:声明为抽象,由子类实现来创建具体 Logger
protected abstract Logger createLogger();
// 使用产品的通用方法 (示例)
public void writeLog(String message) {
// 先通过工厂方法获取 Logger 对象
Logger logger = createLogger();
// 再使用 Logger 对象记录日志
logger.log(message);
}
}
4. 创建具体创建者类 (ConcreteCreator):
/**
* 具体创建者A:专门用于创建 FileLogger 的工厂
*/
class FileLoggerFactory extends LoggerFactory {
@Override
protected Logger createLogger() {
// 负责创建并返回 FileLogger 实例
System.out.println("文件日志工厂正在创建 FileLogger...");
return new FileLogger();
}
}
/**
* 具体创建者B:专门用于创建 DatabaseLogger 的工厂
*/
class DatabaseLoggerFactory extends LoggerFactory {
@Override
protected Logger createLogger() {
// 负责创建并返回 DatabaseLogger 实例
System.out.println("数据库日志工厂正在创建 DatabaseLogger...");
return new DatabaseLogger();
}
}
// 如果新增了 ConsoleLogger,只需对应增加 ConsoleLoggerFactory
class ConsoleLoggerFactory extends LoggerFactory {
@Override
protected Logger createLogger() {
System.out.println("控制台日志工厂正在创建 ConsoleLogger...");
return new ConsoleLogger();
}
}
5. 客户端使用:
public class FactoryMethodClient {
public static void main(String[] args) {
// 需要文件日志?使用文件日志工厂
LoggerFactory fileFactory = new FileLoggerFactory();
fileFactory.writeLog("这是一条需要记录到文件的重要信息。");
System.out.println("--------------------");
// 需要数据库日志?使用数据库日志工厂
LoggerFactory dbFactory = new DatabaseLoggerFactory();
dbFactory.writeLog("这是一条需要存入数据库的操作记录。");
System.out.println("--------------------");
// 需要控制台日志?使用控制台日志工厂 (轻松扩展!)
LoggerFactory consoleFactory = new ConsoleLoggerFactory();
consoleFactory.writeLog("这是一条调试信息,输出到控制台即可。");
// 关键:客户端代码 (main方法) 只与 LoggerFactory 和 Logger 接口交互,
// 完全不知道具体的 FileLogger、DatabaseLogger 等类的存在,
// 也不知道它们是如何被创建的。实现了高层模块与具体实现的解耦。
}
}
代码解读:
- 客户端 (
FactoryMethodClient) 想要记录日志,它不直接new具体的Logger,而是选择一个合适的LoggerFactory(比如FileLoggerFactory)。 - 它调用
LoggerFactory的writeLog方法(或者直接调用createLogger获取Logger对象)。 LoggerFactory的writeLog方法内部会调用抽象的createLogger()方法。- 由于多态,实际执行的是具体工厂子类(如
FileLoggerFactory)重写的createLogger()方法。 - 这个具体工厂的方法负责
new出对应的具体产品(如FileLogger)并返回。 - 客户端最终通过抽象的
Logger接口来使用这个被创建出来的具体产品。
五、模式的价值:工厂方法带来的益处
工厂方法模式之所以经典,在于它带来的显著优势:
- 优秀的解耦性 (Decoupling):
- 客户端与具体产品解耦: 客户端代码只依赖于抽象的 Product 和 Creator 接口/类,完全不知道也不关心具体的 ConcreteProduct 类名和创建细节。更换或增加产品类型,客户端代码无需修改。
- 创建职责分离: 将对象创建的复杂逻辑封装在具体的工厂子类中,使得 Creator 类可以更专注于其核心职责(可能是使用产品进行某些操作)。符合单一职责原则 (SRP)。
- 强大的扩展性 (Extensibility):
- 符合开闭原则 (OCP): 当需要引入新的产品类型时,只需要:
- 创建一个新的 ConcreteProduct 类。
- 创建一个新的 ConcreteCreator 类,实现
createLogger()方法来返回新产品的实例。
完全不需要修改现有的 Creator 基类、其他 ConcreteCreator 或客户端代码。系统的扩展性极佳。
- 符合开闭原则 (OCP): 当需要引入新的产品类型时,只需要:
- 增强代码的灵活性与可维护性 (Flexibility & Maintainability):
- 子类拥有更大的自主权: 子类可以自由决定如何创建对象,甚至可以返回缓存的实例、进行额外的初始化等。
- 代码结构更清晰: 创建逻辑被集中到了各个具体的工厂类中,使得代码更容易理解和维护。
六、权衡与考量:工厂方法的“成本”
引入工厂方法模式并非没有代价,需要考虑其可能带来的复杂性:
- 类爆炸风险 (Class Proliferation): 每增加一个新的产品类型,通常需要同时增加一个新的具体产品类和一个新的具体创建者类。如果产品种类非常多,会导致类层级结构变得庞大和复杂。
- 增加了抽象层次: 相比直接
new,引入了 Creator 和 Product 的抽象层,增加了代码的间接性和理解成本(虽然这种抽象正是其优势所在)。
因此,如果你的对象创建逻辑非常简单,且未来不太可能变化或扩展,直接 new 可能更直接。但对于需要灵活性、可扩展性、或者希望将创建逻辑与使用逻辑分离的场景,工厂方法模式的优势远大于其引入的少量复杂性。
七、明辨异同:与其他创建模式的对话 (FAQ)
-
Q1: 工厂方法模式 vs 简单工厂模式 (Simple Factory / Static Factory)?
- A1:
- 简单工厂: 通常是一个包含静态方法的类,该方法根据传入的参数(如类型字符串)使用
if-else或switch来创建并返回不同的产品实例。它不是 GoF 23 种设计模式之一。 - 区别:
- 结构: 工厂方法是基于继承和多态的(抽象 Creator 和具体 Creator 子类),而简单工厂通常是一个类 + 一个静态方法。
- OCP: 简单工厂在增加新产品时通常需要修改工厂类的静态方法(违反 OCP),而工厂方法通过增加新子类来实现扩展(符合 OCP)。
- 职责: 工厂方法将创建逻辑分散到各个具体工厂子类中,而简单工厂将所有创建逻辑集中在一个地方。
- 联系: 简单工厂可以看作是工厂方法的一种简化或初级形态,适用于产品种类不多且不经常变化的简单场景。
- 简单工厂: 通常是一个包含静态方法的类,该方法根据传入的参数(如类型字符串)使用
- A1:
-
Q2: 工厂方法模式 vs 抽象工厂模式 (Abstract Factory)?
- A2: 这是初学者容易混淆的一对。
- 关注点: 工厂方法关注创建单个产品对象,其目的是让子类决定实例化哪一个类。抽象工厂关注创建一系列相互关联或相互依赖的产品对象(一个产品族),确保这些产品能够协同工作。
- 结构: 工厂方法通常通过继承(子类重写父类的工厂方法)来实现。抽象工厂通常包含多个工厂方法(每个方法创建一个族中的产品),其实现往往基于对象组合(将一个具体的工厂对象传递给客户端,客户端通过这个工厂对象来创建产品族)。
- 产品: 工厂方法通常只有一个抽象产品和多个具体产品。抽象工厂有多个抽象产品(构成产品族的各个部分)和对应的多个具体产品。
- 可以简单记:工厂方法造一个,抽象工厂造一窝。
- A2: 这是初学者容易混淆的一对。
-
Q3: 工厂方法模式与依赖倒置原则 (DIP) 的关系?
- A3: 工厂方法模式是实践 DIP 的一个良好范例。客户端代码(高层模块)依赖于抽象的 Creator 和 Product 接口,而具体的 ConcreteProduct 和 ConcreteCreator(低层模块/细节)也依赖于这些抽象。创建的具体细节被封装在 ConcreteCreator 中,高层模块无需关心,实现了依赖关系的“倒置”。
八、心法归纳:授权创建,拥抱变化
工厂方法模式的核心“心法”在于**“授权”与“契约”。它通过在父类中定义一个创建对象的契约**(抽象工厂方法),将实际的创建决策权(实例化哪个具体类)授权给子类。这种“延迟决策”和“职责下放”带来了巨大的好处:
- 解耦了使用者与创建者、使用者与具体产品,使得系统更加灵活。
- 完美契合了开闭原则,让添加新产品类型变得轻松自然,无需修改现有稳定代码。
- 将创建逻辑封装,使得代码结构更清晰,职责更分明。
当你遇到需要根据不同情况创建不同类型对象,并且希望系统能够轻松扩展以支持未来更多类型时,工厂方法模式就是你设计工具箱中一把锋利且优雅的“瑞士军刀”。它鼓励我们面向接口编程,拥抱变化,是构建健壮、可演进软件系统的重要基石。
下一章预告: 《Java 设计模式心法:抽象工厂 (Abstract Factory) - 构建产品家族的蓝图》。如果我们需要创建的不是单个对象,而是一整套相互配套、风格统一的对象(比如一套特定主题的 UI 控件),抽象工厂模式将为我们展现它的威力。敬请期待!
更多推荐
所有评论(0)