Java工廠模式用法之如何動(dòng)態(tài)選擇對(duì)象詳解
前言
工廠設(shè)計(jì)模式可能是最常用的設(shè)計(jì)模式之一,我想大家在自己的項(xiàng)目中都用到過(guò)??赡苣銜?huì)不屑一顧,但這篇文章不僅僅是關(guān)于工廠模式的基本知識(shí),更是討論如何在運(yùn)行時(shí)動(dòng)態(tài)選擇不同的方法進(jìn)行執(zhí)行,你們可以看看是不是和你們項(xiàng)目中用的一樣?
小菜鳥的問(wèn)題
直接上例子說(shuō)明,設(shè)計(jì)一個(gè)日志記錄的功能,但是支持記錄到不同的地方,例如:
- 內(nèi)存中
- 磁盤上的文件
- 數(shù)據(jù)庫(kù)
- 百度網(wǎng)盤等遠(yuǎn)程存儲(chǔ)服務(wù)
面對(duì)這么一個(gè)需求,你會(huì)怎么做呢?我們先來(lái)看看小菜鳥的做法吧。
小菜鳥創(chuàng)建了一個(gè)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)在突然說(shuō)要增加一種存儲(chǔ)介質(zhì)FLASH_DRIVE
,就要改了這個(gè)類?不拍改錯(cuò)嗎?也不符合“開閉原則”,而且隨著存儲(chǔ)介質(zhì)變多,類也會(huì)變的很大,小菜鳥懵逼了,不知道怎么辦?
有沒(méi)有更好的方法呢
這時(shí)候小菜鳥去找你幫忙,你一頓操作,改成了下面這樣:
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); } } }
在這個(gè)實(shí)現(xiàn)中,你已經(jīng)將單獨(dú)的代碼分離到它們對(duì)應(yīng)的文件中,但是Logger
類與存儲(chǔ)介質(zhì)的具體實(shí)現(xiàn)緊密耦合,如FileLog
、DBLog
等。隨著存儲(chǔ)介質(zhì)的增加,類中將引入更多的實(shí)例Logger
。
還有什么更好的辦法嗎
你想了想,上面的實(shí)現(xiàn)都是直接寫具體的實(shí)現(xiàn)類,是面向?qū)崿F(xiàn)編程,更合理的做法是面向接口編程,接口意味著協(xié)議,契約,是一種更加穩(wěn)定的方式。
定義一個(gè)日志操作的接口
public interface LoggingOperation { void log(String message); }
實(shí)現(xiàn)這個(gè)接口
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 } }
你定義了一個(gè)類,據(jù)傳遞的參數(shù),在運(yùn)行時(shí)動(dòng)態(tài)選擇具體實(shí)現(xiàn),這就是所謂的工廠類,不過(guò)是基礎(chǔ)版。
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
類的實(shí)現(xiàn)就是下面這個(gè)樣子了。
class Logger { public void log(String message, String loggerMedium) { LoggingOperation instance = LoggerFactory.getInstance(loggerMedium); instance.log(message); } }
這里的代碼變得非常統(tǒng)一,創(chuàng)建實(shí)際存儲(chǔ)實(shí)例的責(zé)任已經(jīng)轉(zhuǎn)移到LoggerFactory
,各個(gè)存儲(chǔ)類只實(shí)現(xiàn)它們?nèi)绾螌⑾⒂涗浀剿鼈兊奶囟ń橘|(zhì),最后該類Logger
只關(guān)心通過(guò)LoggerFactory
將實(shí)際的日志記錄委托給具體的實(shí)現(xiàn)。這樣,代碼就很松耦合了。你想要添加一個(gè)新的存儲(chǔ)介質(zhì),例如FLASH_DRIVE
,只需創(chuàng)建一個(gè)實(shí)現(xiàn)LoggingOperation
接口的新類并將其注冊(cè)到LoggerFactory
中就好了。這就是工廠模式可以幫助您動(dòng)態(tài)選擇實(shí)現(xiàn)的方式。
還能做得更好嗎
你已經(jīng)完成了一個(gè)松耦合的設(shè)計(jì),但是想象一下假如有數(shù)百個(gè)存儲(chǔ)介質(zhì)的場(chǎng)景,所以我們最終會(huì)在工廠類LoggerFactory
中的switch case
部分case
數(shù)百個(gè)。這看起來(lái)還是很糟糕,如果管理不當(dāng),它有可能成為技術(shù)債務(wù),這該怎么辦呢?
擺脫不斷增長(zhǎng)的if else
或者 switch case
的一種方法是維護(hù)類中所有實(shí)現(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本身添加一個(gè)type } return null; } }
但是請(qǐng)注意,還不夠,在所有上述實(shí)現(xiàn)中,無(wú)論if else、switch case
還是上面的做法,都是讓存儲(chǔ)實(shí)現(xiàn)與LoggerFactory
緊密耦合的。你添加一種實(shí)現(xiàn),就要修改LoggerFactory
,有什么更好的做法嗎?
逆向思維一下,我們是不是讓具體的實(shí)現(xiàn)主動(dòng)注冊(cè)上來(lái)呢?通過(guò)這種方式,工廠不需要知道系統(tǒng)中有哪些實(shí)例可用,而是實(shí)例本身會(huì)注冊(cè)并且如果它們?cè)谙到y(tǒng)中可用,工廠就會(huì)為它們提供服務(wù)。具體代碼如下:
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
提供了一個(gè)register
注冊(cè)的方法,具體的存儲(chǔ)實(shí)現(xiàn)可以調(diào)用該方法注冊(cè)上來(lái),保存在工廠的instances
map對(duì)象中。
我們來(lái)看看具體的存儲(chǔ)實(shí)現(xiàn)注冊(cè)的代碼如下:
class RemoteServiceLog implements LoggingOperation { static { LoggerFactory.register("REMOTE", new RemoteServiceLog()); } public void log(String message) { // Implementation } }
由于注冊(cè)應(yīng)該只發(fā)生一次,所以它發(fā)生在static
類加載器加載存儲(chǔ)類時(shí)的塊中。
但是又有一個(gè)問(wèn)題,默認(rèn)情況下JVM不加載類RemoteServiceLog
,除非它由應(yīng)用程序在外部實(shí)例化或調(diào)用。因此,盡管存儲(chǔ)類有注冊(cè)的代碼,但實(shí)際上注冊(cè)并不會(huì)發(fā)生,因?yàn)闆](méi)有被JVM加載,不會(huì)調(diào)用static代碼塊中的代碼, 你又犯難了。
你靈機(jī)一動(dòng),LoggerFactory
是獲取存儲(chǔ)實(shí)例的入口點(diǎn),能否在這個(gè)類上做點(diǎn)文章,就寫下了下面的代碼:
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('.'))); } } } } }
在上面的實(shí)現(xiàn)中,你使用了一個(gè)名為loadClasses
的方法,該方法掃描提供的包名稱com.alvin.storage.impl
并將駐留在該目錄中的所有類加載到類加載器。以這種方式,當(dāng)類加載時(shí),它們的static
塊被初始化并且它們將自己注冊(cè)到LoggerFactory
中。
如何在 SpringBoot 中實(shí)現(xiàn)此技術(shù)
你突然發(fā)現(xiàn)你的應(yīng)用是springboot應(yīng)用,突然想到有更方便的解決方案。
因?yàn)槟愕拇鎯?chǔ)實(shí)現(xiàn)類都被標(biāo)記上注解@Component
,這樣 Spring
會(huì)在應(yīng)用程序啟動(dòng)時(shí)自動(dòng)加載類,它們會(huì)自行注冊(cè),在這種情況下你不需要使用loadClasses
功能,Spring
會(huì)負(fù)責(zé)加載類。具體的代碼實(shí)現(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
對(duì)象,這樣就可以根據(jù)類型獲取具體的實(shí)現(xiàn)了。
修改所有存儲(chǔ)實(shí)現(xiàn)類,如下所示:
import org.springframework.stereotype.Component; @Component class RemoteServiceLog implements LoggingOperation { static { LoggerFactory.register("REMOTE", RemoteServiceLog.class); } public void log(String message) { // Implementation } }
總結(jié)
我們通過(guò)一個(gè)例子,不斷迭代帶大家理解了工廠模式,工廠模式是一種創(chuàng)建型設(shè)計(jì)模式,用于創(chuàng)建同一類型的不同實(shí)現(xiàn)對(duì)象。我們來(lái)總結(jié)下這種動(dòng)態(tài)選擇對(duì)象工廠模式的優(yōu)缺點(diǎn)。
優(yōu)點(diǎn):
- 容易管理。在添加新的存儲(chǔ)類時(shí),只需將該類放入特定包中,在static代碼塊中注冊(cè)它自己到工廠中。
- 松耦合,當(dāng)您添加新的存儲(chǔ)實(shí)現(xiàn)時(shí),您不需要在工廠類中進(jìn)行任何更改。
- 遵循SOLID編程原則。
缺點(diǎn):
- 如果是用原生通過(guò)類加載的方式,代價(jià)比較大,因?yàn)樗婕?I/O 操作。但是如果使用的是SpringBoot,則無(wú)需擔(dān)心,因?yàn)榭蚣鼙旧頃?huì)調(diào)用組件。
- 需要額外編寫一個(gè)
static
塊,注冊(cè)自己到工廠中,一不小心就遺漏了。
到此這篇關(guān)于Java工廠模式用法之如何動(dòng)態(tài)選擇對(duì)象詳解的文章就介紹到這了,更多相關(guān)Java工廠模式動(dòng)態(tài)選擇對(duì)象內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java使用CollectionUtils工具類判斷集合是否為空方式
這篇文章主要介紹了java使用CollectionUtils工具類判斷集合是否為空方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-02-02Spring Boot打開URL出現(xiàn)signin問(wèn)題的解決
這篇文章主要介紹了Spring Boot打開URL出現(xiàn)signin問(wèn)題的解決,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12springboot啟動(dòng)后和停止前執(zhí)行方法示例詳解
這篇文章主要介紹了springboot啟動(dòng)后和停止前執(zhí)行方法,本文通過(guò)示例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-08-08Java Runtime類詳解_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
Runtime類封裝了運(yùn)行時(shí)的環(huán)境。每個(gè) Java 應(yīng)用程序都有一個(gè) Runtime 類實(shí)例,使應(yīng)用程序能夠與其運(yùn)行的環(huán)境相連接。下面通過(guò)本文給大家分享Java Runtime類詳解,需要的朋友參考下吧2017-04-04解決Process.getInputStream()阻塞的問(wèn)題
這篇文章主要介紹了解決Process.getInputStream()阻塞的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-06-06Maven包沖突導(dǎo)致NoSuchMethodError錯(cuò)誤的解決辦法
web 項(xiàng)目 能正常編譯,運(yùn)行時(shí)也正常啟動(dòng),但執(zhí)行到需要調(diào)用 org.codehaus.jackson 包中的某個(gè)方法時(shí),產(chǎn)生運(yùn)行異常,這篇文章主要介紹了Maven包沖突導(dǎo)致NoSuchMethodError錯(cuò)誤的解決辦法,需要的朋友可以參考下2024-05-05使用IntelliJ IDEA 2017.2.5 x64中的Spring Initializr插件快速創(chuàng)建Spring
這篇文章主要介紹了使用IntelliJ IDEA 2017.2.5 x64中的Spring Initializr插件快速創(chuàng)建Spring Boot/Cloud工程(圖解),需要的朋友可以參考下2018-01-01源碼閱讀之storm操作zookeeper-cluster.clj
這篇文章主要介紹了源碼閱讀之storm操作zookeeper-cluster.clj的相關(guān)內(nèi)容,對(duì)其源碼進(jìn)行了簡(jiǎn)要分析,具有參考意義,需要的朋友可以了解下。2017-10-10