設(shè)計(jì)模式——工廠方法模式
工廠方法模式
亦稱(chēng):虛擬構(gòu)造函數(shù)、Virtual Constructor、Factory Method
意圖
工廠方法模式是一種創(chuàng)建型設(shè)計(jì)模式, 其在父類(lèi)中提供一個(gè)創(chuàng)建對(duì)象的方法, 允許子類(lèi)決定實(shí)例化對(duì)象的類(lèi)型。

問(wèn)題
假設(shè)你正在開(kāi)發(fā)一款物流管理應(yīng)用。最初版本只能處理卡車(chē)運(yùn)輸, 因此大部分代碼都在位于名為 卡車(chē)的類(lèi)中。
一段時(shí)間后, 這款應(yīng)用變得極受歡迎。你每天都能收到十幾次來(lái)自海運(yùn)公司的請(qǐng)求, 希望應(yīng)用能夠支持海上物流功能。

如果代碼其余部分與現(xiàn)有類(lèi)已經(jīng)存在耦合關(guān)系, 那么向程序中添加新類(lèi)其實(shí)并沒(méi)有那么容易。
這可是個(gè)好消息。但是代碼問(wèn)題該如何處理呢?目前, 大部分代碼都與 卡車(chē)類(lèi)相關(guān)。在程序中添加 輪船類(lèi)需要修改全部代碼。更糟糕的是, 如果你以后需要在程序中支持另外一種運(yùn)輸方式, 很可能需要再次對(duì)這些代碼進(jìn)行大幅修改。
最后, 你將不得不編寫(xiě)繁復(fù)的代碼, 根據(jù)不同的運(yùn)輸對(duì)象類(lèi), 在應(yīng)用中進(jìn)行不同的處理。
解決方案
工廠方法模式建議使用特殊的工廠方法代替對(duì)于對(duì)象構(gòu)造函數(shù)的直接調(diào)用 (即使用 new運(yùn)算符)。不用擔(dān)心, 對(duì)象仍將通過(guò) new運(yùn)算符創(chuàng)建, 只是該運(yùn)算符改在工廠方法中調(diào)用罷了。工廠方法返回的對(duì)象通常被稱(chēng)作 “產(chǎn)品”。

子類(lèi)可以修改工廠方法返回的對(duì)象類(lèi)型。
乍看之下, 這種更改可能毫無(wú)意義:我們只是改變了程序中調(diào)用構(gòu)造函數(shù)的位置而已。但是, 仔細(xì)想一下, 現(xiàn)在你可以在子類(lèi)中重寫(xiě)工廠方法, 從而改變其創(chuàng)建產(chǎn)品的類(lèi)型。
但有一點(diǎn)需要注意:僅當(dāng)這些產(chǎn)品具有共同的基類(lèi)或者接口時(shí), 子類(lèi)才能返回不同類(lèi)型的產(chǎn)品, 同時(shí)基類(lèi)中的工廠方法還應(yīng)將其返回類(lèi)型聲明為這一共有接口。

所有產(chǎn)品都必須使用同一接口。
舉例來(lái)說(shuō), 卡車(chē)Truck和 輪船Ship類(lèi)都必須實(shí)現(xiàn) 運(yùn)輸Trans-port接口, 該接口聲明了一個(gè)名為 deliv-er交付的方法。每個(gè)類(lèi)都將以不同的方式實(shí)現(xiàn)該方法:卡車(chē)走陸路交付貨物, 輪船走海路交付貨物。 陸路運(yùn)輸Road-Logis-tics類(lèi)中的工廠方法返回卡車(chē)對(duì)象, 而 海路運(yùn)輸Sea-Logis-tics類(lèi)則返回輪船對(duì)象。

