设计杂谈-工厂模式
“工厂”模式在各种框架中非常常见,包括 MyBatis,它是一种创建对象的设计模式。使用工厂模式有很多好处,尤其是在复杂的框架中,它可以带来更好的灵活性、可维护性和可配置性。
让我们以 MyBatis 为例,来理解工厂模式及其优点:
MyBatis 中的工厂:SqlSessionFactoryBuilder
和 SqlSessionFactory
在 MyBatis 中,主要的工厂类是 SqlSessionFactoryBuilder
和 SqlSessionFactory
。
-
SqlSessionFactoryBuilder
(构建器):- 它的作用是读取 MyBatis 的配置文件(例如
mybatis-config.xml
)或通过 Java 代码构建Configuration
对象。Configuration
对象包含了 MyBatis 的所有配置信息,例如数据源、事务管理器、映射器等。 SqlSessionFactoryBuilder
的生命周期很短。一旦SqlSessionFactory
被创建出来,SqlSessionFactoryBuilder
通常就不再需要了。你可以把它想象成一个“临时的工厂的建造者”。
- 它的作用是读取 MyBatis 的配置文件(例如
-
SqlSessionFactory
(会话工厂):- 它的作用是根据
Configuration
对象创建一个SqlSession
对象。SqlSession
是 MyBatis 中与数据库交互的核心接口,通过它你可以执行 SQL 语句、管理事务等。 SqlSessionFactory
的生命周期通常是整个应用的生命周期。它是一个“持久的工厂”,负责生产SqlSession
。
- 它的作用是根据
使用工厂模式的好处(以 MyBatis 为例):
-
封装对象的创建过程:
- 工厂模式将对象的创建逻辑封装在一个或多个工厂类中。在 MyBatis 的例子中,创建
SqlSession
的复杂过程被封装在SqlSessionFactory
中。 - 客户端代码(你的业务代码)不需要知道创建
SqlSession
的具体细节,只需要从SqlSessionFactory
获取即可。这降低了客户端代码的复杂性。
- 工厂模式将对象的创建逻辑封装在一个或多个工厂类中。在 MyBatis 的例子中,创建
-
解耦对象的创建和使用:
- 工厂模式将对象的创建和使用分离。你的业务代码依赖的是
SqlSession
接口,而SqlSession
的具体实现是由SqlSessionFactory
负责创建的。 - 这种解耦使得在需要更换
SqlSession
的实现或者修改其创建方式时,你的业务代码不需要做大的改动,只需要修改工厂的配置即可。
- 工厂模式将对象的创建和使用分离。你的业务代码依赖的是
-
提高灵活性和可配置性:
- 通过配置文件(
mybatis-config.xml
)或编程方式配置SqlSessionFactoryBuilder
,你可以灵活地指定 MyBatis 的各种行为,例如使用哪个数据源、事务管理器、是否开启缓存等等。 SqlSessionFactory
会根据这些配置创建出具有相应特性的SqlSession
。这使得框架具有很高的可配置性。
- 通过配置文件(
-
隐藏对象的创建细节:
- 工厂可以隐藏对象创建的复杂性,例如对象的初始化参数、依赖关系等。客户端只需要简单地向工厂请求对象,而不需要关心这些内部细节。
- 在 MyBatis 中,
SqlSessionFactory
负责处理数据源的创建、连接池的管理等复杂细节,客户端只需要获取SqlSession
来执行 SQL。
-
控制对象的生命周期:
- 工厂可以控制所创建对象的生命周期。例如,
SqlSessionFactory
可以管理数据源和连接池的生命周期,而SqlSession
的生命周期通常是请求级别的。
- 工厂可以控制所创建对象的生命周期。例如,
-
易于扩展和维护:
- 当需要引入新的实现或者修改对象的创建逻辑时,只需要修改工厂类或其配置,而不需要修改所有使用该对象的客户端代码。这提高了框架的可扩展性和可维护性。
为了更直观地理解工厂模式的优势,我将提供一个简单的场景,分别用不用工厂模式来实现,并对比它们的差异。
场景:创建不同类型的日志记录器
假设我们需要根据配置创建不同类型的日志记录器,目前有两种:控制台日志记录器 (ConsoleLogger
) 和文件日志记录器 (FileLogger
)。
1. 不使用工厂模式的实现
interface Logger {void log(String message);
}class ConsoleLogger implements Logger {@Overridepublic void log(String message) {System.out.println("[Console] " + message);}
}class FileLogger implements Logger {private String filePath;public FileLogger(String filePath) {this.filePath = filePath;// 初始化文件写入器等...System.out.println("FileLogger initialized with path: " + filePath);}@Overridepublic void log(String message) {// 将消息写入文件System.out.println("[File] " + message + " (written to " + filePath + ")");}
}public class LoggingServiceWithoutFactory {private String loggerType;private String fileLogPath;public LoggingServiceWithoutFactory(String loggerType, String fileLogPath) {this.loggerType = loggerType;this.fileLogPath = fileLogPath;}public Logger getLogger() {if ("console".equalsIgnoreCase(loggerType)) {return new ConsoleLogger();} else if ("file".equalsIgnoreCase(loggerType)) {return new FileLogger(fileLogPath);} else {throw new IllegalArgumentException("Unsupported logger type: " + loggerType);}}public void logMessage(String message) {Logger logger = getLogger();logger.log(message);}public static void main(String[] args) {LoggingServiceWithoutFactory consoleService = new LoggingServiceWithoutFactory("console", null);consoleService.logMessage("Log to console.");LoggingServiceWithoutFactory fileService = new LoggingServiceWithoutFactory("file", "app.log");fileService.logMessage("Log to file.");// 如果要添加新的日志记录器类型,需要修改 LoggingServiceWithoutFactory}
}
缺点(不使用工厂模式):
- 紧耦合:
LoggingServiceWithoutFactory
类直接依赖于ConsoleLogger
和FileLogger
的具体实现。如果添加新的日志记录器类型,你需要修改getLogger()
方法。 - 违反开闭原则: 对修改开放(需要修改
getLogger()
),对扩展关闭(不容易在不修改现有代码的情况下添加新的日志记录器)。 - 创建逻辑分散: 创建不同类型
Logger
的逻辑集中在一个getLogger()
方法中,如果创建逻辑变得复杂,这个方法会变得难以维护。 - 客户端需要知道具体的类名:
LoggingServiceWithoutFactory
的构造函数需要知道loggerType
字符串,这间接暴露了具体的实现类名。
2. 使用工厂模式的实现
interface Logger {void log(String message);
}class ConsoleLogger implements Logger {@Overridepublic void log(String message) {System.out.println("[Console] " + message);}
}class FileLogger implements Logger {private String filePath;public FileLogger(String filePath) {this.filePath = filePath;System.out.println("FileLogger initialized with path: " + filePath);}@Overridepublic void log(String message) {System.out.println("[File] " + message + " (written to " + filePath + ")");}
}// 日志记录器工厂接口
interface LoggerFactory {Logger createLogger();
}// 控制台日志记录器工厂
class ConsoleLoggerFactory implements LoggerFactory {@Overridepublic Logger createLogger() {return new ConsoleLogger();}
}// 文件日志记录器工厂
class FileLoggerFactory implements LoggerFactory {private String filePath;public FileLoggerFactory(String filePath) {this.filePath = filePath;}@Overridepublic Logger createLogger() {return new FileLogger(filePath);}
}public class LoggingServiceWithFactory {private LoggerFactory loggerFactory;public LoggingServiceWithFactory(LoggerFactory loggerFactory) {this.loggerFactory = loggerFactory;}public void logMessage(String message) {Logger logger = loggerFactory.createLogger();logger.log(message);}public static void main(String[] args) {LoggerFactory consoleFactory = new ConsoleLoggerFactory();LoggingServiceWithFactory consoleService = new LoggingServiceWithFactory(consoleFactory);consoleService.logMessage("Log to console.");LoggerFactory fileFactory = new FileLoggerFactory("app.log");LoggingServiceWithFactory fileService = new LoggingServiceWithFactory(fileFactory);fileService.logMessage("Log to file.");// 要添加新的日志记录器类型,只需要创建新的 Logger 和 LoggerFactory}
}
优点(使用工厂模式):
- 解耦:
LoggingServiceWithFactory
类依赖于LoggerFactory
接口,而不是具体的Logger
实现。具体的Logger
对象的创建由相应的工厂负责。 - 符合开闭原则: 要添加新的日志记录器类型,你只需要创建新的
Logger
类和对应的LoggerFactory
类,而不需要修改LoggingServiceWithFactory
的代码。 - 职责分离: 对象创建的逻辑被委托给专门的工厂类,使得
LoggingServiceWithFactory
专注于日志记录的服务逻辑。 - 隐藏实现细节:
LoggingServiceWithFactory
的构造函数接收LoggerFactory
接口,不需要知道具体的Logger
实现类名。 - 更灵活的对象创建: 工厂可以包含更复杂的对象创建逻辑,例如读取配置文件、依赖注入等。
对比总结:
特性 | 不使用工厂模式 | 使用工厂模式 |
---|---|---|
耦合度 | 高,直接依赖具体实现 | 低,依赖抽象(接口) |
开闭原则 | 违反,添加新类型需要修改现有代码 | 符合,添加新类型只需创建新类 |
创建逻辑 | 集中在 getLogger() 方法中 | 分散在不同的工厂类中 |
灵活性 | 较低,不易于扩展和修改 | 较高,易于扩展和修改 |
客户端依赖 | 间接依赖具体实现类名 | 依赖抽象工厂接口 |
维护性 | 随着类型的增加,getLogger() 方法变得难以维护 | 每个工厂类职责单一,更易于维护 |
咱们用最简单的大白话总结一下“工厂模式”是干啥的,以及为啥像 MyBatis 这样的框架爱用它:
想象一下你要买不同口味的冰淇淋:
不用工厂模式就像这样:
- 你直接跑到冰柜前,自己翻箱倒柜地找你想要的口味(比如草莓味、巧克力味)。
- 如果下次出了个新口味(比如抹茶味),你就得知道这个新口味的名字,然后自己去冰柜里找。
- 如果冰淇淋的制作过程很复杂(比如要加很多配料、特殊冷冻),你买的时候也得稍微了解一下,不然可能买错。
用工厂模式就像这样:
- 你不去冰柜里直接找,而是找到一个“冰淇淋工厂的售货员”(这就是“工厂”)。
- 你只需要告诉售货员你想要什么口味(比如“草莓味”)。
- 售货员知道去哪里、怎么给你拿出正确的冰淇淋。
- 如果出了新口味,你只需要告诉售货员这个新口味的名字,售货员自然会去工厂里帮你拿。
- 你不需要知道冰淇淋是怎么做的,售货员(工厂)帮你处理好了一切。
总结一下“工厂模式”:
- 简单来说: 就是专门找一个“家伙”(工厂)来帮你创建你需要的“东西”(对象),而不是你自己去直接创建。
- 好处就像上面的冰淇淋例子:
- 更省事: 你不用自己操心“东西”是怎么被创建出来的,交给工厂就行。
- 更灵活: 如果想换一种“东西”或者创建“东西”的方式变了,你只需要告诉工厂,不用改你自己的用法。
- 更好管理: 创建“东西”的逻辑都放在工厂里,管理起来更方便,不会乱糟糟地散在各处。
为啥像 MyBatis 这样的框架爱用工厂模式?
MyBatis 需要创建很多跟数据库打交道的“东西”(比如 SqlSession
,就是用来执行 SQL 的)。创建这些“东西”可能挺复杂的,需要配置很多信息(连接哪个数据库、用什么方式等等)。
用了“工厂模式”(SqlSessionFactory
就是个工厂),你的代码就不用去管这些复杂的创建过程了,只需要跟工厂说“给我一个能干活的 SqlSession
”,工厂就会根据它的配置帮你弄好。
这样一来:
- 你的代码更干净: 不用一堆创建
SqlSession
的复杂代码。 - MyBatis 更灵活: 如果你想换个数据库或者改一下连接方式,只需要改一下 MyBatis 的配置(告诉工厂),你的代码基本不用动。
LoggingServiceWithoutFactory 的构造函数需要知道 loggerType 字符串,这间接暴露了具体的实现类名。 为什么呢
在 LoggingServiceWithoutFactory
的构造函数中:
public LoggingServiceWithoutFactory(String loggerType, String fileLogPath) {this.loggerType = loggerType;this.fileLogPath = fileLogPath;
}
以及在 getLogger()
方法中:
public Logger getLogger() {if ("console".equalsIgnoreCase(loggerType)) {return new ConsoleLogger();} else if ("file".equalsIgnoreCase(loggerType)) {return new FileLogger(fileLogPath);} else {throw new IllegalArgumentException("Unsupported logger type: " + loggerType);}
}
为什么说构造函数需要知道 loggerType
字符串间接暴露了具体的实现类名?
-
字符串
loggerType
的含义: 传递给构造函数的loggerType
字符串(例如"console"
或"file"
)并不是一个抽象的概念,而是直接对应着你希望创建的具体日志记录器类的名称(或其简写)。 -
getLogger()
方法的逻辑:getLogger()
方法内部的if
和else if
语句会根据loggerType
字符串的值来硬编码地创建具体的Logger
实现类的实例 (new ConsoleLogger()
和new FileLogger(fileLogPath)
)。 -
客户端代码的依赖: 当客户端代码创建
LoggingServiceWithoutFactory
的实例时,它必须知道要使用哪个loggerType
字符串,而这个字符串的选择直接决定了最终会创建哪个具体的Logger
实现类的对象。例如,在
main
方法中:
LoggingServiceWithoutFactory consoleService = new LoggingServiceWithoutFactory("console", null); // 客户端需要知道 "console" 对应 ConsoleLogger
LoggingServiceWithoutFactory fileService = new LoggingServiceWithoutFactory("file", "app.log"); // 客户端需要知道 "file" 对应 FileLogger
-
这里,客户端代码需要使用字符串
"console"
来请求一个控制台日志记录器,使用字符串"file"
来请求一个文件日志记录器。这些字符串与具体的类名ConsoleLogger
和FileLogger
之间存在着直接的、虽然是通过字符串间接的关联。 -
修改的影响: 如果你想要添加一个新的日志记录器类型(比如
DatabaseLogger
),你需要修改LoggingServiceWithoutFactory
的getLogger()
方法,增加一个新的else if
分支来创建DatabaseLogger
的实例。同时,客户端代码也需要知道使用一个新的字符串(比如"database"
)来请求这个新的日志记录器。
对比使用工厂模式的情况:
在使用工厂模式的例子中,LoggingServiceWithFactory
的构造函数接收的是 LoggerFactory
接口:
public LoggingServiceWithFactory(LoggerFactory loggerFactory) {this.loggerFactory = loggerFactory;
}
客户端代码直接传递一个实现了 LoggerFactory
接口的具体工厂对象(例如 ConsoleLoggerFactory
或 FileLoggerFactory
):
LoggerFactory consoleFactory = new ConsoleLoggerFactory();
LoggingServiceWithFactory consoleService = new LoggingServiceWithFactory(consoleFactory);LoggerFactory fileFactory = new FileLoggerFactory("app.log");
LoggingServiceWithFactory fileService = new LoggingServiceWithFactory(fileFactory);
在这里,LoggingServiceWithFactory
不直接依赖于具体的 Logger
实现类名,而是依赖于一个抽象的工厂接口。客户端代码虽然仍然需要知道具体的工厂类名,但 LoggingServiceWithFactory
本身与具体的 Logger
实现类解耦了。
总结:
在不使用工厂模式的例子中,loggerType
字符串充当了一个“配置标识符”,客户端代码通过这个标识符间接地告诉 LoggingServiceWithoutFactory
需要创建哪个具体的 Logger
实现类的对象。虽然没有直接使用类名,但字符串的值与具体的类名之间存在着明确的映射关系,这仍然是一种形式的依赖,使得添加新的日志记录器类型需要修改 LoggingServiceWithoutFactory
类的代码。这就是为什么说构造函数需要知道 loggerType
字符串间接暴露了具体的实现类名。