“工廠”模式在各種框架中非常常見,包括 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
字符串間接暴露了具體的實現類名。