前言
工廠設計模式可能是最常用的設計模式之一,我想大家在自己的項目中都用到過??赡苣銜恍家活?,但這篇文章不僅僅是關于工廠模式的基本知識,更是討論如何在運行時動態(tài)選擇不同的方法進行執(zhí)行,你們可以看看是不是和你們項目中用的一樣?
小菜鳥的問題
直接上例子說明,設計一個日志記錄的功能,但是支持記錄到不同的地方,例如:
- 內(nèi)存中
- 磁盤上的文件
- 數(shù)據(jù)庫
- 百度網(wǎng)盤等遠程存儲服務
面對這么一個需求,你會怎么做呢?我們先來看看小菜鳥的做法吧。
- 小菜鳥創(chuàng)建了一個Logger類
class Logger {
public void log(String message, String loggerMedium) {}
}
- 小菜鳥想都不想,直接一通if else。
class Logger {
public void log(String message, String loggerMedium) {
if (loggerMedium.equals("MEMORY")) {
logInMemory(message);
} else if (loggerMedium.equals("FILE")) {
logOnFile(message);
} else if (loggerMedium.equals("DB")) {
logToDB(message);
} else if (loggerMedium.equals("REMOTE_SERVICE")) {
logToRemote(message);
}
}
private void logInMemory(String message) {
// Implementation
}
private void logOnFile(String message) {
// Implementation
}
private void logToDB(String message) {
// Implementation
}
private void logToRemote(String message) {
// Implementation
}
}
現(xiàn)在突然說要增加一種存儲介質(zhì)FLASH_DRIVE,就要改了這個類?不拍改錯嗎?也不符合“開閉原則”,而且隨著存儲介質(zhì)變多,類也會變的很大,小菜鳥懵逼了,不知道怎么辦?
有沒有更好的方法呢?
這時候小菜鳥去找你幫忙,你一頓操作,改成了下面這樣:
class InMemoryLog {
public void logToMemory(String message) {
// Implementation
}
}
class FileLog {
public void logToFile(String message) {
//Implementation
}
}
class DBLog {
public void logToDB(String message) {
// Implementation
}
}
class RemoteServiceLog {
public void logToService(String message) {
// Implementation
}
}
class Logger {
private InMemoryLog mLog;
private FileLog fLog;
private DBLog dbLog;
private RemoteServiceLog sLog;
public Logger() {
mLog = new InMemoryLog();
fLog = new FileLog();
dbLog = new DBLog();
sLog = new RemoteServiceLog();
}
public void log(String message, String loggerMedium) {
if (loggerMedium.equals("MEMORY")) {
mLog.logToMemory(message);
} else if (loggerMedium.equals("FILE")) {
fLog.logToFile(message);
} else if (loggerMedium.equals("DB")) {
dbLog.logToDB(message);
} else if (loggerMedium.equals("REMOTE_SERVICE")) {
sLog.logToService(message);
}
}
}
在這個實現(xiàn)中,你已經(jīng)將單獨的代碼分離到它們對應的文件中,但是Logger?類與存儲介質(zhì)的具體實現(xiàn)緊密耦合,如FileLog、DBLog?等。隨著存儲介質(zhì)的增加,類中將引入更多的實例Logger。
還有什么更好的辦法嗎?
你想了想,上面的實現(xiàn)都是直接寫具體的實現(xiàn)類,是面向?qū)崿F(xiàn)編程,更合理的做法是面向接口編程,接口意味著協(xié)議,契約,是一種更加穩(wěn)定的方式。
- 定義一個日志操作的接口
public interface LoggingOperation {
void log(String message);
}
- 實現(xiàn)這個接口
class InMemoryLog implements LoggingOperation {
public void log(String message) {
// Implementation
}
}
class FileLog implements LoggingOperation {
public void log(String message) {
//Implementation
}
}
class DBLog implements LoggingOperation {
public void log(String message) {
// Implementation
}
}
class RemoteServiceLog implements LoggingOperation {
public void log(String message) {
// Implementation
}
}
- 你定義了一個類,根據(jù)傳遞的參數(shù),在運行時動態(tài)選擇具體實現(xiàn),這就是所謂的工廠類,不過是基礎版。
class LoggerFactory {
public static LoggingOperation getInstance(String loggerMedium) {
LoggingOperation op = null;
switch (loggerMedium) {
case "MEMORY":
op = new InMemoryLog();
break;
case "FILE":
op = new FileLog();
break;
case "DB":
op = new DBLog();
break;
case "REMOTE_SERVICE":
op = new RemoteServiceLog();
break;
}
return op;
}
}
- 現(xiàn)在你的 Logger類的實現(xiàn)就是下面這個樣子了。
class Logger {
public void log(String message, String loggerMedium) {
LoggingOperation instance = LoggerFactory.getInstance(loggerMedium);
instance.log(message);
}
}
這里的代碼變得非常統(tǒng)一,創(chuàng)建實際存儲實例的責任已經(jīng)轉移到LoggerFactory?,各個存儲類只實現(xiàn)它們?nèi)绾螌⑾⒂涗浀剿鼈兊奶囟ń橘|(zhì),最后該類Logger?只關心通過LoggerFactory?將實際的日志記錄委托給具體的實現(xiàn)。這樣,代碼就很松耦合了。你想要添加一個新的存儲介質(zhì),例如FLASH_DRIVE?,只需創(chuàng)建一個實現(xiàn)LoggingOperation?接口的新類并將其注冊到LoggerFactory中就好了。這就是工廠模式可以幫助您動態(tài)選擇實現(xiàn)的方式。
還能做得更好嗎?
你已經(jīng)完成了一個松耦合的設計,但是想象一下假如有數(shù)百個存儲介質(zhì)的場景,所以我們最終會在工廠類LoggerFactory?中的switch case?部分case數(shù)百個。這看起來還是很糟糕,如果管理不當,它有可能成為技術債務,這該怎么辦呢?
擺脫不斷增長的if else?或者 switch case?的一種方法是維護類中所有實現(xiàn)類的列表,LoggerFactory代碼如下所示:
class LoggerFactory {
private static final List<LoggingOperation> instances = new ArrayList<>();
static {
instances.addAll(Arrays.asList(
new InMemoryLog(),
new FileLog(),
new DBLog(),
new RemoteServiceLog()
));
}
public static LoggingOperation getInstance(ApplicationContext context, String loggerMedium) {
for(LoggingOperation op : instances) {
// 比如判斷StrUtil.equals(loggerMedium, op.getType()) op本身添加一個type
}
return null;
}
}
但是請注意,還不夠,在所有上述實現(xiàn)中,無論if else、switch case? 還是上面的做法,都是讓存儲實現(xiàn)與LoggerFactory?緊密耦合的。你添加一種實現(xiàn),就要修改LoggerFactory,有什么更好的做法嗎?
逆向思維一下,我們是不是讓具體的實現(xiàn)主動注冊上來呢?通過這種方式,工廠不需要知道系統(tǒng)中有哪些實例可用,而是實例本身會注冊并且如果它們在系統(tǒng)中可用,工廠就會為它們提供服務。具體代碼如下:
class LoggerFactory {
private static final Map<String, LoggingOperation> instances = new HashMap<>();
public static void register(String loggerMedium, LoggingOperation instance) {
if (loggerMedium != null && instance != null) {
instances.put(loggerMedium, instance);
}
}
public static LoggingOperation getInstance(String loggerMedium) {
if (instances.containsKey(loggerMedium)) {
return instances.get(loggerMedium);
}
return null;
}
}
在這里,LoggerFactory?提供了一個register?注冊的方法,具體的存儲實現(xiàn)可以調(diào)用該方法注冊上來,保存在工廠的instancesmap對象中。
我們來看看具體的存儲實現(xiàn)注冊的代碼如下:
class RemoteServiceLog implements LoggingOperation {
static {
LoggerFactory.register("REMOTE", new RemoteServiceLog());
}
public void log(String message) {
// Implementation
}
}
由于注冊應該只發(fā)生一次,所以它發(fā)生在static類加載器加載存儲類時的塊中。
但是又有一個問題,默認情況下JVM不加載類RemoteServiceLog,除非它由應用程序在外部實例化或調(diào)用。因此,盡管存儲類有注冊的代碼,但實際上注冊并不會發(fā)生,因為沒有被JVM加載,不會調(diào)用static代碼塊中的代碼, 你又犯難了。
你靈機一動,LoggerFactory是獲取存儲實例的入口點,能否在這個類上做點文章,就寫下了下面的代碼:
class LoggerFactory {
private static final Map<String, LoggingOperation> instances = new HashMap<>();
static {
try {
loadClasses(LoggerFactory.class.getClassLoader(), "com.alvin.storage.impl");
} catch (Exception e) {
// log or throw exception.
}
}
public static void register(String loggerMedium, LoggingOperation instance) {
if (loggerMedium != null && instance != null) {
instances.put(loggerMedium, instance);
}
}
public static LoggingOperation getInstance(String loggerMedium) {
if (instances.containsKey(loggerMedium)) {
return instances.get(loggerMedium);
}
return null;
}
private static void loadClasses(ClassLoader cl, String packagePath) throws Exception {
String dottedPackage = packagePath.replaceAll("[/]", ".");
URL upackage = cl.getResource(packagePath);
URLConnection conn = upackage.openConnection();
String rr = IOUtils.toString(conn.getInputStream(), "UTF-8");
if (rr != null) {
String[] paths = rr.split("\n");
for (String p : paths) {
if (p.endsWith(".class")) {
Class.forName(dottedPackage + "." + p.substring(0, p.lastIndexOf('.')));
}
}
}
}
}
在上面的實現(xiàn)中,你使用了一個名為loadClasses?的方法,該方法掃描提供的包名稱com.alvin.storage.impl?并將駐留在該目錄中的所有類加載到類加載器。以這種方式,當類加載時,它們的static?塊被初始化并且它們將自己注冊到LoggerFactory中。
如何在 SpringBoot 中實現(xiàn)此技術?
你突然發(fā)現(xiàn)你的是springboot應用,突然想到有更方便的解決方案。
因為你的存儲實現(xiàn)類都被標記上注解@Component?,這樣 Spring? 會在應用程序啟動時自動加載類,它們會自行注冊,在這種情況下你不需要使用loadClasses?功能,Spring 會負責加載類。具體的代碼實現(xiàn)如下:
class LoggerFactory {
private static final Map<String, Class<? extends LoggingOperation>> instances = new HashMap<>();
public static void register(String loggerMedium, Class<? extends LoggingOperation> instance) {
if (loggerMedium != null && instance != null) {
instances.put(loggerMedium, instance);
}
}
public static LoggingOperation getInstance(ApplicationContext context, String loggerMedium) {
if (instances.containsKey(loggerMedium)) {
return context.getBean(instances.get(loggerMedium));
}
return null;
}
}
getInstance?需要傳入ApplicationContext對象,這樣就可以根據(jù)類型獲取具體的實現(xiàn)了。
修改所有存儲實現(xiàn)類,如下所示:
import org.springframework.stereotype.Component;
@Component
class RemoteServiceLog implements LoggingOperation {
static {
LoggerFactory.register("REMOTE", RemoteServiceLog.class);
}
public void log(String message) {
// Implementation
}
}
總結
我們通過一個例子,不斷迭代帶大家理解了工廠模式,工廠模式是一種創(chuàng)建型設計模式,用于創(chuàng)建同一類型的不同實現(xiàn)對象。我們來總結下這種動態(tài)選擇對象工廠模式的優(yōu)缺點。
優(yōu)點:
- 容易管理。在添加新的存儲類時,只需將該類放入特定包中,在static代碼塊中注冊它自己到工廠中。
- 松耦合,當您添加新的存儲實現(xiàn)時,您不需要在工廠類中進行任何更改。
- 遵循SOLID編程原則。
缺點:
- 如果是用原生通過類加載的方式,代價比較大,因為它涉及 I/O 操作。但是如果使用的是SpringBoot,則無需擔心,因為框架本身會調(diào)用組件。
- 需要額外編寫一個static塊,注冊自己到工廠中,一不小心就遺漏了。