Python設(shè)計(jì)模式:工廠方法模式初探
軟件設(shè)計(jì)大師總是要比初學(xué)者更加清楚該如何設(shè)計(jì)軟件,因?yàn)樗麄兪种姓莆罩O(shè)計(jì)模式這一法寶。作為一種高級(jí)的軟件復(fù)用形式,設(shè)計(jì)模式是眾多優(yōu)秀軟件設(shè)計(jì)師集體智慧的結(jié)晶,能夠很好地指導(dǎo)軟件設(shè)計(jì)過(guò)程。
51CTO推薦閱讀:Python設(shè)計(jì)模式:用模式改變軟件設(shè)計(jì)
工廠方法(Factory Method)模式又稱為虛擬構(gòu)造器(Virtual Constructor)模式或者多態(tài)工廠(Polymorphic Factory)模式,屬于類的創(chuàng)建型模式。在工廠方法模式中,父類負(fù)責(zé)定義創(chuàng)建對(duì)象的公共接口,而子類則負(fù)責(zé)生成具體的對(duì)象,這樣做的目的是將類的實(shí)例化操作延遲到子類中完成,即由子類來(lái)決定究竟應(yīng)該實(shí)體化哪一個(gè)類。
在簡(jiǎn)單工廠模式中,一個(gè)工廠類處于對(duì)產(chǎn)品類進(jìn)行實(shí)例化的中心位置上,它知道每一個(gè)產(chǎn)品類的細(xì)節(jié),并決定何時(shí)哪一個(gè)產(chǎn)品類應(yīng)當(dāng)被實(shí)例化。簡(jiǎn)單工廠模式的優(yōu)點(diǎn)是能夠使客戶端獨(dú)立于產(chǎn)品的創(chuàng)建過(guò)程,并且在系統(tǒng)中引入新產(chǎn)品時(shí)無(wú)需對(duì)客戶端進(jìn)行修改,缺點(diǎn)是當(dāng)有新產(chǎn)品要加入到系統(tǒng)中時(shí),必須對(duì)工廠類進(jìn)行修改,以加入必要的處理邏輯。簡(jiǎn)單工廠模式的致命弱點(diǎn)就是處于核心地位的工廠類,因?yàn)橐坏┧鼰o(wú)法確定要對(duì)哪個(gè)類進(jìn)行實(shí)例化時(shí),就無(wú)法使用該模式,而工廠方法模式則可以很好地避免這一問(wèn)題。
考慮這樣一個(gè)應(yīng)用程序框架(Framework),它可以用來(lái)瀏覽各種格式的文檔,如TXT、DOC、PDF、HTML等,設(shè)計(jì)時(shí)為了讓軟件的體系結(jié)構(gòu)能夠盡可能地通用,定義了Application和Document這兩個(gè)抽象父類,客戶必須通過(guò)它們的子類來(lái)處理某一具體類型的文檔。例如,要想利用該框架來(lái)編寫(xiě)一個(gè)PDF文件瀏覽器,必須先定義PDFApplication和PDFDocument這兩個(gè)類,它們應(yīng)該分別繼承于Application和Document。
Application的職責(zé)是對(duì)Document進(jìn)行管理,并且在需要時(shí)創(chuàng)建它們,比如當(dāng)用戶從菜單中選擇Open或者New的時(shí)候,Application就要負(fù)責(zé)創(chuàng)建一個(gè)Document的實(shí)例。顯而易見(jiàn),被實(shí)例化的特定Document子類是與具體應(yīng)用相關(guān)的,因此Application無(wú)法預(yù)測(cè)哪個(gè)Document的子類將被實(shí)例化,它只知道一個(gè)新的Document何時(shí)(When)被創(chuàng)建,但并不知道哪種(Which)具體的Document將被創(chuàng)建。此時(shí)若仍堅(jiān)持使用簡(jiǎn)單工廠模式會(huì)出現(xiàn)一個(gè)非常尷尬的局面:框架必須實(shí)例化類,但它只知道不能被實(shí)例化的抽象類。
解決的辦法是使用工廠方法模式,它封裝了哪一個(gè)Document子類將被創(chuàng)建的信息,并且能夠?qū)⑦@些信息從框架中分離出來(lái)。如圖1所示,Application的子類重新定義了Application的抽象方法createDocument(),并返回某個(gè)恰當(dāng)?shù)腄ocument子類的實(shí)例。我們稱createDocument()是一個(gè)工廠方法(factory method),因?yàn)樗浅P蜗蟮孛枋隽祟惖膶?shí)例化過(guò)程,即負(fù)責(zé)"生產(chǎn)"一個(gè)對(duì)象。
簡(jiǎn)單說(shuō)來(lái),工廠方法模式的作用就是可以根據(jù)不同的條件生成各種類的實(shí)例,這些實(shí)例通常屬于多個(gè)相似的類型,并且具有共同的父類。工廠方法模式將這些實(shí)例的創(chuàng)建過(guò)程封裝了起來(lái),從而簡(jiǎn)化了客戶程序的編寫(xiě),并改善了軟件體系結(jié)構(gòu)的可擴(kuò)展性,使得將來(lái)能夠以最小的代價(jià)加入新的子類。工廠方法這一模式適合在如下場(chǎng)合中運(yùn)用:
◆當(dāng)無(wú)法得知必須創(chuàng)建的對(duì)象屬于哪個(gè)類的時(shí)候,或者無(wú)法得知屬于哪個(gè)類的對(duì)象將被返回的時(shí)候,但前提是這些對(duì)象都符合一定的接口標(biāo)準(zhǔn)。
◆當(dāng)一個(gè)類希望由它的子類來(lái)決定所創(chuàng)建的對(duì)象的時(shí)候,其目的是使程序的可擴(kuò)展性更好,在加入其他類時(shí)更具彈性。
◆當(dāng)創(chuàng)建對(duì)象的職責(zé)被委托給多個(gè)幫助子類(helper subclass)中的某一個(gè),并且希望將哪個(gè)子類是代理者這一信息局部化的時(shí)候。
需要說(shuō)明的是,使用工廠方法模式創(chuàng)建對(duì)象并不意味著一定會(huì)讓代碼變得更短(實(shí)事上往往更長(zhǎng)),并且可能需要設(shè)計(jì)更多的輔助類,但它的確可以靈活地、有彈性地創(chuàng)建尚未確定的對(duì)象,從而簡(jiǎn)化了客戶端應(yīng)用程序的邏輯結(jié)構(gòu),并提高了代碼的可讀性和可重用性。
#p#
二、模式引入
工廠方法這一模式本身雖然并不復(fù)雜,但卻是最重要的設(shè)計(jì)模式之一,無(wú)論是在COM、CORBA或是EJB中,都可以隨處見(jiàn)到它的身影。面向?qū)ο蟮囊粋€(gè)基本思想是在不同的對(duì)象間進(jìn)行責(zé)權(quán)的合理分配,從本質(zhì)上講,工廠方法模式是一種用來(lái)創(chuàng)建對(duì)象的多態(tài)方法(polymorphic method),它在抽象父類中聲明用來(lái)創(chuàng)建對(duì)象的方法接口,而具體子類則通過(guò)覆蓋該方法將對(duì)象的創(chuàng)建過(guò)程局部化,包括是否實(shí)例化一個(gè)子類,以及是否對(duì)它進(jìn)行初始化等等。從某種程度上說(shuō),工廠方法可以看成是構(gòu)造函數(shù)的特殊化,其特殊性表現(xiàn)在能夠用一致的方法來(lái)創(chuàng)建不同的對(duì)象,而不用擔(dān)心當(dāng)前正在對(duì)哪個(gè)類進(jìn)行實(shí)例化,因?yàn)榫烤箘?chuàng)建哪個(gè)類的對(duì)象將取決于它的子類。
假設(shè)我們打算開(kāi)發(fā)一個(gè)用于個(gè)人信息管理(Personal Information Manager,PIM)的軟件,它可以保存日常工作和生活中所需的各種信息,包括地址本、電話簿、約會(huì)提醒、日程安排等等。很顯然,PIM用戶界面(User Interface)的設(shè)計(jì)將是比較復(fù)雜的,因?yàn)楸仨殲槊糠N信息的輸入、驗(yàn)證和修改都提供單獨(dú)的界面,以便同用戶進(jìn)行交互。比較簡(jiǎn)單的做法是在PIM中為各種信息的處理編寫(xiě)相應(yīng)的用戶界面,但代價(jià)是將導(dǎo)致軟件的可擴(kuò)展性非常差,因?yàn)橐坏┙窈笠尤雽?duì)其他信息(比如銀行帳戶)進(jìn)行管理的功能時(shí),就必須對(duì)PIM進(jìn)行修改,添加相應(yīng)的用戶界面,從而最終導(dǎo)致PIM變得越來(lái)越復(fù)雜,結(jié)構(gòu)龐大而難以維護(hù)。改進(jìn)的辦法是將處理各種信息的用戶界面從PIM中分離出來(lái),使PIM不再關(guān)心用戶如何輸入數(shù)據(jù),如何對(duì)用戶輸入進(jìn)行驗(yàn)證,以及用戶如何修改信息等,所有的這些都交由一個(gè)專門(mén)的軟件模塊來(lái)完成,而PIM要做的只是提供一個(gè)對(duì)這些個(gè)人信息進(jìn)行管理的總體框架。
在具體實(shí)現(xiàn)時(shí)可以設(shè)計(jì)一個(gè)通用接口Editable,并且讓所有處理特定個(gè)人信息(如通信地址和電話號(hào)碼)的用戶界面都繼承于它,而PIM則通過(guò)Editable提供的方法getEditor()獲得Editor的一個(gè)實(shí)例,并利用它來(lái)對(duì)用戶輸入進(jìn)行統(tǒng)一的處理。例如,當(dāng)用戶完成輸入之后,PIM可以調(diào)用Editor中的方法getContent()來(lái)獲取用戶輸入的數(shù)據(jù),或者調(diào)用resetUI()來(lái)清除用戶輸入的數(shù)據(jù)。在采用這一體系結(jié)構(gòu)之后,如果要擴(kuò)展PIM的功能,只需添加與之對(duì)應(yīng)的Editable和Editor就可以了,而不用對(duì)PIM本身進(jìn)行修改。
現(xiàn)在離目標(biāo)還有一步之遙,由于Editable和Editor都只是通用的接口,但PIM卻需要對(duì)它們的子類進(jìn)行實(shí)例化,此時(shí)自然應(yīng)該想到運(yùn)用工廠方法模式,為PIM定義一個(gè)EditableFactory接口來(lái)創(chuàng)建Editable的對(duì)象。這樣一來(lái),整個(gè)PIM的體系結(jié)構(gòu)就將如圖2所示。
Editable接口定義了一個(gè)公共的構(gòu)造性方法(builder method)getEditor(),它返回一個(gè)Editor對(duì)象,其完整的代碼如清單1所示。任何一項(xiàng)個(gè)人信息都擁有自己獨(dú)立的用戶界面(Editor),負(fù)責(zé)獲取數(shù)據(jù)并在需要的時(shí)候進(jìn)行修改,而PIM唯一要做事情的只是通過(guò)Editable來(lái)獲得Editor,并利用它來(lái)對(duì)用戶輸入的數(shù)據(jù)進(jìn)行相應(yīng)的操作。
- 代碼清單1:editable.py
- class Editable:
- """ 個(gè)人信息用戶界面的公共接口 """
- # 獲得個(gè)人信息編輯界面
- def getEditor(self):
- pass
Editor接口給出了處理所有個(gè)人信息的公共接口,其完整的代碼如清單2所示。PIM通過(guò)調(diào)用getUI()方法能夠獲得與用戶進(jìn)行交互的UI組件,根據(jù)當(dāng)前正在處理的個(gè)人信息的不同,這些組件可能簡(jiǎn)單到只是一個(gè)文本輸入框,也可以復(fù)雜到是一個(gè)包含了多個(gè)圖形控件(Widget)的對(duì)話框。利用Editor提供的getContent()、commitChanges()和resetUI()方法,PIM還可以獲取、提交或者清空用戶輸入的個(gè)人信息。在引入Editor之后, PIM就能夠從處理特定個(gè)人信息的用戶界面中解脫出來(lái),從而可以將注意力集中在如何對(duì)這些信息進(jìn)行統(tǒng)一管理的問(wèn)題上。
- 代碼清單2:editor.py
- class Editor:
- """ 用戶使用特定的Editor來(lái)編輯個(gè)人信息 """
- # 獲取代表用戶界面(UI)的對(duì)象
- def getUI(self):
- pass
- # 獲取用戶輸入的數(shù)據(jù)
- def getContent(self):
- pass
- # 提交用戶輸入的數(shù)據(jù)
- def commitChanges(self):
- pass
- # 清空用戶輸入的數(shù)據(jù)
- def resetUI(self):
- pass
EditableAddress是Editable的一個(gè)具體實(shí)現(xiàn),PIM使用它來(lái)處理個(gè)人地址信息,其完整的代碼如清單3所示。
- 代碼清單3:editableaddress.py
- from editor import Editor
- from editable import Editable
- import Tkinter
- class EditableAddress(Editable):
- """ 用于處理個(gè)人地址信息的Editable """
- # 構(gòu)造函數(shù)
- def __init__(self, master):
- self.master = master
- self.name = ""
- self.province = ""
- self.city = ""
- self.street = ""
- self.zipcode = ""
- self.editor = AddressEditor(self)
- # 獲取相關(guān)聯(lián)的Editor
- def getEditor(self):
- return self.editor
- class AddressEditor(Editor, Tkinter.Frame):
- """ 用于處理個(gè)人地址信息的Editor """
- # 構(gòu)造函數(shù)
- def __init__(self, owner):
- Tkinter.Frame.__init__(self, owner.master)
- self.owner = owner
- self.name = Tkinter.StringVar()
- self.province = Tkinter.StringVar()
- self.city = Tkinter.StringVar()
- self.street = Tkinter.StringVar()
- self.zipcode = Tkinter.StringVar()
- self.createWidgets()
- # 構(gòu)造用戶界面
- def createWidgets(self):
- # 姓名
- nameFrame = Tkinter.Frame(self)
- nameLabel = Tkinter.Label(nameFrame, text="Name:")
- nameEntry = Tkinter.Entry(nameFrame, textvariable=self.name)
- nameLabel.config(anchor=Tkinter.E, width=8, pady=3)
- nameLabel.pack(side=Tkinter.LEFT)
- nameEntry.pack(side=Tkinter.LEFT)
- nameFrame.pack()
- # 省份
- provinceFrame = Tkinter.Frame(self)
- provinceLabel = Tkinter.Label(provinceFrame, text="Province:")
- provinceEntry = Tkinter.Entry(provinceFrame, textvariable=self.province)
- provinceLabel.config(anchor=Tkinter.E, width=8, pady=3)
- provinceLabel.pack(side=Tkinter.LEFT)
- provinceEntry.pack(side=Tkinter.LEFT)
- provinceFrame.pack()
- # 城市
- cityFrame = Tkinter.Frame(self)
- cityLabel = Tkinter.Label(cityFrame, text="City:")
- cityEntry = Tkinter.Entry(cityFrame, textvariable=self.city)
- cityLabel.config(anchor=Tkinter.E, width=8, pady=3)
- cityLabel.pack(side=Tkinter.LEFT)
- cityEntry.pack(side=Tkinter.LEFT)
- cityFrame.pack()
- # 街道
- streetFrame = Tkinter.Frame(self)
- streetLabel = Tkinter.Label(streetFrame, text="Street:")
- streetEntry = Tkinter.Entry(streetFrame, textvariable=self.street)
- streetLabel.config(anchor=Tkinter.E, width=8, pady=3)
- streetLabel.pack(side=Tkinter.LEFT)
- streetEntry.pack(side=Tkinter.LEFT)
- streetFrame.pack()
- # 郵編
- zipcodeFrame = Tkinter.Frame(self)
- zipcodeLabel = Tkinter.Label(zipcodeFrame, text="ZIP Code:")
- zipcodeEntry = Tkinter.Entry(zipcodeFrame, textvariable=self.zipcode)
- zipcodeLabel.config(anchor=Tkinter.E, width=8, pady=3)
- zipcodeLabel.pack(side=Tkinter.LEFT)
- zipcodeEntry.pack(side=Tkinter.LEFT)
- zipcodeFrame.pack()
- # 重載Editor中的方法,獲取代表用戶界面(UI)的對(duì)象
- def getUI(self):
- return self
- # 重載Editor中的方法,獲取用戶輸入的數(shù)據(jù)
- def getContent(self):
- content = " Name: " + self.name.get() + "\n"
- content += "Province: " + self.province.get() + "\n"
- content += " City: " + self.city.get() + "\n"
- content += " Street: " + self.street.get() + "\n"
- content += "ZIP Code: " + self.zipcode.get()
- return content
- # 重載Editor中的方法,提交用戶輸入的數(shù)據(jù)
- def commitChanges(self):
- selfself.owner.name = self.name.get()
- selfself.owner.province = self.province.get()
- selfself.owner.city = self.city.get()
- selfself.owner.street = self.street.get()
- selfself.owner.zipcode = self.zipcode.get()
- # 重載Editor中的方法,清空用戶輸入的數(shù)據(jù)
- def resetUI(self):
- self.name.set("")
- self.province.set("")
- self.city.set("")
- self.street.set("")
- self.zipcode.set("")
EditablePhone是Editable的另一個(gè)具體實(shí)現(xiàn),PIM使用它來(lái)處理個(gè)人電話號(hào)碼,其完整的代碼如清單4所示。
- 代碼清單4:editablephone.py
- from editor import Editor
- from editable import Editable
- import Tkinter
- class EditablePhone(Editable):
- """ 用于處理個(gè)人電話號(hào)碼的Editable """
- # 構(gòu)造函數(shù)
- def __init__(self, master):
- self.master =master
- self.areaCode = "";
- self.phoneNumber = ""
- self.editor = PhoneEditor(self)
- # 獲取相關(guān)聯(lián)的Editor
- def getEditor(self):
- return self.editor
- class PhoneEditor(Editor, Tkinter.Frame):
- """ 用于處理個(gè)人電話號(hào)碼的Editor """
- # 構(gòu)造函數(shù)
- def __init__(self, owner):
- self.owner = owner
- Tkinter.Frame.__init__(self, self.owner.master)
- self.areaCode = Tkinter.StringVar()
- self.phoneNumber = Tkinter.StringVar()
- # 構(gòu)造用戶界面
- codeLabel = Tkinter.Label(self, text="Area Code:")
- codeEntry = Tkinter.Entry(self, textvariable=self.areaCode)
- codeLabel.config(anchor=Tkinter.E, width=12, pady=3)
- codeLabel.grid(row=0, column=0)
- codeEntry.grid(row=0, column=1)
- numberLabel = Tkinter.Label(self, text="Phone Number:")
- numberEntry = Tkinter.Entry(self, textvariable=self.phoneNumber)
- numberLabel.config(anchor=Tkinter.E, width=12, pady=3)
- numberLabel.grid(row=1, column=0)
- numberEntry.grid(row=1, column=1)
- # 重載Editor中的方法,獲取代表用戶界面(UI)的對(duì)象
- def getUI(self):
- return self
- # 重載Editor中的方法,獲取用戶輸入的數(shù)據(jù)
- def getContent(self):
- content = " Area Code: " + self.areaCode.get() + "\n"
- content += "Phone Number: " + self.phoneNumber.get() + "\n"
- return content
- # 重載Editor中的方法,提交用戶輸入的數(shù)據(jù)
- def commitChanges(self):
- selfself.owner.areaCode = self.areaCode.get()
- selfself.owner.phoneNumber = self.phoneNumber.get()
- # 重載Editor中的方法,清空用戶輸入的數(shù)據(jù)
- def resetUI(self):
- self.areaCode.set("")
- self.phoneNumber.set("")
EditableFactory接口是在PIM中應(yīng)用工廠方法模式的核心,其完整的代碼如清單5所示。與簡(jiǎn)單工廠模式中負(fù)責(zé)創(chuàng)建所有對(duì)象的"超級(jí)類"不同,EditableFactory只定義了如何實(shí)例化Editable的工廠方法createEditable(),并不掌握它們的生殺大權(quán),真正負(fù)責(zé)完成創(chuàng)建工作的是EditableFactory的子類。
- 代碼清單5:editablefactory.py
- class EditableFactory:
- """ 用于創(chuàng)建Editable的工廠類 """
- # 實(shí)例化Editable對(duì)象
- def createEditable(self, master):
- pass
EditableAddressFactory是EditableFactory的一個(gè)具體實(shí)現(xiàn),PIM使用它來(lái)實(shí)例化EditableAddress對(duì)象,其完整的代碼如清單6所示。
- 代碼清單6:editableaddressfactory.py
- from editablefactory import EditableFactory
- from editableaddress import EditableAddress
- class EditableAddressFactory(EditableFactory):
- """ 用于創(chuàng)建EditableAddress的工廠類 """
- # 重載EditableFactory中的方法,實(shí)例化EditableAddress對(duì)象
- def createEditable(self, master):
- address = EditableAddress(master)
- return address
EditablePhoneFactory是EditableFactory的另一個(gè)具體實(shí)現(xiàn),PIM使用它來(lái)實(shí)例化EditablePhone對(duì)象,其完整的代碼如清單7所示。
- 代碼清單7:editablephonefactory.py
- from editablefactory import EditableFactory
- from editablephone import EditablePhone
- class EditablePhoneFactory(EditableFactory):
- """ 用于創(chuàng)建EditablePhone的工廠類 """
- # 重載EditableFactory中的方法,實(shí)例化EditablePhone對(duì)象
- def createEditable(self, master):
- phone = EditablePhone(master)
- return phone
所有這些輔助類都定義好之后,接下去就可以編寫(xiě)PIM類了,它提供了一個(gè)對(duì)各種個(gè)人信息進(jìn)行統(tǒng)一管理的框架,其完整的代碼如清單8所示。
- 代碼清單8:pim.py
- from editablephone import EditablePhone
- from editableaddressfactory import EditableAddressFactory
- from editablephonefactory import EditablePhoneFactory
- import Tkinter
- class PIM:
- """ 個(gè)人信息管理 """
- # 構(gòu)造函數(shù)
- def __init__(self):
- mainFrame = Tkinter.Frame()
- mainFrame.master.title("PIM")
- # 命令按鈕
- addressButton = Tkinter.Button(mainFrame, width=10, text="Address")
- phoneButton = Tkinter.Button(mainFrame, width=10, text="Phone")
- commitButton = Tkinter.Button(mainFrame, width=10, text="Commit")
- resetButton = Tkinter.Button(mainFrame, width=10, text="Reset")
- addressButton.config(command=self.addressClicked)
- phoneButton.config(command=self.phoneClicked)
- commitButton.config(command=self.commitClicked)
- resetButton.config(command=self.resetClicked)
- addressButton.grid(row=0, column=1, padx=10, pady=5, stick=Tkinter.E)
- phoneButton.grid(row=1, column=1, padx=10, pady=5, stick=Tkinter.E)
- commitButton.grid(row=2, column=1, padx=10, pady=5, stick=Tkinter.E)
- resetButton.grid(row=3, column=1, padx=10, pady=5, stick=Tkinter.E)
- # 用來(lái)容納各類Editor的容器
- self.editorFrame = Tkinter.Frame(mainFrame)
- self.editorFrame.grid(row=0, column=0, rowspan=4)
- self.editorFrame.grid_configure(stick=Tkinter.N, pady=15)
- self.editor = Tkinter.Frame(self.editorFrame)
- self.editor.grid()
- # 個(gè)人信息顯示區(qū)域
- self.content = Tkinter.StringVar()
- self.contentLabel = Tkinter.Label(mainFrame, width=50, height=5)
- self.contentLabel.configure(textvariable=self.content)
- self.contentLabel.configure(anchor=Tkinter.W, font="Arial 10 italic bold")
- self.contentLabel.configure(relief=Tkinter.RIDGE, pady=5, padx=10)
- self.contentLabel.grid(row=4, column=0, columnspan=2)
- mainFrame.pack()
- mainFrame.mainloop()
- # Address按鈕的回調(diào)函數(shù)
- def addressClicked(self):
- address = EditableAddressFactory().createEditable(self.editorFrame)
- self.editor.grid_remove()
- self.editor = address.getEditor()
- self.editor.getUI().grid()
- # Phone按鈕的回調(diào)函數(shù)
- def phoneClicked(self):
- phone = EditablePhoneFactory().createEditable(self.editorFrame)
- self.editor.grid_remove()
- self.editor = phone.getEditor()
- self.editor.getUI().grid()
- # Commit按鈕的回調(diào)函數(shù)
- def commitClicked(self):
- content = self.editor.getContent()
- self.content.set(content)
- # Reset按鈕的回調(diào)函數(shù)
- def resetClicked(self):
- self.editor.resetUI()
- # 主函數(shù)
- if (__name__ == "__main__"):
- app = PIM()
圖3是PIM在運(yùn)行時(shí)的界面效果。
#p#
三、一般結(jié)構(gòu)
工廠方法模式是簡(jiǎn)單工廠模式的進(jìn)一步抽象和推廣,它不僅保持了簡(jiǎn)單工廠模式能夠向客戶隱藏類的實(shí)例化過(guò)程這一優(yōu)點(diǎn),而且還通過(guò)多態(tài)性克服了工廠類過(guò)于復(fù)雜且不易于擴(kuò)展的缺點(diǎn)。在工廠方法模式中,處于核心地位的工廠類不再負(fù)責(zé)所有產(chǎn)品的創(chuàng)建,而是將具體的創(chuàng)建工作交由子類去完成。工廠方法模式中的核心工廠類經(jīng)過(guò)功能抽象之后,成為了一個(gè)抽象的工廠角色,僅負(fù)責(zé)給出具體工廠子類必須實(shí)現(xiàn)的接口,而不涉及哪種產(chǎn)品類應(yīng)當(dāng)被實(shí)例化這一細(xì)節(jié)。工廠方法模式的一般性結(jié)構(gòu)如圖4所示,圖中為了簡(jiǎn)化只給出了一個(gè)產(chǎn)品類和一個(gè)工廠類,但在實(shí)際系統(tǒng)中通常需要設(shè)計(jì)多個(gè)產(chǎn)品類和多個(gè)工廠類。
工廠方法模式的實(shí)質(zhì)是將對(duì)象的創(chuàng)建延遲到其子類實(shí)現(xiàn),即由子類根據(jù)當(dāng)前情況動(dòng)態(tài)決定應(yīng)該實(shí)例化哪一個(gè)產(chǎn)品類。從上圖可以看出,工廠方法模式涉及到抽象工廠角色、具體工廠角色、抽象產(chǎn)品角色和具體產(chǎn)品角色四個(gè)參與者。
◆抽象工廠(Creator)角色:是工廠方法模式的核心,它負(fù)責(zé)定義創(chuàng)建抽象產(chǎn)品對(duì)象的工廠方法。抽象工廠不能被外界直接調(diào)用,但任何在模式中用于創(chuàng)建產(chǎn)品對(duì)象的工廠類都必須實(shí)現(xiàn)由它所定義的工廠方法。
具體工廠(Concrete Creator)角色:是工廠方法模式的對(duì)外接口,它負(fù)責(zé)實(shí)現(xiàn)創(chuàng)建具體產(chǎn)品對(duì)象的內(nèi)部邏輯。具體工廠與應(yīng)用密切相關(guān),可以被外界直接調(diào)用,創(chuàng)建所需要的產(chǎn)品。
抽象產(chǎn)品(Product)角色:是工廠方法模式所創(chuàng)建的所有對(duì)象的父類,它負(fù)責(zé)描述所有具體產(chǎn)品共有的公共接口。
具體產(chǎn)品(Concrete Product)角色:是工廠方法模式的創(chuàng)建目標(biāo),所有創(chuàng)建的對(duì)象都是充當(dāng)這一角色的某個(gè)具體類的實(shí)例。
抽象工廠角色負(fù)責(zé)聲明工廠方法(factory method),用來(lái)"生產(chǎn)"抽象產(chǎn)品,以下是抽象工廠的示例性Python代碼:
- 代碼清單9:creator.py
- class Creator:
- """ 抽象工廠角色 """
- # 創(chuàng)建抽象產(chǎn)品的工廠方法
- def factoryMethod(self):
- pass
具體工廠角色負(fù)責(zé)創(chuàng)建一個(gè)具體產(chǎn)品的實(shí)例,并將其返回給調(diào)用者。具體工廠是與具體產(chǎn)品相關(guān)的,實(shí)現(xiàn)時(shí)一般常用的做法是為每個(gè)具體產(chǎn)品定義一個(gè)具體工廠。以下是具體工廠的示例性Python代碼:
- 代碼清單10:concretecreator.py
- class ConcreteCreator(Creator):
- """ 具體工廠角色 """
- # 創(chuàng)建具體產(chǎn)品的工廠方法
- def factoryMethod(self):
- product = ConcreteProduct()
- return product
抽象產(chǎn)品角色的主要目的是為所有的具體產(chǎn)品提供一個(gè)共同的接口,通常只需給出相應(yīng)的聲明就可以了,而不用給出具體的實(shí)現(xiàn)。以下是抽象產(chǎn)品類的示例性Python代碼:
- 代碼清單11:product.py
- class Product:
- """ 抽象產(chǎn)品角色 """
- # 所有產(chǎn)品類的公共接口
- def interface(self):
- pass
具體產(chǎn)品角色充當(dāng)最終的創(chuàng)建目標(biāo),一般來(lái)講它是抽象產(chǎn)品類的子類,實(shí)現(xiàn)了抽象產(chǎn)品類中定義的所有工廠方法,實(shí)際應(yīng)用時(shí)通常會(huì)具有比較復(fù)雜的業(yè)務(wù)邏輯。以下是具體產(chǎn)品類的示例性Python代碼:
- 代碼清單12:concreteproduct.py
- class ConcreteProduct(Product):
- """ 具體產(chǎn)品角色 """
- # 公共接口的實(shí)現(xiàn)
- def interface(self):
- print "Concrete Product Method"
在應(yīng)用工廠方法模式時(shí),通常還需要再引入一個(gè)客戶端角色,由它負(fù)責(zé)創(chuàng)建具體的工廠對(duì)象,然后再調(diào)用工廠對(duì)象中的工廠方法來(lái)創(chuàng)建相應(yīng)的產(chǎn)品對(duì)象。以下是客戶端的示例性Python代碼:
- 代碼清單13:client.py
- class Client:
- """ 客戶端角色 """
- def run(self):
- creator = ConcreteCreator()
- product = creator.factoryMethod()
- product.interface()
- # 主函數(shù)
- if (__name__ == "__main__"):
- client = Client()
- client.run()
在這個(gè)簡(jiǎn)單的示意性實(shí)現(xiàn)里,充當(dāng)具體產(chǎn)品和具體工廠角色的類都只有一個(gè),但在真正的實(shí)際應(yīng)用中,通常遇到的都是同時(shí)會(huì)有多個(gè)具體產(chǎn)品類的情況,此時(shí)相應(yīng)地需要提供多個(gè)具體工廠類,每個(gè)具體工廠都負(fù)責(zé)生產(chǎn)對(duì)應(yīng)的具體產(chǎn)品。
工廠方法模式的活動(dòng)序列如圖5所示,客戶端Client首先創(chuàng)建ConcreteCreator對(duì)象,然后調(diào)用ConcreteCreator對(duì)象的工廠方法factoryMethod(),由它負(fù)責(zé)"生產(chǎn)"出所需要的ConcreteProduct對(duì)象。
#p#
四、實(shí)際運(yùn)用
使用工廠方法模式可以在不修改具體工廠角色的情況下引入新的產(chǎn)品,這一點(diǎn)無(wú)疑使得工廠方法模式具有比簡(jiǎn)單工廠模式更好的可擴(kuò)展性。在開(kāi)發(fā)實(shí)際的軟件系統(tǒng)時(shí),通常是先設(shè)計(jì)產(chǎn)品角色,然后才開(kāi)始設(shè)計(jì)工廠角色,而復(fù)雜的需求導(dǎo)致將在抽象產(chǎn)品和具體產(chǎn)品之間形成非常龐大的樹(shù)狀結(jié)構(gòu),如圖6所示。
在上面的產(chǎn)品等級(jí)結(jié)構(gòu)中,出現(xiàn)了多于一個(gè)的抽象產(chǎn)品類,以及多于兩個(gè)的類層次,這是在構(gòu)造真實(shí)系統(tǒng)中經(jīng)常遇到的情況。在為這一軟件體系結(jié)構(gòu)應(yīng)用工廠方法模式時(shí),通常的做法是按照產(chǎn)品的等級(jí)結(jié)構(gòu)再設(shè)計(jì)一個(gè)相同的工廠等級(jí)結(jié)構(gòu),如圖7所示。
定義工廠角色的目的是為了創(chuàng)建相應(yīng)的產(chǎn)品角色,因此整個(gè)系統(tǒng)的架構(gòu)將如圖8所示。這一結(jié)構(gòu)常常被稱為平行的類層次(parallel class hierarchies),它使得一個(gè)類能夠?qū)⑺囊恍┞氊?zé)委托給另一個(gè)獨(dú)立的類,而工廠方法則是聯(lián)系兩者之間的紐帶。工廠方法模式并沒(méi)有限制產(chǎn)品等級(jí)的層數(shù),雖然前面給出的一般性結(jié)構(gòu)中只有兩個(gè)層次(抽象產(chǎn)品層和具體產(chǎn)品層),但在實(shí)際運(yùn)用時(shí)卻往往需要更加復(fù)雜的產(chǎn)品層次。
在工廠方法模式的一般性結(jié)構(gòu)中,每當(dāng)具體工廠類中的工廠方法被請(qǐng)求時(shí),都會(huì)調(diào)用具體產(chǎn)品類的構(gòu)造函數(shù)來(lái)創(chuàng)建一個(gè)新的產(chǎn)品實(shí)例,然后再將這個(gè)實(shí)例提供給客戶端。但在實(shí)際軟件系統(tǒng)中應(yīng)用工廠方法模式時(shí),工廠方法所做的事情可能更加復(fù)雜,其中最常見(jiàn)到的一種情況是循環(huán)使用產(chǎn)品對(duì)象。所采用的策略是將工廠對(duì)象創(chuàng)建的所有產(chǎn)品對(duì)象登記到一個(gè)對(duì)象池(object pool)中,這樣每當(dāng)客戶請(qǐng)求工廠方法創(chuàng)建相應(yīng)的產(chǎn)品對(duì)象時(shí),可以先從對(duì)象池中查詢符合條件的產(chǎn)品對(duì)象,如果對(duì)象池中恰巧有這樣的對(duì)象,那就直接將這個(gè)產(chǎn)品對(duì)象返回給客戶端;如果對(duì)象池中沒(méi)有這樣的對(duì)象,那就創(chuàng)建一個(gè)新的滿足要求的產(chǎn)品對(duì)象,將其登記到對(duì)象池中,然后再返回給客戶端。
工廠方法模式依賴于工廠角色和產(chǎn)品角色的多態(tài)性,但在實(shí)際運(yùn)用時(shí)這個(gè)模式可能出現(xiàn)退化,其表現(xiàn)就是多態(tài)性的喪失。在工廠方法模式中,所有的具體工廠對(duì)象應(yīng)該共享一個(gè)抽象的超類,或者換句話說(shuō),應(yīng)當(dāng)有多個(gè)具體工廠類作為一個(gè)抽象工廠類的子類存在于工廠等級(jí)結(jié)構(gòu)中,但如果工廠等級(jí)結(jié)構(gòu)中只有一個(gè)具體工廠類的話,那么抽象工廠角色可以省略。當(dāng)抽象工廠角色被省略時(shí),工廠方法模式就發(fā)生了退化,這一退化表現(xiàn)為工廠角色多態(tài)性的喪失,退化后的模式仍然可以發(fā)揮部分工廠方法模式的作用,通常被稱為退化的工廠方法模式。退化的工廠方法模式在很大程度上與簡(jiǎn)單工廠模式相似,如圖9所示,實(shí)際運(yùn)用時(shí)可以考慮用簡(jiǎn)單工廠模式進(jìn)行替代。
在工廠方法模式中,從工廠方法返回的應(yīng)當(dāng)是抽象產(chǎn)品類型,而不是具體產(chǎn)品類型,因?yàn)橹挥羞@樣才能保證產(chǎn)品角色的多態(tài)性。也就是說(shuō),調(diào)用工廠方法的客戶端可以針對(duì)抽象產(chǎn)品類進(jìn)行編程,而不必依賴于具體產(chǎn)品類。在實(shí)際運(yùn)用時(shí)有可能會(huì)出現(xiàn)一種很特殊的情況,那就是工廠方法只需要返回一個(gè)具體產(chǎn)品類,此時(shí)工廠方法模式的功能同樣會(huì)發(fā)生退化,但這一退化將表現(xiàn)為產(chǎn)品角色多態(tài)性的喪失,如圖10所示。嚴(yán)格說(shuō)來(lái),當(dāng)工廠方法模式出現(xiàn)這一退化時(shí),就不能再稱為工廠方法模式了,因?yàn)榭蛻舳藦墓S方法的靜態(tài)類型就可以判斷出將要得到的是什么類型的對(duì)象,而這一點(diǎn)恰好違背了工廠方法模式的初衷。
五、優(yōu)勢(shì)和不足
在工廠方法模式中,工廠方法用來(lái)創(chuàng)建客戶所需要的產(chǎn)品,同時(shí)還向客戶隱藏了哪種具體產(chǎn)品類將被實(shí)例化這一細(xì)節(jié)。工廠方法模式的核心是一個(gè)抽象工廠類,各種具體工廠類通過(guò)從抽象工廠類中將工廠方法繼承下來(lái),使得客戶可以只關(guān)心抽象產(chǎn)品和抽象工廠,完全不用理會(huì)返回的是哪一種具體產(chǎn)品,也不用關(guān)心它是如何被具體工廠創(chuàng)建的。
基于工廠角色和產(chǎn)品角色的多態(tài)性設(shè)計(jì)是工廠方法模式的關(guān)鍵,它使得工廠可以自主確定創(chuàng)建何種產(chǎn)品對(duì)象,而如何創(chuàng)建這個(gè)對(duì)象的細(xì)節(jié)則完全封裝在具體工廠內(nèi)部。工廠方法模式之所以又被稱為多態(tài)工廠模式,顯然是因?yàn)樗械木唧w工廠類都具有同一抽象父類。
使用工廠方法模式的另一個(gè)優(yōu)點(diǎn)是在系統(tǒng)中加入新產(chǎn)品時(shí),不需要對(duì)抽象工廠和抽象產(chǎn)品提供的接口進(jìn)行修改,而只要添加一個(gè)具體工廠和具體產(chǎn)品就可以了,沒(méi)有必要修改客戶端,也沒(méi)有必須修改其他的具體工廠和具體產(chǎn)品,系統(tǒng)的可擴(kuò)展性非常好。優(yōu)秀的面向?qū)ο笤O(shè)計(jì)鼓勵(lì)使用封裝(Encapsulation)和委托(Delegation)來(lái)構(gòu)造軟件系統(tǒng),而工廠方法模式則是使用了封裝和委托的典型例子,其中封裝是通過(guò)抽象工廠來(lái)體現(xiàn)的,而委托則是通過(guò)抽象工廠將創(chuàng)建對(duì)象的責(zé)任完全交給具體工廠來(lái)體現(xiàn)的。
使用工廠方法模式的缺點(diǎn)是在添加新產(chǎn)品時(shí),需要編寫(xiě)新的具體產(chǎn)品類,而且還要提供與之對(duì)應(yīng)的具體工廠類,當(dāng)兩者都比較簡(jiǎn)單時(shí),系統(tǒng)的額外開(kāi)銷相對(duì)較大。
六、小結(jié)
工廠方法模式的核心思想是定義一個(gè)用來(lái)創(chuàng)建對(duì)象的公共接口,由工廠而不是客戶來(lái)決定需要被實(shí)例化的類,它通常在構(gòu)造系統(tǒng)整體框架時(shí)被用到。工廠方法模式看上去似乎比較簡(jiǎn)單,但是內(nèi)涵卻極其深刻,抽象、封裝、繼承、委托、多態(tài)等面向?qū)ο笤O(shè)計(jì)中的理論都得到了很好的體現(xiàn),應(yīng)用范圍非常廣泛。
原文鏈接:http://www.ibm.com/developerworks/cn/linux/l-pypt/part3/index.html
【編輯推薦】