只要產(chǎn)品類(lèi)實(shí)現(xiàn)一個(gè)共同的接口, 你就可以將其對(duì)象傳遞給客戶代碼, 而無(wú)需提供額外數(shù)據(jù)。
調(diào)用工廠方法的代碼 (通常被稱(chēng)為客戶端代碼) 無(wú)需了解不同子類(lèi)返回實(shí)際對(duì)象之間的差別。客戶端將所有產(chǎn)品視為抽象的 運(yùn)輸 。客戶端知道所有運(yùn)輸對(duì)象都提供 交付方法, 但是并不關(guān)心其具體實(shí)現(xiàn)方式。
工廠方法模式結(jié)構(gòu)
1. 產(chǎn)品 (Prod-uct) 將會(huì)對(duì)接口進(jìn)行聲明。對(duì)于所有由創(chuàng)建者及其子類(lèi)構(gòu)建的對(duì)象, 這些接口都是通用的。
2. 具體產(chǎn)品 (Con-crete Prod-ucts) 是產(chǎn)品接口的不同實(shí)現(xiàn)。
3. 創(chuàng)建者 (Cre-ator) 類(lèi)聲明返回產(chǎn)品對(duì)象的工廠方法。該方法的返回對(duì)象類(lèi)型必須與產(chǎn)品接口相匹配。你可以將工廠方法聲明為抽象方法, 強(qiáng)制要求每個(gè)子類(lèi)以不同方式實(shí)現(xiàn)該方法。或者, 你也可以在基礎(chǔ)工廠方法中返回默認(rèn)產(chǎn)品類(lèi)型。注意, 盡管它的名字是創(chuàng)建者, 但它最主要的職責(zé)并不是創(chuàng)建產(chǎn)品。一般來(lái)說(shuō), 創(chuàng)建者類(lèi)包含一些與產(chǎn)品相關(guān)的核心業(yè)務(wù)邏輯。工廠方法將這些邏輯處理從具體產(chǎn)品類(lèi)中分離出來(lái)。打個(gè)比方, 大型軟件開(kāi)發(fā)公司擁有程序員培訓(xùn)部門(mén)。但是, 這些公司的主要工作還是編寫(xiě)代碼, 而非生產(chǎn)程序員。
4. 具體創(chuàng)建者 (Con-crete Cre-ators) 將會(huì)重寫(xiě)基礎(chǔ)工廠方法, 使其返回不同類(lèi)型的產(chǎn)品。注意, 并不一定每次調(diào)用工廠方法都會(huì)創(chuàng)建新的實(shí)例。工廠方法也可以返回緩存、 對(duì)象池或其他來(lái)源的已有對(duì)象。
偽代碼
以下示例演示了如何使用工廠方法開(kāi)發(fā)跨平臺(tái) UI (用戶界面) 組件, 并同時(shí)避免客戶代碼與具體 UI 類(lèi)之間的耦合。

