十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

工厂方法模式:优雅解耦对象创建,提升代码可维护性与扩展性

工厂方法模式:优雅解耦对象创建,提升代码可维护性与扩展性 1. 项目概述为什么我们需要工厂方法如果你写过一段时间的代码尤其是面向对象的代码大概率遇到过这样的场景你需要创建一个对象但这个对象的具体类型在写代码的时候并不确定它可能取决于一个配置文件、一个用户输入或者程序运行时的某个状态。最直接的做法可能就是写一堆if-else或者switch-case语句根据条件去new不同的对象。代码写起来很快但维护起来简直就是一场噩梦——每增加一种新类型你就得去修改那个已经臃肿不堪的条件判断块这直接违反了“对修改关闭对扩展开放”的开闭原则。工厂方法模式Factory Method Pattern就是为了优雅地解决这个问题而生的。它不是什么高深莫测的黑科技而是一种经过时间考验的、用于封装对象创建逻辑的设计思路。简单来说它定义了一个用于创建对象的接口或抽象方法但将具体创建哪个类实例的决定权推迟到了子类。这样客户端代码就不再需要关心它得到的对象具体是哪个子类它只需要和抽象接口打交道。这就像你去一家咖啡店点单你只需要告诉店员“我要一杯咖啡”至于这杯咖啡是拿铁、美式还是卡布奇诺由后厨工厂根据你的订单参数来决定你作为顾客客户端并不需要关心具体的冲泡过程。在当今的软件开发中无论是构建复杂的业务系统、开发可扩展的框架还是设计易于测试的代码结构工厂方法都扮演着至关重要的角色。它解耦了客户端代码和具体产品类使得系统更容易应对变化。结合网络热词来看无论是面试中高频出现的“设计模式面试题”还是实际项目中“基于saas模式的中小企业进销存信息系统”的模块化设计亦或是追求优雅API设计的“fluent api 设计中的经典模式”工厂方法都是其底层坚实的思想基石之一。接下来我将以一个贯穿始终的、贴近实战的例子带你从零开始拆解工厂方法不仅理解其“形”更要掌握其“神”。2. 核心思路与模式结构拆解2.1 从“简单工厂”到“工厂方法”的演进在深入工厂方法之前我们先看一个更简单的变体——简单工厂Simple Factory这有助于我们理解问题的根源和工厂方法的价值所在。假设我们正在开发一个日志记录器Logger。最初我们可能只需要记录到控制台ConsoleLogger。代码很简单public class LoggerClient { public void doSomething() { Logger logger new ConsoleLogger(); logger.log(An operation is performed.); } }很快需求来了需要支持将日志写入文件FileLogger。于是你可能会这样改public class LoggerClient { public void doSomething(String loggerType) { Logger logger; if (console.equals(loggerType)) { logger new ConsoleLogger(); } else if (file.equals(loggerType)) { logger new FileLogger(); } else { throw new IllegalArgumentException(Unsupported logger type); } logger.log(An operation is performed.); } }这已经引入了条件判断。当需要新增数据库日志DatabaseLogger时你必须回来修改这个if-else块。这里的创建逻辑和客户端业务逻辑doSomething紧耦合在一起。简单工厂尝试将创建逻辑剥离出来封装到一个专门的类里public class LoggerFactory { public static Logger createLogger(String type) { if (console.equals(type)) { return new ConsoleLogger(); } else if (file.equals(type)) { return new FileLogger(); } else if (database.equals(type)) { return new DatabaseLogger(); } throw new IllegalArgumentException(Unsupported logger type); } } // 客户端使用 Logger logger LoggerFactory.createLogger(file);这比之前好多了客户端不再包含创建逻辑。但是LoggerFactory的createLogger方法仍然是一个集中式的、静态的条件判断。增加一个新的日志类型仍然需要修改LoggerFactory类的源代码。它只是转移了耦合点并没有从根本上解决“对修改关闭”的问题。注意简单工厂并不是23种经典设计模式之一它更像是一种编程习惯。它的主要问题在于静态方法违背了“基于接口而非实现编程”的原则且不利于继承和扩展。工厂方法模式则更进一步。它不再提供一个统一的、处理所有情况的“万能工厂”而是为每一种产品定义一个专门的工厂。它通过多态性将对象创建的任务委托给工厂子类。2.2 工厂方法模式的四大角色工厂方法模式包含以下四个核心角色理解它们之间的关系是掌握该模式的关键产品Product定义工厂方法所创建对象的接口。在我们的例子中就是Logger接口。具体产品Concrete Product实现Product接口的具体类。例如ConsoleLogger,FileLogger,DatabaseLogger。创建者/工厂Creator声明工厂方法factoryMethod的抽象类或接口。它可能包含一些依赖于Product对象的核心业务逻辑。注意它的主要职责不一定是创建对象而是包含一些依赖于产品对象的核心操作。具体创建者/具体工厂Concrete Creator重写工厂方法返回一个具体的Concrete Product实例。例如ConsoleLoggerFactory,FileLoggerFactory。它们之间的关系可以用以下UML类图的核心思想来描述注意这里用文字表述结构Creator依赖于抽象的Product接口。ConcreteCreatorA继承自Creator并实现其factoryMethod()在该方法内部返回new ConcreteProductA()。同样ConcreteProductA实现了Product接口。这样客户端代码将与Creator抽象类交互由具体的ConcreteCreator来决定实例化哪个ConcreteProduct。这种设计的精妙之处在于客户端代码完全与具体产品类解耦它只和抽象创建者以及抽象产品打交道。当需要扩展一个新的产品时我们只需要新增一个具体产品类和一个对应的具体工厂类然后由客户端决定使用哪个工厂。原有的任何代码包括原有的具体工厂和客户端都无需修改。这完美地符合了开闭原则。2.3 何时该用工厂方法—— 模式的应用场景辨析工厂方法不是银弹在错误的地方使用它会增加不必要的复杂度。以下是它最适用的几种典型场景你可以对照自己的项目需求进行判断框架设计将实现留给用户这是工厂方法的经典应用。框架定义好核心流程和抽象产品但将具体产品的创建留给框架的使用者即你的应用程序来实现。例如Spring 框架中的BeanFactory、JUnit 测试框架中的Test用例创建。依赖解耦便于单元测试当你需要将代码与一个不易测试的具体类如涉及网络、数据库、文件系统的类解耦时可以使用工厂方法。在测试中你可以提供一个返回 Mock 对象模拟对象的工厂。这在“设计模式面试题”中经常被问到是编写可测试代码的重要技巧。对象创建过程复杂如果创建一个对象需要一系列复杂的步骤如读取配置、组装部件、进行校验将这些步骤封装在工厂方法中可以保持客户端代码的简洁性和可读性。需要灵活控制实例创建过程例如你可能需要实现对象池缓存已创建的对象、实现单例确保全局唯一实例或者根据上下文返回不同的子类。工厂方法提供了一个统一的入口点来施加这些控制逻辑。反过来在以下情况你可能不需要工厂方法对象创建极其简单如果只是new SomeClass()直接实例化往往更清晰。不存在变化的可能如果确定系统中永远只会有一种具体实现那么引入抽象层就是过度设计。使用更高级的依赖注入容器在现代企业级开发中像 Spring 这样的 IoC控制反转容器已经是一个功能超级强大的“超级工厂”它通过配置和注解管理对象的生命周期和依赖关系在很多场景下可以替代手写的工厂模式。3. 实战演练从零实现一个可扩展的日志工厂理论说得再多不如动手写一遍。让我们用 Java 语言实现一个完整的、支持多种日志输出方式的日志系统并在此过程中融入工厂方法模式。3.1 第一步定义抽象产品与具体产品首先我们定义所有日志记录器都必须遵守的契约——Logger接口。/** * 抽象产品日志记录器接口 * 定义了产品家族的统一行为。 */ public interface Logger { /** * 记录日志信息 * param message 日志内容 */ void log(String message); /** * 设置日志级别可选扩展 * param level 级别如 DEBUG, INFO, ERROR */ void setLevel(String level); }接着我们实现几个具体产品。为了让例子更真实我们为文件日志器添加一个文件路径属性。/** * 具体产品控制台日志记录器 */ public class ConsoleLogger implements Logger { private String level INFO; Override public void log(String message) { // 这里可以添加颜色等格式化输出使其在控制台更醒目 System.out.println([ level ] Console: message); } Override public void setLevel(String level) { this.level level; } } /** * 具体产品文件日志记录器 */ public class FileLogger implements Logger { private String filePath; private String level INFO; public FileLogger(String filePath) { this.filePath filePath; // 模拟初始化文件句柄等操作 System.out.println(初始化文件日志器路径: filePath); } Override public void log(String message) { // 这里应包含写入文件的真实逻辑例如使用 FileWriter // 为简化示例我们仅模拟 System.out.println([ level ] Writing to file filePath : message); } Override public void setLevel(String level) { this.level level; } // 可能还需要一个 close() 方法来释放资源 } /** * 具体产品数据库日志记录器模拟 */ public class DatabaseLogger implements Logger { private String level INFO; private String connectionString; public DatabaseLogger(String connectionString) { this.connectionString connectionString; System.out.println(初始化数据库日志器连接: connectionString); } Override public void log(String message) { // 模拟执行 INSERT INTO log_table (message, level, timestamp) VALUES (...) System.out.println([ level ] Inserting into database: message); } Override public void setLevel(String level) { this.level level; } }实操心得在定义产品接口时不要一开始就追求“大而全”的接口。应从当前需求出发定义最小、最必要的公共方法。像setLevel这样的方法如果所有日志器都需要就放在接口里如果只有部分需要可以考虑放在抽象类中或者使用缺省适配器模式。避免接口污染。3.2 第二步定义抽象创建者与具体创建者现在我们来定义工厂。注意创建者类通常包含一些不依赖于具体产品类型的核心业务逻辑。/** * 抽象创建者日志记录器工厂 * 1. 声明了工厂方法 (createLogger)。 * 2. 可能包含一些依赖于Logger的核心操作如模板方法。 */ public abstract class LoggerFactory { /** * 工厂方法创建日志记录器。 * 注意返回类型是抽象产品 Logger。 * return 一个具体的Logger实例 */ public abstract Logger createLogger(); /** * 一个依赖于Logger的核心业务操作示例。 * 这是一个“模板方法”它定义了操作的骨架其中具体的Logger创建步骤延迟到了子类。 */ public void performLogging(String operationName) { // 1. 创建Logger (由子类决定具体类型) Logger logger createLogger(); // 2. 执行一些公共的前置逻辑如记录开始时间 System.out.println(--- Starting operation: operationName ---); // 3. 使用Logger记录信息 logger.log(Operation operationName is being executed.); // 4. 执行一些公共的后置逻辑 System.out.println(--- Finished operation: operationName ---\n); // 注意这里没有调用logger.close()实际中需要考虑资源管理。 } // 可以定义更多的工厂方法用于创建有参的Logger // public abstract Logger createLoggerWithParam(String param); }接下来为每一种具体的日志记录器实现一个对应的工厂。关键点在于每个具体工厂只负责创建一种具体产品。/** * 具体创建者控制台日志记录器工厂 */ public class ConsoleLoggerFactory extends LoggerFactory { /** * 工厂方法的具体实现返回一个新的ConsoleLogger实例。 */ Override public Logger createLogger() { // 这里可以进行一些简单的初始化配置 ConsoleLogger logger new ConsoleLogger(); logger.setLevel(DEBUG); // 例如默认设置控制台日志为DEBUG级别 return logger; } } /** * 具体创建者文件日志记录器工厂 */ public class FileLoggerFactory extends LoggerFactory { private String filePath; public FileLoggerFactory(String filePath) { this.filePath filePath; } Override public Logger createLogger() { // 将文件路径这个“创建知识”封装在工厂里 return new FileLogger(this.filePath); } } /** * 具体创建者数据库日志记录器工厂 */ public class DatabaseLoggerFactory extends LoggerFactory { private String connectionString; public DatabaseLoggerFactory(String connectionString) { this.connectionString connectionString; } Override public Logger createLogger() { // 封装数据库连接信息 return new DatabaseLogger(this.connectionString); } }注意事项工厂方法createLogger()的返回值一定是抽象产品类型Logger而不是任何具体类型。这是实现多态和依赖倒置的关键。工厂内部可以对新创建的对象进行一些初始化和配置如设置默认级别、注入依赖等然后再返回。3.3 第三步客户端代码如何使用客户端代码现在变得非常干净和灵活。它只需要与LoggerFactory这个抽象类打交道。/** * 客户端代码 */ public class ClientApplication { public static void main(String[] args) { // 场景1使用控制台日志 LoggerFactory consoleFactory new ConsoleLoggerFactory(); consoleFactory.performLogging(Data Processing Job); // 场景2使用文件日志 LoggerFactory fileFactory new FileLoggerFactory(/app/logs/system.log); fileFactory.performLogging(File Backup Task); // 场景3使用数据库日志 LoggerFactory dbFactory new DatabaseLoggerFactory(jdbc:mysql://localhost:3306/app_logs); dbFactory.performLogging(User Audit Trail); // 更动态的用法工厂类型可以从配置读取 String loggerType System.getProperty(logger.type, console); LoggerFactory factory; switch (loggerType) { case file: factory new FileLoggerFactory(/app/logs/config.log); break; case database: factory new DatabaseLoggerFactory(jdbc:mysql://localhost:3306/app_logs); break; case console: default: factory new ConsoleLoggerFactory(); break; } // 后续所有业务代码都使用这个统一的factory无需关心具体类型 factory.performLogging(Configuration Driven Task); } }运行上述ClientApplication的main方法你会看到类似以下的输出清晰地展示了不同工厂创建了不同的产品并执行了统一的日志操作流程--- Starting operation: Data Processing Job --- [DEBUG] Console: Operation Data Processing Job is being executed. --- Finished operation: Data Processing Job --- --- Starting operation: File Backup Task --- 初始化文件日志器路径: /app/logs/system.log [INFO] Writing to file /app/logs/system.log: Operation File Backup Task is being executed. --- Finished operation: File Backup Task --- --- Starting operation: User Audit Trail --- 初始化数据库日志器连接: jdbc:mysql://localhost:3306/app_logs [INFO] Inserting into database: Operation User Audit Trail is being executed. --- Finished operation: User Audit Trail --- ... (配置驱动任务的输出)这种架构带来的好处是显而易见的如果明天我们需要增加一个“网络日志服务”NetworkLogger我们只需要做两件事1) 创建NetworkLogger类实现Logger接口2) 创建NetworkLoggerFactory类继承LoggerFactory。然后在客户端选择使用这个新工厂即可。原有的ConsoleLoggerFactory、FileLoggerFactory以及它们的客户端调用代码一行都不需要修改。这就是“对扩展开放对修改关闭”。4. 模式变体与高级应用技巧掌握了标准形式后我们来看看工厂方法在实战中的几种常见变体和高级技巧这能让你更灵活地运用它。4.1 变体一参数化工厂方法有时工厂方法需要根据传入的参数来创建不同的产品。但要注意这容易滑向简单工厂的老路。更优雅的做法是让参数决定工厂的类型而非在工厂方法内部进行条件判断。如果必须在同一个工厂方法内根据参数创建不同产品应确保这些产品属于同一个继承体系并且参数逻辑清晰、稳定。例如我们的FileLogger可能需要根据日志级别决定是写入普通文件还是错误文件。我们可以这样做public class AdvancedFileLoggerFactory extends LoggerFactory { private String basePath; public AdvancedFileLoggerFactory(String basePath) { this.basePath basePath; } Override public Logger createLogger() { // 默认返回普通文件日志器 return createLogger(INFO); } // 重载的工厂方法接收参数 public Logger createLogger(String level) { String filePath; if (ERROR.equalsIgnoreCase(level)) { filePath basePath /error.log; } else { filePath basePath /app.log; } FileLogger logger new FileLogger(filePath); logger.setLevel(level); return logger; } }4.2 变体二使用Lambda表达式或方法引用Java 8在Java 8及以后如果工厂逻辑非常简单基本上就是调用构造函数我们可以直接用SupplierLogger或者方法引用来表示工厂这极大地简化了代码特别是在依赖注入或配置场景中。import java.util.function.Supplier; public class LambdaLoggerFactory { // 核心就是一个Supplier private final SupplierLogger loggerSupplier; public LambdaLoggerFactory(SupplierLogger supplier) { this.loggerSupplier supplier; } public Logger getLogger() { return loggerSupplier.get(); } public static void main(String[] args) { // 使用方法引用 LambdaLoggerFactory consoleFactory new LambdaLoggerFactory(ConsoleLogger::new); // 使用Lambda表达式 LambdaLoggerFactory fileFactory new LambdaLoggerFactory(() - new FileLogger(./log.txt)); Logger logger1 consoleFactory.getLogger(); Logger logger2 fileFactory.getLogger(); } }Spring Framework 的Bean注解方法本质上就是一个返回产品实例的工厂方法其实现思想与此相通。4.3 技巧工厂方法与依赖注入DI的结合在现代框架中工厂方法模式常常以另一种形式出现——依赖注入容器。容器本身就是一个超级工厂。你通过Component、Service等注解声明“产品”通过Autowired在需要的地方声明依赖容器负责在运行时将具体的产品实例“注入”进来。你甚至可以使用Qualifier或Resource来指定要注入哪个具体产品这相当于选择不同的“具体工厂”。例如在Spring中你可以这样配置多个Logger实现// 产品定义 Component(consoleLogger) public class ConsoleLogger implements Logger { /*...*/ } Component(fileLogger) public class FileLogger implements Logger { /*...*/ } // 客户端使用 Service public class BusinessService { // 通过限定符指定要注入哪个具体产品 Autowired Qualifier(fileLogger) private Logger logger; public void doBusiness() { logger.log(Business logic executed.); } }Spring 的ApplicationContext在这里扮演了抽象工厂的角色它根据你的注解配置动态地决定提供哪个具体的Logger实例。理解工厂方法模式能帮助你更好地理解DI容器的工作原理。4.4 技巧利用工厂实现对象池或单例工厂方法不仅用于创建新对象还可以用于管理对象生命周期。例如实现一个简单的数据库连接池public class ConnectionPoolFactory extends LoggerFactory { // 假设我们有一个连接池 private BlockingQueueLogger pool new LinkedBlockingQueue(10); public ConnectionPoolFactory() { // 预先创建一些连接Logger放入池中 for (int i 0; i 5; i) { pool.offer(new DatabaseLogger(common_conn_string)); } } Override public Logger createLogger() { // 从池中获取而不是新建 try { return pool.take(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return new DatabaseLogger(fallback_conn_string); } } public void returnLogger(Logger logger) { pool.offer(logger); } }同样如果你想确保某个 Logger 是单例的也可以在工厂方法中控制public class SingletonFileLoggerFactory extends LoggerFactory { private volatile FileLogger instance; Override public Logger createLogger() { if (instance null) { synchronized (this) { if (instance null) { instance new FileLogger(/singleton.log); } } } return instance; } }5. 常见“坑点”与最佳实践指南即使理解了原理在实际编码中依然会踩到一些坑。下面是我总结的一些常见问题和最佳实践。5.1 典型问题与排查表问题现象可能原因解决方案新增产品类型后仍需修改大量客户端代码来选择新工厂。客户端直接硬编码了具体工厂类的实例化new ConcreteFactory()。将工厂的选择逻辑集中化例如使用配置文件、环境变量或一个简单的“工厂选择器”。可以考虑结合抽象工厂模式或依赖注入容器来管理工厂实例。工厂类数量爆炸每个产品一个工厂类太多了。过度使用工厂方法。当产品种类非常多且创建逻辑简单时为每个产品建一个工厂确实繁琐。评估是否真的需要如此细粒度的控制。可以考虑使用简单工厂如果变化不频繁或者使用“参数化工厂方法”在一个工厂内根据参数创建一族相关对象。工厂方法中包含了大量与对象创建无关的业务逻辑。混淆了“创建者”的职责。工厂的主要职责是创建对象。将与对象创建紧密相关的初始化逻辑放在工厂里将对象创建后的业务逻辑剥离到客户端或其他服务类中。遵循单一职责原则。单元测试时无法轻松替换掉工厂创建的真实对象如真实的数据库Logger。工厂方法被声明为final或类是final无法被 Mock/Stub。或者客户端与具体工厂耦合。1. 确保工厂方法可被重写非final。2. 在测试中使用子类或 Mock 框架如 Mockito来创建一个返回 Mock 对象的测试专用工厂。这正是工厂方法模式提升可测试性的体现。觉得引入了太多抽象接口、抽象类代码变复杂了。在需求明确且不会变化的简单场景中使用了模式属于过度设计。“如无必要勿增实体”。在项目初期或逻辑简单时直接new可能是更优选择。当变化点出现时再运用重构手法引入工厂方法。5.2 最佳实践心得命名要清晰工厂类的命名应明确其创建的产品如XxxFactory、XxxCreator。工厂方法名常用createXxx()、makeXxx()、newInstance()等。考虑将工厂方法设为静态的吗通常不建议。静态工厂方法如LoggerFactory.createLogger()会阻碍通过继承来改变创建行为也使得子类无法重写工厂方法以返回不同的产品。它更接近“简单工厂”。仅在创建逻辑完全确定、无需多态性时考虑使用静态方法。与“抽象工厂模式”区分工厂方法针对的是“单个产品”的创建而抽象工厂针对的是“产品族”多个相关或依赖的产品的创建。例如一个 GUI 抽象工厂会创建一套匹配风格的按钮、文本框、对话框。如果你发现你的工厂类里有多个工厂方法且这些方法创建的产品是成组出现的你可能更需要抽象工厂模式。优先使用依赖注入在大型项目或使用 Spring 等框架时优先考虑使用框架的依赖注入机制来管理对象创建和依赖关系这比手动编写工厂类更强大、更便捷。手动工厂模式更适合在框架底层、工具类库或需要精细控制对象创建逻辑的场景中使用。文档化工厂的职责在工厂类或方法的注释中明确说明它创建的是什么对象以及是否对对象进行了特殊的初始化或配置。这有助于团队其他成员理解代码意图。工厂方法模式是一种强大的工具其核心价值在于通过多态将对象的创建与使用分离从而提高了代码的灵活性、可维护性和可测试性。它不仅仅是new关键字的替代品更是一种体现“依赖倒置”和“开闭原则”的架构思想。当你发现代码中散布着根据条件创建对象的逻辑时就是考虑引入工厂方法模式的好时机。记住设计模式是“术”而追求高内聚、低耦合的代码设计才是“道”。
返回列表