Spring中的設計模式--工廠方法模式
關於工廠方法模式的誤會
不太在意設計模式的同事會對工廠模式有極深的誤解,總會把“工廠模式”與“靜態工廠方法”混為一談,什麼是靜態工廠方法?看一個簡單的例子:
public class SimpleClientFactory {
public static Client createClient(){
return new Client();
}
}
通過一個靜態方法來建立例項,這種方式在程式碼中比較常見,但這並不是我們今天要說的工廠模式,它只是一個“靜態工廠方法”。
個人覺得很難給模式一個語句上的定義,因為這些模式本身只是一些幫助我們養成好的程式碼習慣的一些建議,它們甚至算不上是一種規範。對於工廠模式,我覺得某一段定義說的是比較準確的。
父類定義了建立物件的介面,但是由子類來具體實現,工廠方法讓類把例項化的動作推遲到了子類當中。
也就是說,父類知道什麼時候該去建立這個物件,也知道拿到這個物件之後應該對這個物件做什麼事情,但是不知道如何去建立這個物件,物件的建立由子類來完成。
之所以有這種設計模式,也是多年業務邏輯的積累導致,大多數業務場景下,對某一類物件總是要執行相同的流程,但是並不在意這些物件之間的微小差異,這種業務場景就非常符合工廠模式的設計。公共的父類決定了怎麼去處理這一類物件,而子類決定了如何建立這些有著微小差異的不同物件。
既然是工廠方法模式,那什麼是“工廠方法”?舉個基礎的例子:
// 這是一個網頁爬蟲類,它利用HttpClient來獲取資料並分析
public abstract class WebCrawler {
// 爬取網頁資料
public WebResult getWebInfo(String url) {
HttpClient c = getClient();
HtmlResult res = c.getPage(url);
return processHtml(res);
}
// HttpClient是介面或者抽象類,下文統稱為介面
private HttpClient getClient() {
// 如果快取中不存在client則建立
HttpClient c = getFromCache();
if (c == null) {
c = createClient();
}
// 建立之後對client進行初始化
initClient(c);
}
// 提供一個抽象讓子類來實現建立client
// 這個抽象方法就是“工廠方法”
protected abstract HttpClient createClient();
}
// A和B型別兩種client工廠都不需要關心建立client前的邏輯判斷以及建立後的流程處理,他們只關心建立物件
class ATypeCrawler extends WebCrawler {
HttpClient createClient() {
return new ATypeClient();
}
}
class BTypeCrawler extends WebCrawler {
HttpClient createClient() {
return new BTypeClient();
}
}
工廠方法模式能夠封裝具體型別的例項化,WebCrawler
提供了一個用於建立HttpClient
的方法的介面,這個方法也稱為“工廠方法”,在WebCrawler
中的任何方法在任何時候都可能會使用到這個“工廠方法”,但由子類具體實現這個“工廠方法”。
Spring中的工廠模式
Spring原始碼中有非常多的地方用到了工廠模式,幾乎是無處不見,但是筆者決定拿大家最為常用的Bean來說,用Spring很多程度上是依賴它的物件管理,也就是IoC容器對於Bean的管理,Spring的IoC容器如何建立和管理Bean其實是比較複雜的,它並不在我們此次的討論範圍中。我們關心的是Spring如何利用工廠模式來實現了更加優良J2EE鬆耦合設計。
接下來我們就一起檢視一下Spring中非常重要的一個類AbstractFactoryBean
是如何利用工廠模式的。
// AbstractFactoryBean.java
// 繼承了FactoryBean,工廠Bean的主要作用是為了實現getObject()返回Bean例項
public abstract class AbstractFactoryBean<T> implements FactoryBean<T>, BeanClassLoaderAware, BeanFactoryAware, InitializingBean, DisposableBean {
// 定義了獲取物件的前置判斷工作,建立物件的工作則交給了一個抽象方法
// 這裡判斷了Bean是不是單例並且是否已經被載入過了(未初始化但載入過了,這個問題涉及到Spring處理迴圈依賴,以後會討論到)
public final T getObject() throws Exception {
return this.isSingleton()?(this.initialized?this.singletonInstance:this.getEarlySingletonInstance()):this.createInstance();
}
// 由子類負責具體建立物件
protected abstract T createInstance() throws Exception;
}
之所以這麼寫是因為這種寫法帶來了兩個好處:
(1) 保證了建立Bean的方式的多樣性
Bean工廠有很多種,它們負責建立各種各樣不同的Bean,比如Map型別的Bean,List型別的Bean,Web服務Bean,子類們不需要關心單例或非單例情況下是否需要額外操作,只需要關心如何建立Bean,並且創建出來的Bean是多種多樣的。
(2) 嚴格規定了Bean建立前後的其它動作
雖然子類可以自由的去建立Bean,但是建立Bean之前的準備工作以及建立Bean之後對Bean的處理工作是AbstractFactoryBean設定好了的,子類不需要關心,也沒權力關心,在這個例子中父類只負責一些前置判斷工作。
工廠方法模式非常的有趣,它給了子類建立例項的自由,又嚴格的規定了例項建立前後的業務流程。
依賴倒置原則
工廠方法模式非常好的詮釋了面向物件六大設計原則之一的依賴倒置原則:要依賴抽象,不要依賴具體類。
對依賴倒置的原則這個解釋有點過於籠統,不太好理解,到底是哪些依賴被倒置了呢?
回想最開始的基礎例子,如果不使用工廠模式,我們的程式碼可能是這樣的
public class WebCrawler {
public WebResult getWebInfo(int clientType, String url) {
HttpClient c = getClient(clientType);
HtmlResult res = c.getPage(url);
return processHtml(res);
}
private HttpClient getClient(int clientType) {
HttpClient c = getFromCache();
if (c == null) {
c = createClient(clientType);
}
initClient(c);
}
// 根據不同的型別引數來建立不同的HttpClient
private HttpClient createClient(int clientType){
if (clientType == 1) {
return ATypeClient();
} else if (clientType == 2) {
return BTypeClient();
} else if (clientType == 3) {
return CTypeClient();
} else
......
}
}
上述程式碼最大的問題就是違背了開放-關閉原則,對擴充套件開放,對修改關閉。當有新的HttpClient
加入,則需要修改WebCrawler
類的程式碼,但是WebCrawler
並不關心具體的HttpClient
的具體型別,它只知道可以使用HttpClient
來獲取網頁資訊,然後它自己就可以對這些網頁資訊就行分析。目前的程式碼寫法導致WebCrawler
依賴於具體的HttpClient
實現類。
如果使用工廠模式,則可以避免這樣的尷尬,工廠模式使得WebCrawler
不必關心HttpClient
的具體型別,因為這些具體的HttpClient
是由子類具體建立的,自己根本不知道到底有哪些HttpClient
型別,它只關心使用。同樣的,各個子類也只管著建立HttpClient
的例項,至於這些例項被拿去做什麼事情,什麼時候做,它們並不知情。
按理說,高層元件應該依賴於低層元件,低層元件為高層元件提供一些最基礎的服務,但是工廠模式倒置了這一依賴現象,讓低層元件反而要依賴於統一的抽象介面。
工廠模式讓高層元件(WebCrawler)和低層元件(ATypeClient|BTypeClient|……)都依賴於共同的介面(HttpClient),這倒置了原本的依賴模型,解除了高層元件和低層元件之間的強依賴關係
小結
工廠模式是非常常用且容易理解的設計模式,它也很好的詮釋了六大原則之一的依賴倒置原則,能夠幫助寫出鬆耦合且方便擴充套件的程式碼。
要知道,在程式的世界,唯一不變的就是變化的需求,所以程式碼的可擴充套件性相當重要。