跨平臺(tái)對(duì)話框示例。
基礎(chǔ)對(duì)話框類(lèi)使用不同的 UI 組件渲染窗口。在不同的操作系統(tǒng)下, 這些組件外觀或許略有不同, 但其功能保持一致。Windows 系統(tǒng)中的按鈕在 Linux 系統(tǒng)中仍然是按鈕。
如果使用工廠方法, 就不需要為每種操作系統(tǒng)重寫(xiě)對(duì)話框邏輯。如果我們聲明了一個(gè)在基本對(duì)話框類(lèi)中生成按鈕的工廠方法, 那么我們就可以創(chuàng)建一個(gè)對(duì)話框子類(lèi), 并使其通過(guò)工廠方法返回 Windows 樣式按鈕。子類(lèi)將繼承對(duì)話框基礎(chǔ)類(lèi)的大部分代碼, 同時(shí)在屏幕上根據(jù) Windows 樣式渲染按鈕。
如需該模式正常工作, 基礎(chǔ)對(duì)話框類(lèi)必須使用抽象按鈕 (例如基類(lèi)或接口), 以便將其擴(kuò)展為具體按鈕。這樣一來(lái), 無(wú)論對(duì)話框中使用何種類(lèi)型的按鈕, 其代碼都可以正常工作。
你可以使用此方法開(kāi)發(fā)其他 UI 組件。不過(guò), 每向?qū)υ捒蛑刑砑右粋€(gè)新的工廠方法, 你就離抽象工廠模式更近一步。我們將在稍后談到這個(gè)模式。
// 創(chuàng)建者類(lèi)聲明的工廠方法必須返回一個(gè)產(chǎn)品類(lèi)的對(duì)象。創(chuàng)建者的子類(lèi)通常會(huì)提供// 該方法的實(shí)現(xiàn)。class Dialog is// 創(chuàng)建者還可提供一些工廠方法的默認(rèn)實(shí)現(xiàn)。abstract method createButton():Button// 請(qǐng)注意,創(chuàng)建者的主要職責(zé)并非是創(chuàng)建產(chǎn)品。其中通常會(huì)包含一些核心業(yè)務(wù)// 邏輯,這些邏輯依賴(lài)于由工廠方法返回的產(chǎn)品對(duì)象。子類(lèi)可通過(guò)重寫(xiě)工廠方// 法并使其返回不同類(lèi)型的產(chǎn)品來(lái)間接修改業(yè)務(wù)邏輯。method render() is// 調(diào)用工廠方法創(chuàng)建一個(gè)產(chǎn)品對(duì)象。Button okButton = createButton()// 現(xiàn)在使用產(chǎn)品。okButton.onClick(closeDialog)okButton.render()// 具體創(chuàng)建者將重寫(xiě)工廠方法以改變其所返回的產(chǎn)品類(lèi)型。class WindowsDialog extends Dialog ismethod createButton():Button isreturn new WindowsButton()class WebDialog extends Dialog ismethod createButton():Button isreturn new HTMLButton()// 產(chǎn)品接口中將聲明所有具體產(chǎn)品都必須實(shí)現(xiàn)的操作。interface Button ismethod render()method onClick(f)// 具體產(chǎn)品需提供產(chǎn)品接口的各種實(shí)現(xiàn)。class WindowsButton implements Button ismethod render(a, b) is// 根據(jù) Windows 樣式渲染按鈕。method onClick(f) is// 綁定本地操作系統(tǒng)點(diǎn)擊事件。class HTMLButton implements Button ismethod render(a, b) is// 返回一個(gè)按鈕的 HTML 表述。method onClick(f) is// 綁定網(wǎng)絡(luò)瀏覽器的點(diǎn)擊事件。class Application isfield dialog: Dialog// 程序根據(jù)當(dāng)前配置或環(huán)境設(shè)定選擇創(chuàng)建者的類(lèi)型。method initialize() isconfig = readApplicationConfigFile()if (config.OS == "Windows") thendialog = new WindowsDialog()else if (config.OS == "Web") thendialog = new WebDialog()elsethrow new Exception("錯(cuò)誤!未知的操作系統(tǒng)。")// 當(dāng)前客戶端代碼會(huì)與具體創(chuàng)建者的實(shí)例進(jìn)行交互,但是必須通過(guò)其基本接口// 進(jìn)行。只要客戶端通過(guò)基本接口與創(chuàng)建者進(jìn)行交互,你就可將任何創(chuàng)建者子// 類(lèi)傳遞給客戶端。method main() isthis.initialize()dialog.render()
工廠方法模式適合應(yīng)用場(chǎng)景
當(dāng)你在編寫(xiě)代碼的過(guò)程中, 如果無(wú)法預(yù)知對(duì)象確切類(lèi)別及其依賴(lài)關(guān)系時(shí), 可使用工廠方法。
工廠方法將創(chuàng)建產(chǎn)品的代碼與實(shí)際使用產(chǎn)品的代碼分離, 從而能在不影響其他代碼的情況下擴(kuò)展產(chǎn)品創(chuàng)建部分代碼。
例如, 如果需要向應(yīng)用中添加一種新產(chǎn)品, 你只需要開(kāi)發(fā)新的創(chuàng)建者子類(lèi), 然后重寫(xiě)其工廠方法即可。
如果你希望用戶能擴(kuò)展你軟件庫(kù)或框架的內(nèi)部組件, 可使用工廠方法。
繼承可能是擴(kuò)展軟件庫(kù)或框架默認(rèn)行為的最簡(jiǎn)單方法。但是當(dāng)你使用子類(lèi)替代標(biāo)準(zhǔn)組件時(shí), 框架如何辨識(shí)出該子類(lèi)?
解決方案是將各框架中構(gòu)造組件的代碼集中到單個(gè)工廠方法中, 并在繼承該組件之外允許任何人對(duì)該方法進(jìn)行重寫(xiě)。
讓我們看看具體是如何實(shí)現(xiàn)的。假設(shè)你使用開(kāi)源 UI 框架編寫(xiě)自己的應(yīng)用。你希望在應(yīng)用中使用圓形按鈕, 但是原框架僅支持矩形按鈕。你可以使用 圓形按鈕Round-But-ton子類(lèi)來(lái)繼承標(biāo)準(zhǔn)的 按鈕But-ton類(lèi)。但是, 你需要告訴 UI框架UIFrame-work類(lèi)使用新的子類(lèi)按鈕代替默認(rèn)按鈕。
為了實(shí)現(xiàn)這個(gè)功能, 你可以根據(jù)基礎(chǔ)框架類(lèi)開(kāi)發(fā)子類(lèi) 圓形按鈕 UIUIWith-Round-But-tons , 并且重寫(xiě)其 cre-ate-But-ton創(chuàng)建按鈕方法。基類(lèi)中的該方法返回 按鈕對(duì)象, 而你開(kāi)發(fā)的子類(lèi)返回 圓形按鈕對(duì)象。現(xiàn)在, 你就可以使用 圓形按鈕 UI類(lèi)代替 UI框架類(lèi)。就是這么簡(jiǎn)單!
如果你希望復(fù)用現(xiàn)有對(duì)象來(lái)節(jié)省系統(tǒng)資源, 而不是每次都重新創(chuàng)建對(duì)象, 可使用工廠方法。
在處理大型資源密集型對(duì)象 (比如數(shù)據(jù)庫(kù)連接、 文件系統(tǒng)和網(wǎng)絡(luò)資源) 時(shí), 你會(huì)經(jīng)常碰到這種資源需求。
讓我們思考復(fù)用現(xiàn)有對(duì)象的方法:
1. 首先, 你需要?jiǎng)?chuàng)建存儲(chǔ)空間來(lái)存放所有已經(jīng)創(chuàng)建的對(duì)象。
2. 當(dāng)他人請(qǐng)求一個(gè)對(duì)象時(shí), 程序?qū)⒃趯?duì)象池中搜索可用對(duì)象。
3. … 然后將其返回給客戶端代碼。
4. 如果沒(méi)有可用對(duì)象, 程序則創(chuàng)建一個(gè)新對(duì)象 (并將其添加到對(duì)象池中)。
這些代碼可不少!而且它們必須位于同一處, 這樣才能確保重復(fù)代碼不會(huì)污染程序。
可能最顯而易見(jiàn), 也是最方便的方式, 就是將這些代碼放置在我們?cè)噲D重用的對(duì)象類(lèi)的構(gòu)造函數(shù)中。但是從定義上來(lái)講, 構(gòu)造函數(shù)始終返回的是新對(duì)象, 其無(wú)法返回現(xiàn)有實(shí)例。
因此, 你需要有一個(gè)既能夠創(chuàng)建新對(duì)象, 又可以重用現(xiàn)有對(duì)象的普通方法。這聽(tīng)上去和工廠方法非常相像。
實(shí)現(xiàn)方式
1. 讓所有產(chǎn)品都遵循同一接口。該接口必須聲明對(duì)所有產(chǎn)品都有意義的方法。
2. 在創(chuàng)建類(lèi)中添加一個(gè)空的工廠方法。該方法的返回類(lèi)型必須遵循通用的產(chǎn)品接口。
3. 在創(chuàng)建者代碼中找到對(duì)于產(chǎn)品構(gòu)造函數(shù)的所有引用。將它們依次替換為對(duì)于工廠方法的調(diào)用, 同時(shí)將創(chuàng)建產(chǎn)品的代碼移入工廠方法。你可能需要在工廠方法中添加臨時(shí)參數(shù)來(lái)控制返回的產(chǎn)品類(lèi)型。工廠方法的代碼看上去可能非常糟糕。其中可能會(huì)有復(fù)雜的
switch分支運(yùn)算符, 用于選擇各種需要實(shí)例化的產(chǎn)品類(lèi)。但是不要擔(dān)心, 我們很快就會(huì)修復(fù)這個(gè)問(wèn)題。4. 現(xiàn)在, 為工廠方法中的每種產(chǎn)品編寫(xiě)一個(gè)創(chuàng)建者子類(lèi), 然后在子類(lèi)中重寫(xiě)工廠方法, 并將基本方法中的相關(guān)創(chuàng)建代碼移動(dòng)到工廠方法中。
5. 如果應(yīng)用中的產(chǎn)品類(lèi)型太多, 那么為每個(gè)產(chǎn)品創(chuàng)建子類(lèi)并無(wú)太大必要, 這時(shí)你也可以在子類(lèi)中復(fù)用基類(lèi)中的控制參數(shù)。例如, 設(shè)想你有以下一些層次結(jié)構(gòu)的類(lèi)。基類(lèi)
郵件及其子類(lèi)航空郵件和陸路郵件;運(yùn)輸及其子類(lèi)飛機(jī),卡車(chē)和火車(chē)。航空郵件僅使用飛機(jī)對(duì)象, 而陸路郵件則會(huì)同時(shí)使用卡車(chē)和火車(chē)對(duì)象。你可以編寫(xiě)一個(gè)新的子類(lèi) (例如火車(chē)郵件) 來(lái)處理這兩種情況, 但是還有其他可選的方案。客戶端代碼可以給陸路郵件類(lèi)傳遞一個(gè)參數(shù), 用于控制其希望獲得的產(chǎn)品。6. 如果代碼經(jīng)過(guò)上述移動(dòng)后, 基礎(chǔ)工廠方法中已經(jīng)沒(méi)有任何代碼, 你可以將其轉(zhuǎn)變?yōu)槌橄箢?lèi)。如果基礎(chǔ)工廠方法中還有其他語(yǔ)句, 你可以將其設(shè)置為該方法的默認(rèn)行為。
工廠方法模式優(yōu)缺點(diǎn)
? 你可以避免創(chuàng)建者和具體產(chǎn)品之間的緊密耦合。
? 單一職責(zé)原則。你可以將產(chǎn)品創(chuàng)建代碼放在程序的單一位置, 從而使得代碼更容易維護(hù)。
? 開(kāi)閉原則。無(wú)需更改現(xiàn)有客戶端代碼, 你就可以在程序中引入新的產(chǎn)品類(lèi)型。
? 應(yīng)用工廠方法模式需要引入許多新的子類(lèi), 代碼可能會(huì)因此變得更復(fù)雜。最好的情況是將該模式引入創(chuàng)建者類(lèi)的現(xiàn)有層次結(jié)構(gòu)中。
與其他模式的關(guān)系
? 在許多設(shè)計(jì)工作的初期都會(huì)使用工廠方法模式 (較為簡(jiǎn)單, 而且可以更方便地通過(guò)子類(lèi)進(jìn)行定制), 隨后演化為使用抽象工廠模式、 原型模式或生成器模式 (更靈活但更加復(fù)雜)。
? 抽象工廠模式通常基于一組工廠方法, 但你也可以使用原型模式來(lái)生成這些類(lèi)的方法。
? 你可以同時(shí)使用工廠方法和迭代器模式來(lái)讓子類(lèi)集合返回不同類(lèi)型的迭代器, 并使得迭代器與集合相匹配。
? 原型并不基于繼承, 因此沒(méi)有繼承的缺點(diǎn)。另一方面, 原型需要對(duì)被復(fù)制對(duì)象進(jìn)行復(fù)雜的初始化。 工廠方法基于繼承, 但是它不需要初始化步驟。
? 工廠方法是模板方法模式的一種特殊形式。同時(shí), 工廠方法可以作為一個(gè)大型模板方法中的一個(gè)步驟。
額外內(nèi)容
? 如果你希望了解不同工廠模式和概念之間的區(qū)別, 之后我將編寫(xiě)工廠模式比較指南。

入骨相思知不知
玲瓏骰子安紅豆



入我相思門(mén),知我相思苦,長(zhǎng)相思兮長(zhǎng)相憶,短相思兮無(wú)窮極。



