资讯动态

工厂方法模式:从if-else到灵活对象创建的Java设计模式实践

发布时间:2026/8/14 7:31:33 来源:尧图企业网站定制
1. 工厂方法模式从“造车”到“造工厂”的思维跃迁如果你写过几年代码肯定遇到过这样的场景你需要创建一个对象但这个对象的具体类型在写代码时并不确定它可能根据配置文件、用户输入或者运行时的某些条件来决定。比如你要开发一个日志系统它需要支持输出到控制台、文件或者网络具体用哪一种得看用户怎么配置。最直接也是最笨的做法可能就是写一堆if-else或者switch-caseif (logType.equals(console)) { logger new ConsoleLogger(); } else if (logType.equals(file)) { logger new FileLogger(); } else if (logType.equals(network)) { logger new NetworkLogger(); }这段代码看起来没什么问题功能也能实现。但作为一个有追求的开发者你很快就会感到不适每次新增一种日志类型比如要加一个DatabaseLogger你就得回来修改这个创建对象的逻辑。这违反了开闭原则对扩展开放对修改关闭也让核心业务逻辑和对象创建的细节耦合在了一起代码的“坏味道”越来越浓。这时候工厂方法模式Factory Method就该登场了。它不是什么高深莫测的黑科技其核心思想非常朴素把创建对象的具体过程封装起来延迟到子类中去决定。简单说就是我不在“我”这里决定造什么车我定义一个“造车”的流程工厂方法然后让我的“分厂”子类去决定具体造轿车还是SUV。这样当需要造一种新车比如电动车时我只需要新开一个“电动车分厂”而不用去改动总厂的流水线。这个模式在Java、C#、C乃至任何面向对象语言的设计中都是基石级别的存在。它解耦了客户端代码和具体产品类让系统更灵活、更易维护。接下来我们就深入这个“分厂”的内部看看它到底是怎么运作的以及在实际项目中如何用好它。2. 模式核心抽象工厂与具体产品的舞蹈要理解工厂方法首先要厘清它涉及的几个关键角色。这就像一场精心编排的舞蹈每个角色都有其固定的位置和作用。2.1 角色定义与职责划分产品Product定义工厂方法所创建对象的接口。这是所有具体产品需要实现的“标准合同”。在我们的日志例子中它就是Logger接口里面定义了log(String message)方法。具体产品Concrete Product实现产品接口的具体类。比如ConsoleLogger、FileLogger、NetworkLogger。它们是最终被创建和使用的对象。创建者/工厂Creator声明工厂方法的类。这个类可能是一个抽象类也可能是一个带有默认实现的普通类。它的核心是包含一个抽象的或可被重写的工厂方法该方法返回一个产品类型的对象。关键点在于创建者通常不关心具体创建哪个产品它只依赖产品接口进行工作。具体创建者Concrete Creator重写工厂方法返回一个具体产品实例的类。比如ConsoleLoggerFactory、FileLoggerFactory。每个具体创建者负责生产一种特定的产品。注意很多初学者容易混淆“简单工厂”和“工厂方法”。简单工厂是把所有创建逻辑集中在一个静态方法里比如一个LoggerFactory.createLogger(type)它虽然封装了创建过程但新增产品类型时仍需修改这个工厂类。而工厂方法通过继承和多态将创建责任分散到各个具体工厂中真正实现了对扩展开放。2.2 UML类图与协作流程用文字描述可能有点抽象我们来看一个典型的UML类图它清晰地展示了各个角色之间的关系interface或抽象类 Product method() ^ | 实现 ____________|____________ | | ConcreteProductA ConcreteProductB method() method() abstract或类 Creator factoryMethod(): Product someOperation() ^ | 继承/实现 ____________|____________ | | ConcreteCreatorA ConcreteCreatorB factoryMethod(): Product factoryMethod(): Product (返回ConcreteProductA) (返回ConcreteProductB)协作流程如下客户端代码依赖的是Creator类通常通过其抽象类型或接口。客户端调用Creator的someOperation()方法。这个方法内部会调用factoryMethod()来获取一个产品对象。factoryMethod()是抽象的其具体实现由ConcreteCreatorA或ConcreteCreatorB提供。具体创建者返回一个具体的产品对象ConcreteProductA或ConcreteProductB。Creator的someOperation()方法使用这个产品对象通过产品接口完成业务逻辑而完全不知道它具体是哪种产品。这个流程的精妙之处在于客户端和Creator都只与产品的抽象接口打交道。具体是哪个“分厂”在生产生产的是哪个具体型号它们都不关心。这使得替换产品族变得异常容易只需更换使用的具体创建者即可。2.3 为何选择工厂方法权衡与考量为什么不用new直接创建或者用简单工厂选择工厂方法通常基于以下几点考量应对未来变化这是最主要的动机。当你预见到系统中需要创建的对象类型可能会频繁增加或变化时工厂方法提供了一个清晰的扩展点。新增产品时你只需增加新的具体产品和对应的具体工厂无需修改任何现有客户端和创建者代码。解耦客户端与具体类客户端代码无需引入一大堆具体产品类的头文件或导入语句它只依赖于抽象的产品接口和创建者。这降低了模块间的耦合度使得代码更清晰也更易于进行单元测试你可以轻松地用Mock对象替换真实产品。实现产品创建的个性化不同的具体创建者可以在工厂方法中实现不同的初始化逻辑。比如FileLoggerFactory可能需要检查磁盘空间和文件权限而NetworkLoggerFactory则需要配置服务器地址和端口。这些差异化的创建逻辑被封装在各自的工厂中互不干扰。框架设计的利器在框架设计中尤为常见。框架定义好创建对象的抽象方法工厂方法而将具体实现的权力交给使用框架的应用开发者。这就是所谓的“好莱坞原则”别打电话给我们我们会打给你Don‘t call us, we’ll call you。Spring 框架中大量的 Bean 定义和创建其思想就与此一脉相承。当然工厂方法也不是银弹。它的主要缺点是引入了额外的类层次结构具体创建者类如果产品类型非常固定且很少变化使用工厂方法可能会让系统显得过于复杂有点“杀鸡用牛刀”的感觉。这时候简单工厂或许是个更轻量的选择。3. 从理论到实践一个完整的日志系统示例理论讲得再多不如一行代码来得实在。我们用一个完整的、可运行的日志系统示例来展示工厂方法模式是如何落地实现的。这个例子将涵盖接口定义、具体产品实现、抽象工厂、具体工厂以及客户端的完整调用链。3.1 定义产品接口与具体产品首先我们定义所有日志器都必须遵守的契约——Logger接口。// 产品接口 public interface Logger { void log(String message); void error(String message); }接着实现几种具体的日志器// 具体产品A控制台日志器 public class ConsoleLogger implements Logger { Override public void log(String message) { System.out.println([INFO] message); } Override public void error(String message) { System.err.println([ERROR] message); } } // 具体产品B文件日志器 public class FileLogger implements Logger { private String filePath; public FileLogger(String filePath) { this.filePath filePath; // 这里可以加入文件打开、检查等初始化逻辑 System.out.println(FileLogger initialized, log file: filePath); } Override public void log(String message) { // 模拟写入文件 System.out.println([INFO to File filePath ] message); } Override public void error(String message) { // 模拟写入文件 System.err.println([ERROR to File filePath ] message); } } // 具体产品C网络日志器模拟 public class NetworkLogger implements Logger { private String serverUrl; public NetworkLogger(String serverUrl) { this.serverUrl serverUrl; System.out.println(NetworkLogger initialized, server: serverUrl); } Override public void log(String message) { // 模拟发送网络请求 System.out.println([INFO to Network serverUrl ] message); } Override public void error(String message) { System.err.println([ERROR to Network serverUrl ] message); } }实操心得注意FileLogger和NetworkLogger的构造函数需要参数。在工厂方法中处理带参数的产品创建是一个常见场景。一种做法是将参数传递给工厂方法本身如createLogger(String filePath)另一种更优雅的方式是在具体工厂的构造函数或初始化方法中设置这些参数工厂方法内部使用这些参数来实例化产品。这保持了工厂方法签名的一致性。3.2 构建抽象工厂与具体工厂现在我们来定义创建这些日志器的工厂。首先是一个抽象的日志器工厂。// 抽象创建者 public abstract class LoggerFactory { // 这就是工厂方法 public abstract Logger createLogger(); // 这是一个业务方法它依赖于工厂方法创建的产品 public void logMessage(String message) { Logger logger createLogger(); // 调用工厂方法 logger.log(message); } public void logError(String error) { Logger logger createLogger(); // 调用工厂方法 logger.error(error); } }LoggerFactory定义了工厂方法createLogger()同时提供了一些使用日志器的通用业务方法logMessage,logError。这些业务方法只通过产品接口Logger来操作这是解耦的关键。接下来为每种日志器实现具体的工厂// 具体创建者A控制台日志器工厂 public class ConsoleLoggerFactory extends LoggerFactory { Override public Logger createLogger() { // 控制台日志器通常不需要复杂初始化直接new return new ConsoleLogger(); } } // 具体创建者B文件日志器工厂 public class FileLoggerFactory extends LoggerFactory { private String filePath; public FileLoggerFactory(String filePath) { this.filePath filePath; } Override public Logger createLogger() { // 文件日志器需要文件路径参数 return new FileLogger(filePath); } } // 具体创建者C网络日志器工厂 public class NetworkLoggerFactory extends LoggerFactory { private String serverUrl; public NetworkLoggerFactory(String serverUrl) { this.serverUrl serverUrl; } Override public Logger createLogger() { // 网络日志器需要服务器地址参数 return new NetworkLogger(serverUrl); } }可以看到每个具体工厂都只负责创建一种产品并且将产品所需的特定初始化逻辑如传递文件路径、服务器地址封装在了自己内部。3.3 客户端代码与运行演示最后看看客户端是如何使用这套体系的。客户端代码的典型特点是它只与抽象工厂 (LoggerFactory) 和抽象产品 (Logger) 交互。public class Client { public static void main(String[] args) { // 1. 使用控制台日志 LoggerFactory factory1 new ConsoleLoggerFactory(); factory1.logMessage(Application started.); factory1.logError(A minor error occurred.); System.out.println(---); // 2. 使用文件日志 LoggerFactory factory2 new FileLoggerFactory(/var/log/myapp.log); factory2.logMessage(User Alice logged in.); // 我们也可以直接获取Logger对象进行更复杂的操作 Logger fileLogger factory2.createLogger(); fileLogger.error(Failed to write to database.); System.out.println(---); // 3. 使用网络日志 LoggerFactory factory3 new NetworkLoggerFactory(http://log-server:8080/ingest); factory3.logMessage(Sending heartbeat.); factory3.logError(Network timeout.); } }运行这段客户端代码你会看到类似以下的输出[INFO] Application started. [ERROR] A minor error occurred. --- FileLogger initialized, log file: /var/log/myapp.log [INFO to File/var/log/myapp.log] User Alice logged in. [ERROR to File/var/log/myapp.log] Failed to write to database. --- NetworkLogger initialized, server: http://log-server:8080/ingest [INFO to Networkhttp://log-server:8080/ingest] Sending heartbeat. [ERROR to Networkhttp://log-server:8080/ingest] Network timeout.这个示例清晰地展示了工厂方法模式的威力当我想从控制台日志切换到文件日志时我只需要将new ConsoleLoggerFactory()改为new FileLoggerFactory(“path”)所有后续的logMessage和logError调用都会自动使用文件日志因为它们是依赖createLogger()这个工厂方法的。客户端代码 (Client类) 完全没有被修改。4. 进阶应用结合依赖注入与配置化在实际的企业级项目中我们很少像上面那样在代码中硬编码new ConsoleLoggerFactory()。更常见的做法是结合依赖注入DI框架和外部配置实现工厂的动态选择和注入。这能让工厂方法模式的优势发挥到极致。4.1 使用Spring框架实现工厂方法假设我们使用Spring Boot。我们可以将具体的工厂类定义为Spring的Bean并通过ConditionalOnProperty或Profile等注解根据配置文件来决定激活哪一个工厂。首先定义产品接口和具体产品同上略。然后定义抽象工厂和具体工厂但这次它们都是Spring的组件// 抽象工厂可以不是抽象类只是一个返回Logger的接口 public interface LoggerFactory { Logger createLogger(); } // 具体工厂实现声明为Spring Bean Component ConditionalOnProperty(name app.logger.type, havingValue console, matchIfMissing true) public class ConsoleLoggerFactory implements LoggerFactory { Override public Logger createLogger() { return new ConsoleLogger(); } } Component ConditionalOnProperty(name app.logger.type, havingValue file) public class FileLoggerFactory implements LoggerFactory { Value(${app.logger.file.path:/default.log}) private String filePath; Override public Logger createLogger() { return new FileLogger(filePath); } } Component ConditionalOnProperty(name app.logger.type, havingValue network) public class NetworkLoggerFactory implements LoggerFactory { Value(${app.logger.network.url}) private String serverUrl; Override public Logger createLogger() { return new NetworkLogger(serverUrl); } }在业务服务中我们直接注入LoggerFactory接口。Spring会根据配置自动将符合条件的那个具体工厂Bean注入进来。Service public class OrderService { private final LoggerFactory loggerFactory; // Spring自动注入具体的LoggerFactory Bean public OrderService(LoggerFactory loggerFactory) { this.loggerFactory loggerFactory; } public void placeOrder(Order order) { // 使用工厂创建日志器并记录 Logger logger loggerFactory.createLogger(); logger.log(Placing order: order.getId()); // ... 业务逻辑 } }在application.yml配置文件中我们只需修改一个属性就能切换整个应用的日志输出方式# 使用控制台日志默认 app: logger: type: console # 使用文件日志 app: logger: type: file file: path: /logs/application.log # 使用网络日志 app: logger: type: network network: url: http://elk-cluster:9200/logs这种方式将对象创建的决策权从代码转移到了配置文件中系统变得极其灵活且符合“约定优于配置”和“依赖倒置”的原则。4.2 工厂方法在Fluent API与框架设计中的体现工厂方法模式也是构建流畅接口Fluent API和内部领域特定语言Internal DSL的常用手段。例如在构建一个查询语句时Query query QueryFactory.createQuery() // 静态工厂方法返回一个查询构建器接口 .select(name, age) .from(users) .where(condition - condition.greaterThan(age, 18)) .build(); // build() 方法才是最终创建具体Query对象的“工厂方法”这里的QueryFactory.createQuery()可能返回一个QueryBuilder接口的实现类。QueryBuilder的build()方法就是一个工厂方法它根据前面链式调用设置的参数最终构造并返回一个复杂的、不可变的Query对象。不同的QueryBuilder实现可能是为不同数据库优化的的build()方法会返回不同的Query具体子类。在框架设计中工厂方法模式无处不在。Java集合框架中的Collections.unmodifiableList()、Collections.synchronizedList()返回的是包装后的列表视图其底层可以看作是一种工厂。JUnit 4的RunWith注解指定一个Runner类来运行测试这个Runner的创建也蕴含着工厂方法的思想——测试框架定义了运行测试的流程但具体用什么Runner如SpringJUnit4ClassRunner来执行则由用户通过注解指定。5. 避坑指南与最佳实践实录用了这么多年工厂方法我踩过的坑和总结的经验可能比教科书上的定义更有价值。下面这些点是你在实际项目中应用工厂方法时需要特别注意的。5.1 常见问题与排查技巧问题1工厂类爆炸现象每增加一个具体产品就必须增加一个具体工厂类。如果产品种类极多比如有上百种会导致类的数量翻倍项目结构臃肿。排查与解决审视产品维度产品是否真的需要独立的工厂有时产品之间的差异很小可以通过参数化工厂方法来合并。例如一个ShapeFactory的createShape(String type)方法内部用switch创建圆形、方形在类型不多且稳定时比为每个形状单独建工厂更合适。这实际上退回到了“简单工厂”但在特定场景下是合理的折衷。使用泛型或反射谨慎可以创建一个通用的GenericFactoryT利用反射T.class.newInstance()来创建对象。但这会牺牲类型安全性和编译期检查且要求产品类必须有默认构造函数。Spring的BeanFactory就大量使用了反射。考虑其他模式如果产品族的概念很强即一系列相关的产品需要一起创建那么抽象工厂模式可能更合适。如果创建过程非常复杂涉及多个步骤建造者模式可能是更好的选择。问题2循环依赖现象在依赖注入场景下工厂A需要产品B而产品B的工厂B又需要工厂A提供的某些服务形成循环依赖导致Spring等容器启动失败。排查与解决重新设计依赖关系这是根本解决方法。检查业务逻辑看是否能打破循环。也许产品B并不需要直接依赖工厂A而是依赖一个更基础的服务接口。使用Setter注入或Lazy将循环依赖一方的注入方式从构造器注入改为Setter注入或者使用Lazy注解进行延迟初始化让容器能够先完成Bean的创建。将工厂方法改为静态方法如果工厂无状态可以考虑将其改为工具类中的静态方法避免被纳入Spring的Bean生命周期管理。问题3难以测试现象因为客户端依赖抽象工厂而测试时想注入一个Mock工厂或Mock产品变得有点绕。排查与解决依赖注入是王道正如前面的Spring示例始终坚持通过构造函数或Setter注入工厂接口。在单元测试中你可以轻松地Mockito.mock(LoggerFactory.class)并定义其行为。不要在生产代码中硬编码new ConcreteFactory()硬编码会使得测试时无法替换实现。确保工厂的实例化也是由外部如Spring容器、主程序组装模块控制的。5.2 参数化工厂方法的处理技巧当产品创建需要参数时如何处理我见过几种方案方案A工厂方法带参数(createLogger(String path))。优点是直观调用时传入。缺点是如果参数很多方法签名会变长且不同产品需要的参数不同接口难以统一。方案B工厂对象带状态如前文的FileLoggerFactory(String path)。将参数保存在具体工厂实例的属性中。工厂方法createLogger()无参。这是更优雅的方式它保证了工厂方法签名的一致性且将参数配置与工厂生命周期绑定。方案C使用配置对象。定义一个LoggerConfig类包含所有可能的配置项文件路径、服务器URL、日志级别等。工厂方法变为createLogger(LoggerConfig config)。具体工厂根据config对象内的字段决定如何初始化产品。这种方式扩展性最好但略显复杂。我的经验是对于1-2个明确、专用的参数用方案B。对于复杂、可选的配置项用方案C。尽量避免方案A除非参数极其简单且唯一。5.3 何时不用工厂方法不要为了模式而模式。以下情况你可能不需要工厂方法对象创建非常简单直接new就能搞定且未来几乎不可能变化。例如创建一个本地的、无依赖的Date或StringBuilder对象。产品类型极少且稳定比如你的系统就两种数据库连接MySQL和PostgreSQL并且永远不会变。一个简单的静态工厂方法或者枚举足矣。创建过程需要极大的灵活性如果对象的构建步骤繁多且顺序、组合方式多变建造者模式Builder更适合。需要强调一系列相关产品的组合创建如果你要创建的是“一套”产品例如为Windows界面创建按钮、文本框、菜单为Mac界面创建另一套那么抽象工厂模式是专门解决这个问题的。工厂方法模式的核心价值在于应对变化和解耦。如果你的场景中“变化”和“耦合”不是主要矛盾那么引入它带来的抽象层反而会增加不必要的复杂度。判断的黄金准则永远是这段代码在未来半年内因为需求变更而需要修改的可能性有多大如果可能性高那么提前用工厂方法做好隔离是值得的如果可能性极低那么YAGNIYou Ain‘t Gonna Need It原则告诉我们不要过度设计。最后再分享一个小心得在团队协作中当你发现多个地方出现了创建同一类对象的switch或if-else语句时这就是一个强烈的信号提醒你该考虑引入工厂方法或抽象工厂来集中管理这些创建逻辑了。这不仅能减少重复代码更是为未来的功能扩展铺平了道路。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价