寫給Python初學(xué)者的設(shè)計(jì)模式入門
有沒有想過設(shè)計(jì)模式到底是什么?通過本文可以看到設(shè)計(jì)模式為什么這么重要,通過幾個(gè)Python的示例展示為什么需要設(shè)計(jì)模式,以及如何使用。
設(shè)計(jì)模式是什么?
設(shè)計(jì)模式是經(jīng)過總結(jié)、優(yōu)化的,對我們經(jīng)常會(huì)碰到的一些編程問題的可重用解決方案。一個(gè)設(shè)計(jì)模式并不像一個(gè)類或一個(gè)庫那樣能夠直接作用于我們的代碼。反之,設(shè)計(jì)模式更為高級,它是一種必須在特定情形下實(shí)現(xiàn)的一種方法模板。設(shè)計(jì)模式不會(huì)綁定具體的編程語言。一個(gè)好的設(shè)計(jì)模式應(yīng)該能夠用大部分編程語言實(shí)現(xiàn)(如果做不到全部的話,具體取決于語言特性)。最為重要的是,設(shè)計(jì)模式也是一把雙刃劍,如果設(shè)計(jì)模式被用在不恰當(dāng)?shù)那樾蜗聦?huì)造成災(zāi)難,進(jìn)而帶來無窮的麻煩。然而如果設(shè)計(jì)模式在正確的時(shí)間被用在正確地地方,它將是你的救星。
起初,你會(huì)認(rèn)為“模式”就是為了解決一類特定問題而特別想出來的明智之舉。說的沒錯(cuò),看起來的確是通過很多人一起工作,從不同的角度看待問題進(jìn)而形成的一個(gè)最通用、最靈活的解決方案。也許這些問題你曾經(jīng)見過或是曾經(jīng)解決過,但是你的解決方案很可能沒有模式這么完備。
雖然被稱為“設(shè)計(jì)模式”,但是它們同“設(shè)計(jì)“領(lǐng)域并非緊密聯(lián)系。設(shè)計(jì)模式同傳統(tǒng)意義上的分析、設(shè)計(jì)與實(shí)現(xiàn)不同,事實(shí)上設(shè)計(jì)模式將一個(gè)完整的理念根植于程序中,所以它可能出現(xiàn)在分析階段或是更高層的設(shè)計(jì)階段。很有趣的是因?yàn)樵O(shè)計(jì)模式的具體體現(xiàn)是程序代碼,因此可能會(huì)讓你認(rèn)為它不會(huì)在具體實(shí)現(xiàn)階段之前出現(xiàn)(事實(shí)上在進(jìn)入具體實(shí)現(xiàn)階段之前你都沒有意識(shí)到正在使用具體的設(shè)計(jì)模式)。
可以通過程序設(shè)計(jì)的基本概念來理解模式:增加一個(gè)抽象層。抽象一個(gè)事物就是隔離任何具體細(xì)節(jié),這么做的目的是為了將那些不變的核心部分從其他細(xì)節(jié)中分離出來。當(dāng)你發(fā)現(xiàn)你程序中的某些部分經(jīng)常因?yàn)槟承┰蚋膭?dòng),而你不想讓這些改動(dòng)的部分引發(fā)其他部分的改動(dòng),這時(shí)候你就需要思考那些不會(huì)變動(dòng)的設(shè)計(jì)方法了。這么做不僅會(huì)使代碼可維護(hù)性更高,而且會(huì)讓代碼更易于理解,從而降低開發(fā)成本。
設(shè)計(jì)出一個(gè)優(yōu)雅的、易于維護(hù)的程序難點(diǎn)在于發(fā)現(xiàn)我所說的“變化的向量”(在這里,“向量”指的是***的梯度變化方向(maximum gradient),而并非指一個(gè)容器類)。意思是找出系統(tǒng)中變化的最重要的部分,或者換句話說,發(fā)現(xiàn)影響系統(tǒng)***的花銷在哪里。一旦你發(fā)現(xiàn)了變化的向量,你就可以圍繞這個(gè)重點(diǎn)設(shè)計(jì)你的程序。
所以設(shè)計(jì)模式的目的就是分離代碼中的可變部分。如果你這么去審視這個(gè)問題,你會(huì)立刻看到多個(gè)設(shè)計(jì)模式。舉個(gè)例子,面向?qū)ο蟮睦^承(inheritance)可以看做一種設(shè)計(jì)模式(雖然是由編譯器實(shí)現(xiàn)的)。它允許通過同樣的接口(不變的部分)來表現(xiàn)不同的行為(變化的部分)。組合也可以被認(rèn)為是一種設(shè)計(jì)模式,因?yàn)樗试S通過動(dòng)態(tài)或靜態(tài)的方式改變實(shí)現(xiàn)類的對象以及他們的行為。
另一個(gè)常見的設(shè)計(jì)模式例子是迭代器。迭代器自Python出現(xiàn)伊始就已經(jīng)隨for循環(huán)的使用而存在了,并且在Python2.2版本的時(shí)候被明確成為其一個(gè)特性。一個(gè)迭代器隱藏了容器內(nèi)部的具體實(shí)現(xiàn),提供一個(gè)依次訪問容器對象內(nèi)每個(gè)元素的方式。所以,你能夠使用通用的代碼對一個(gè)序列的每個(gè)元素做對應(yīng)操作而不用去理會(huì)此序列是怎么建立的。所以你的代碼能夠?qū)θ魏文軌虍a(chǎn)生迭代器的對象有效。
這里列舉了三種最基本的設(shè)計(jì)模式:
-
結(jié)構(gòu)化模式,通常用來處理實(shí)體之間的關(guān)系,使得這些實(shí)體能夠更好地協(xié)同工作。
-
創(chuàng)建模式,提供實(shí)例化的方法,為適合的狀況提供相應(yīng)的對象創(chuàng)建方法。
-
行為模式,用于在不同的實(shí)體建進(jìn)行通信,為實(shí)體之間的通信提供更容易,更靈活的通信方法。
我們?yōu)槭裁匆褂迷O(shè)計(jì)模式?
從理論上來說,設(shè)計(jì)模式是對程序問題比較好的解決方案。無數(shù)的程序員都曾經(jīng)遇到過這些問題,并且他們使用這些解決方案去處理這些問題。所以當(dāng)你遇到同樣的問題,為什么要去想著創(chuàng)建一個(gè)解決方案而不是用現(xiàn)成的并且被證明是有效的呢?
例子
假定現(xiàn)在有一個(gè)任務(wù),需要你找到一個(gè)有效的方法合并兩個(gè)做不同事情的類,在已有系統(tǒng)中這兩個(gè)類在許多不同的地方被大量使用,所以移除這兩個(gè)類或是改動(dòng)已有的代碼都是異常困難的。不僅如此,更改已有的代碼會(huì)導(dǎo)致大量的測試工作,因?yàn)樵谶@樣一種依賴大量不同組件的系統(tǒng)中,這些修改總是會(huì)引入一些新的錯(cuò)誤。為了避免這些麻煩,你可以實(shí)現(xiàn)一個(gè)策略模式(Strategy Pattern)和適配器模式(Adapter Pattern)的變體,這兩種模式能夠很好的處理這種問題。
- class StrategyAndAdapterExampleClass():
- def __init__(self, context, class_one, class_two):
- self.context = context
- self.class_one = class_one
- self.class_two = class_two
- def operation1(self):
- if self.context == "Context_For_Class_One":
- self.class_one.operation1_in_class_one_context()
- else:
- self.class_two.operational_in_class_two_context()
很簡單是吧?現(xiàn)在讓我們來仔細(xì)研究一下策略模式。
策略模式
策略模式是一種與行為相關(guān)的設(shè)計(jì)模式,允許你在運(yùn)行時(shí)根據(jù)指定的上下文確定程序的動(dòng)作。你可以在兩個(gè)類中封裝不同的算法,并且在程序運(yùn)行時(shí)確定到底執(zhí)行哪種策略。
在上面的例子中,策略是根據(jù)實(shí)例化時(shí)context變量的值來決定的。如果給定context變量的值是“class_one”,將會(huì)執(zhí)行class_one,否則就會(huì)執(zhí)行class_two。
我在那里使用它?
假定你現(xiàn)在正在寫一個(gè)類能夠更新或創(chuàng)建一條新的用戶記錄,接收同樣的輸入?yún)?shù)(諸如姓名、地址、手機(jī)號等),但是根據(jù)不同的情況會(huì)調(diào)用對應(yīng)的更新或是創(chuàng)建方法。當(dāng)然,你可能會(huì)用一個(gè)if-else判斷處理這個(gè)問題,但是如果你需要在不同的地方使用這個(gè)類呢?那么你就得不停地重寫if-else判斷。為什么不簡單地通過指定上下文來解決這個(gè)問題。
- class User():
- def create_or_update(self, name, address, mobile, userid=None):
- if userid:
- # it means the user doesn't exist yet, create a new record
- else:
- # it means the user already exists, just update based on the given userid
常規(guī)的策略模式涉及到將算法封裝到另一個(gè)類中,但如果這樣的話,那個(gè)類就太浪費(fèi)了。切記不要死記模板,把握住核心概念靈活的變通,最重要是解決問題。
#p#
適配器模式
適配器模式是一個(gè)結(jié)構(gòu)性的設(shè)計(jì)模式,允許通過不同的接口為一個(gè)類賦予新的用途,這使得使用不同調(diào)用方式的系統(tǒng)都能夠使用這個(gè)類。
也可以讓你改變通過客戶端類接收到的輸入?yún)?shù)以適應(yīng)被適配者的相關(guān)函數(shù)。
怎么使用?
另一個(gè)使用適配器類的地方是包裝器(wrapper),允許你講一個(gè)動(dòng)作包裝成為一個(gè)類,然后可以在合適的情形下復(fù)用這個(gè)類。一個(gè)典型的例子是當(dāng)你為一個(gè)table類簇創(chuàng)建一個(gè)domain類時(shí),你能夠?qū)⑺械膶?yīng)不同表的相同動(dòng)作封裝成為一個(gè)適配器類,而不是一個(gè)接一個(gè)的單獨(dú)調(diào)用這些不同的動(dòng)作。這不僅使得你能夠重用你想要的所有操作,而且當(dāng)你在不同的地方使用同樣的動(dòng)作時(shí)不用重寫代碼。
比較一下兩種實(shí)現(xiàn):
不用適配器的方案
- class User(object):
- def create_or_update(self):
- pass
- class Profile(object):
- def create_or_update(self):
- pass
- user = User()
- user.create_or_update()
- profile = Profile()
- profile.create_or_update()
如果我們需要在不同的地方做同樣的事,或是在不同的項(xiàng)目中重用這段代碼,那么我們需要重新敲一遍。
使用包裝類的解決方案
看看我們怎么反其道而行:
- account_domain = Account()
- account_domain.NewAccount()
在這種情況下,我們通過一個(gè)包裝類來實(shí)現(xiàn)賬戶domain類:
- class User(object):
- def create_or_update(self):
- pass
- class Profile(object):
- def create_or_update(self):
- pass
- class Account():
- def new_account(self):
- user = User()
- user.create_or_update()
- profile = Profile()
- profile.create_or_update()
這樣的話,你就能夠在你需要的時(shí)候使用賬戶domain了,你也可以將其他的類包裝到domain類下。
工廠模式
工廠模式是一種創(chuàng)建型的設(shè)計(jì)模式,作用如其名稱:這是一個(gè)就像工廠那樣生產(chǎn)對象實(shí)例的類。
這個(gè)模式的主要目的是將可能涉及到很多類的對象創(chuàng)建過程封裝到一個(gè)單獨(dú)的方法中。通過給定的上下文輸出指定的對象實(shí)例。
什么時(shí)候使用?
使用工廠模式的***時(shí)機(jī)就是當(dāng)你需要使用到單個(gè)實(shí)體的多個(gè)變體時(shí)。舉個(gè)例子,你有一個(gè)按鈕類,這個(gè)按鈕類有多種變體,例如圖片按鈕、輸入框按鈕或是flash按鈕等。那么在不同的場合你會(huì)需要?jiǎng)?chuàng)建不同的按鈕,這時(shí)候就可以通過一個(gè)工廠來創(chuàng)建不同的按鈕。
讓我們先來創(chuàng)建三個(gè)類:
- class Button(object):
- html = ""
- def get_html(self):
- return self.html
- class Image(Button):
- html = "<img alt="" />"
- class Input(Button):
- html = "<input type="text" />"
- class Flash(Button):
- html = ""
然后創(chuàng)建我們的工廠類:
- class ButtonFactory():
- def create_button(self, typ):
- targetclass = typ.capitalize()
- return globals()[targetclass]()
譯注:globals()將以字典的方式返回所有全局變量,因此targetclass = typ.capitalize()將通過傳入的typ字符串得到類名(Image、Input或Flash),而globals()[targetclass]將通過類名取到類的類(見元類),而globals()[targetclass]()將創(chuàng)建此類的對象。
我們可以這么使用工廠類:
- button_obj = ButtonFactory()
- button = ['image', 'input', 'flash']
- for b in button:
- print button_obj.create_button(b).get_html()
輸出將是所有按鈕類型的HTML屬性。這樣罵你就能夠根據(jù)不同的情況指定不同類型的按鈕了,并且很易于重用。
#p#
裝飾器模式
裝飾器模式是一個(gè)結(jié)構(gòu)性模式,允許我們根據(jù)情況,在運(yùn)行時(shí)為一個(gè)對象添加新的或附加的行為。
目的是為給一個(gè)特定的對象實(shí)例應(yīng)用擴(kuò)展的函數(shù)方法,并且同時(shí)也能夠產(chǎn)生沒有新方法的原對象。它允許多裝飾器結(jié)合用于一個(gè)實(shí)例,所以你就不會(huì)出現(xiàn)實(shí)例同單個(gè)裝飾器相捆綁的情況了。這個(gè)模式是實(shí)現(xiàn)子類繼承外的一個(gè)可選方式,子類繼承是指從父類集成相應(yīng)的功能。與子類繼承必須在編譯時(shí)添加相應(yīng)的行為不同,裝飾器允許你在運(yùn)行時(shí)根據(jù)需要添加新的行為。
可以根據(jù)以下步驟實(shí)現(xiàn)裝飾器模式:
-
以原組件類為基類創(chuàng)建裝飾器類。
-
在裝飾器類中添加一個(gè)組件類的指針域
-
將一個(gè)組件傳遞給裝飾器類的構(gòu)造器以初始化組件類指針
-
在裝飾器類中,將所有的組件方法指向組件類指針,并且,
-
在裝飾器類中,重寫每個(gè)需要修改功能的組件方法。
相關(guān)維基百科(http://en.wikipedia.org/wiki/Decorator_pattern)
什么時(shí)候使用?
使用裝飾器模式的***時(shí)機(jī)是當(dāng)你有一個(gè)根據(jù)情況需要添加新的行為的實(shí)體時(shí)。假設(shè)你有一個(gè)HTML鏈接元素,一個(gè)登出鏈接,并且你希望根據(jù)當(dāng)前頁面對具體的行為做微小的改動(dòng)。這種情況下,我們可以使用裝飾器模式。
首先,建立我們所需要的裝飾模式。
如果我們在主頁并且已經(jīng)登錄,那么將登出鏈接用h2標(biāo)簽標(biāo)記。
如果我們在不同的頁面并且已經(jīng)登錄,那么用下劃線標(biāo)簽標(biāo)記鏈接
如果已登錄,用加粗標(biāo)記鏈接。
一旦建立了裝飾模式,我們就可以開工了。
- class HtmlLinks():
- def set_html(self, html):
- self.html = html
- def get_html(self):
- return self.html
- def render(self):
- print(self.html)
- class LogoutLink(HtmlLinks):
- def __init__(self):
- self.html = "<a href="logout.html"> Logout </a>"
- class LogoutLinkH2Decorator(HtmlLinks):
- def __init__(self, logout_link):
- self.logout_link = logout_link
- self.set_html(" {0} ".format(self.logout_link.get_html()))
- def call(self, name, args):
- self.logout_link.name(args[0])
- class LogoutLinkUnderlineDecorator(HtmlLinks):
- def __init__(self, logout_link):
- self.logout_link = logout_link
- self.set_html(" {0} ".format(self.logout_link.get_html()))
- def call(self, name, args):
- self.logout_link.name(args[0])
- class LogoutLinkStrongDecorator(HtmlLinks):
- def __init__(self, logout_link):
- self.logout_link = logout_link
- self.set_html("<strong> {0} </strong>".format(self.logout_link.get_html()))
- def call(self, name, args):
- self.logout_link.name(args[0])
- logout_link = LogoutLink()
- is_logged_in = 0
- in_home_page = 0
- if is_logged_in:
- logout_link = LogoutLinkStrongDecorator(logout_link)
- if in_home_page:
- logout_link = LogoutLinkH2Decorator(logout_link)
- else:
- logout_link = LogoutLinkUnderlineDecorator(logout_link)
- logout_link.render()
單例模式
單例模式是一個(gè)創(chuàng)建型的設(shè)計(jì)模式,功能是確保運(yùn)行時(shí)對某個(gè)類只存在單個(gè)實(shí)例對象,并且提供一個(gè)全局的訪問點(diǎn)來訪問這個(gè)實(shí)例對象。
因?yàn)閷τ谡{(diào)用單例的其他對象而言這個(gè)全局唯一的訪問點(diǎn)“協(xié)調(diào)”了對單例對象的訪問請求,所以這些調(diào)用者看到的單例內(nèi)變量都將是同一份。
什么時(shí)候能夠使用?
單例模式可能是最簡單的設(shè)計(jì)模式了,它提供特定類型的唯一對象。為了實(shí)現(xiàn)這個(gè)目標(biāo),你必須控制程序之外的對象生成。一個(gè)方便的方法是將一個(gè)私有內(nèi)部類的單個(gè)對象作為單例對象。
- class OnlyOne:
- class __OnlyOne:
- def __init__(self, arg):
- self.val = arg
- def __str__(self):
- return repr(self) + self.val
- instance = None
- def __init__(self, arg):
- if not OnlyOne.instance:
- OnlyOne.instance = OnlyOne.__OnlyOne(arg)
- else:
- OnlyOne.instance.val = arg
- def __getattr__(self, name):
- return getattr(self.instance, name)
- x = OnlyOne('sausage')
- print(x)
- y = OnlyOne('eggs')
- print(y)
- z = OnlyOne('spam')
- print(z)
- print(x)
- print(y)
- print(`x`)
- print(`y`)
- print(`z`)
- output = '''
- <__main__.__OnlyOne instance at 0076B7AC>sausage
- <__main__.__OnlyOne instance at 0076B7AC>eggs
- <__main__.__OnlyOne instance at 0076B7AC>spam
- <__main__.__OnlyOne instance at 0076B7AC>spam
- <__main__.__OnlyOne instance at 0076B7AC>spam
- <__main__.OnlyOne instance at 0076C54C>
- <__main__.OnlyOne instance at 0076DAAC>
- <__main__.OnlyOne instance at 0076AA3C>
- '''
因?yàn)閮?nèi)置類是用雙下劃線開始命名,所以它是私有的,用戶無法直接訪問。內(nèi)置類包含了所有你希望放在普通類中的方法,并且通過外層包裝類的構(gòu)造器控制其創(chuàng)建。當(dāng)***次你創(chuàng)建OnlyOne時(shí),初始化一個(gè)實(shí)例對象,后面則會(huì)忽略創(chuàng)建新實(shí)例的請求。
通過代理的方式進(jìn)行訪問,使用__getattr__()方法將所有調(diào)用指向單例。你可以從輸出看到雖然看起來好像創(chuàng)建了多個(gè)對象(OnlyOne),但 __OnlyOne對象只有一個(gè)。雖然OnlyOne實(shí)例有多個(gè),但他們都是唯一的 __OnlyOne對象的代理。
請注意上面的方法并沒有限制你只能創(chuàng)建一個(gè)對象,這也是一個(gè)創(chuàng)建有限個(gè)對象池的技術(shù)。然而在那種情況下,你可能會(huì)遇到共享池內(nèi)對象的問題。如果這真是一個(gè)問題,那你可以通過為共享對象設(shè)計(jì)簽入“check-in”和遷出“check-out”機(jī)制來解決這個(gè)問題。
總結(jié)
在本文中,我只列舉了幾個(gè)我再編程中覺得十分重要的設(shè)計(jì)模式來講,除此之外還有很多設(shè)計(jì)模式需要學(xué)習(xí)。如果你對其他的設(shè)計(jì)模式感興趣,維基百科的設(shè)計(jì)模式部分(http://en.wikipedia.org/wiki/Design_pattern_%28computer_science%29)可以提供很多信息。如果還嫌不夠,你可以看看四人幫的《設(shè)計(jì)模式:可復(fù)用面向?qū)ο筌浖幕A(chǔ)》(http://www.amazon.com/o/asin/0201633612)一書,此書是關(guān)于設(shè)計(jì)模式的經(jīng)典之作。
***一件事:當(dāng)使用設(shè)計(jì)模式時(shí),確保你是用來解決正確地問題。正如我之前提到的,設(shè)計(jì)模式是把雙刃劍:如果使用不當(dāng),它們會(huì)造成潛在的問題;如果使用得當(dāng),它們則將是不可或缺的。