《精通python設(shè)計模式》讀書筆記之——行為型設(shè)計模式

行為型模式:

介紹處理系統(tǒng)實體之間通信的設(shè)計模式。

①.責(zé)任鏈模式

簡介:
開發(fā)一個應(yīng)用時,多數(shù)時候我們都能預(yù)先知道哪個方法能處理某個特定請求。然而,情況并非總是如此。例如,想想任意一種廣播計算機網(wǎng)絡(luò),例如早的以太網(wǎng)實現(xiàn)。在廣播計算機網(wǎng)絡(luò)中,會將所有請求發(fā)送給所有節(jié)點(簡單起見,不考慮廣播域),但僅對所發(fā)送請求感興趣的節(jié)點會處理請求。加入廣播網(wǎng)絡(luò)的所有計算機使用一種常見的媒介相互連接,
.
如果一個節(jié)點對某個請求不感興趣或者不知道如何處理這個請求,可以執(zhí)行以下兩個操作:
1. 忽略這個請求,什么都不做
2. 將請求轉(zhuǎn)發(fā)給下一個節(jié)點
.
責(zé)任鏈(Chain of Responsibility)模式用于讓多個對象來處理單個請求時,或者用于預(yù)先不知道應(yīng)該由哪個對象(來自某個對象鏈)來處理某個特定請求時。其原則 如下所示:
.
1. 存在一個對象鏈(鏈表、樹或任何其他便捷的數(shù)據(jù)結(jié)構(gòu))。
2. 我們一開始將請求發(fā)送給鏈中的第一個對象。
3. 對象決定其是否要處理該請求。
4. 對象將請求轉(zhuǎn)發(fā)給下一個對象。
5. 重復(fù)該過程,直到到達(dá)鏈尾。
.
客戶端代碼僅知道第一個處理元素,而非擁有對所有處理元素的引用;并且每個處理元素僅知道其直接的下一個鄰居(稱為后繼),而不知道所有其他處理元素。這通常是一種單向關(guān)系,用編程術(shù)語來說是一個單向鏈表,與之相反的是雙向鏈表。單向鏈表不允許雙向地遍歷元素,雙向鏈表則是允許的。這種鏈?zhǔn)浇M織方式大有用處:可以解耦發(fā)送方(客戶端)和接收方(處理元素)

現(xiàn)實生活的例子:
ATM機以及及一般而言用于接收/返回鈔票或硬幣的任意類型機器(比如,零食自動販賣機)都使用了責(zé)任鏈模式。
機器上總會有一個放置各種鈔票的槽口,鈔票放入之后,會被傳遞到恰當(dāng)?shù)娜萜鳌bn票返回時,則是從恰當(dāng)?shù)娜萜髦蝎@取,我們可以把這個槽口視為共享通信媒介,不同的容器則是處理元素。結(jié)果包含來自一個或多個容器的現(xiàn)金。

軟件的例子:
我試過尋找一些使用責(zé)任鏈模式的Python應(yīng)用的好例子,但是沒找到,很可能是因為Python程序員不使用這個名稱。因此,很抱歉,我將使用其他編程語言的例子作為參考。
.
Java的servlet過濾器是在一個HTTP請求到達(dá)目標(biāo)處理程序之前執(zhí)行的一些代碼片段。在使用servlet過濾器時,有一個過濾器鏈,其中每個過濾器執(zhí)行一個不同動作(用戶身份驗證、記日志、數(shù)據(jù)壓縮等),并且將請求轉(zhuǎn)發(fā)給下一個過濾器直到鏈結(jié)束;如果發(fā)生錯誤(例如,連續(xù)三次身 份驗證失?。﹦t跳出處理流程。
.
Apple的Cocoa和Cocoa Touch框架使用責(zé)任鏈來處理事件。在某個視圖接收到一個其并不知道如何處理的事件時,會將事件轉(zhuǎn)發(fā)給其超視圖,直到有個視圖能夠處理這個事件或者視圖鏈結(jié)束。

應(yīng)用案例:
通過使用責(zé)任鏈模式,我們能讓許多不同對象來處理一個特定請求。在我們預(yù)先不知道應(yīng)該由哪個對象來處理某個請求時,這是有用的。其中一個例子是采購系統(tǒng)。在采購系統(tǒng)中,有許多核準(zhǔn)權(quán)限。某個核準(zhǔn)權(quán)限可能可以核準(zhǔn)在一定額度之內(nèi)的訂單,假設(shè)為100美元。如果訂單超過了100美元,則會將訂單發(fā)送給鏈中的下一個核準(zhǔn)權(quán)限,比如能夠核準(zhǔn)在200美元以下的訂單。
.
另一個責(zé)任鏈可以派上用場的場景是,在我們知道可能會有多個對象都需要對同一個請求進(jìn)行處理之時。這在基于事件的編程中是常有的事情。單個事件,比如一次鼠標(biāo)左擊,可被多個事件監(jiān)聽者捕獲
.
不過應(yīng)該注意,如果所有請求都能被單個處理程序處理,責(zé)任鏈就沒那么有用了,除非確實不知道會是哪個程序處理請求。這一模式的價值在于解耦??蛻舳伺c所有處理程序(一個處理程序與所有其他處理程序之間也是如此)之間不再是多對多關(guān)系,客戶端僅需要知道如何與鏈的起始節(jié)點(標(biāo)頭)進(jìn)行通信。

代碼實現(xiàn):

class Event:
    """
    Event類描述一個事件。為了讓它簡單一點,在我們的案例中一個事件只有一個name屬性。
    """
    def __init__(self, name):
        self.name = name

    def __str__(self):
        return self.name

class Widget:
    """
    Widget類是應(yīng)用的核心類。UML圖中展示的parent聚合關(guān)系表明每個控件都有一個到父對象的引用。按照約定,
    我們假定父對象是一個Widget實例。然而,注意,根據(jù)繼承的規(guī)則,
    任何Widget子類的實例(例如,MsgText的實例)也是Widget實例。parent的默認(rèn)值為None。
    """
    def __init__(self, parent=None):
        self.parent = parent

    def handle(self, event):
        """
        handle()方法使用動態(tài)分發(fā),通過hasattr()和getattr()決定一個特定請求(event) 應(yīng)該由誰來處理。
        如果被請求處理事件的控件并不支持該事件,則有兩種回退機制。如果控件有 parent,則執(zhí)行parent的handle()方法
        如果控件沒有parent,但有handle_default()方法,則執(zhí)行handle_default()。
        """
        handler = 'handle_{}'.format(event)
        if hasattr(self, handler):
            getattr(self, handler)(event)
        elif self.parent:
            self.parent.handle(event)
        elif hasattr(self, 'handle_default'):
            self.handle_default(
                event)

class MainWindow(Widget):
    """
    MainWindow是行為的控件。能處理close和default事件。
    """
    def handle_close(self, event):
        print('MainWindow: {}'.format(event))

    def handle_default(self, event):
        print('MainWindow Default: {}'.format(event))

class SendDialog(Widget):
    """
    SendDialog是行為的控件。SendDialog僅能處理paint事件。
    """
    def handle_paint(self, event): print('SendDialog: {}'.format(event))

class MsgText(Widget):
    """
    MMsgText是行為的控件。MsgText僅能處理down事件
    """
    def handle_down(self, event):
        print('MsgText: {}'.format(event))

def main():
    """
    main()函數(shù)展示如何創(chuàng)建一些控件和事件,以及控件如何對那些事件作出反應(yīng)。所有事件都會被發(fā)送給所有控件。
    注意其中每個控件的父子關(guān)系。sd對象(SendDialog的一個實例)的 父對象是mw(MainWindow的一個實例)。
    然而,并不是所有對象都需要一個MainWindow實例的 父對象。例如,msg對象(MsgText的一個實例)是以sd作為父對象。
    """
    mw = MainWindow()
    sd = SendDialog(mw)
    msg = MsgText(sd)

    for e in ('down', 'paint', 'unhandled', 'close'):
        evt = Event(e)
        print('\nSending event -{}- to MainWindow'.format(evt))
        mw.handle(evt)
        print('Sending event -{}- to SendDialog'.format(evt))
        sd.handle(evt)
        print('Sending event -{}- to MsgText'.format(evt))
        msg.handle(evt)

if __name__ == '__main__':
    main()

責(zé)任鏈?zhǔn)录〗Y(jié):
了責(zé)任鏈設(shè)計模式。在無法預(yù)先知道處理程序的數(shù)量和類型時,該模式有助于對請求或處理事件進(jìn)行建模。適合使用責(zé)任鏈模式的系統(tǒng)例子包括基于事件的系統(tǒng)、采購系統(tǒng)和運輸系統(tǒng)。
.
在責(zé)任鏈模式中,發(fā)送方可直接訪問鏈中的首個節(jié)點。若首個節(jié)點不能處理請求,則轉(zhuǎn)發(fā)給下一個節(jié)點,如此直到請求被某個節(jié)點處理或者整個鏈遍歷結(jié)束。這種設(shè)計用于實現(xiàn)發(fā)送方與接收方(多個)之間的解耦。
.
ATM機是責(zé)任鏈的一個例子。用于取放鈔票的槽口可看作是鏈的頭部。從這里開始,根據(jù)具體交易,一個或多個容器會被用于處理交易。這些容器可看作是鏈中的處理程序。
.
Java的servlet過濾器使用責(zé)任鏈模式對一個HTTP請求執(zhí)行不同的動作(例如,壓縮和身份驗證)。Apple的Cocoa框架使用相同的模式來處理事件,比如,按鈕和手勢。

②.命令模式

命令設(shè)計模式:
命令設(shè)計模式幫助我們將一個操作(撤銷、重做、復(fù)制、粘貼等)封裝成一個對象。簡而言之,這意味著創(chuàng)建一個類,包含實現(xiàn)該操作所需要的所有邏輯和方法。
.
1. 我們并不需要直接執(zhí)行一個命令。命令可以按照希望執(zhí)行。
2. 調(diào)用命令的對象與知道如何執(zhí)行命令的對象解耦。調(diào)用者無需知道命令的任何實現(xiàn)細(xì)節(jié)。
3. 如果有意義,可以把多個命令組織起來,這樣調(diào)用者能夠按順序執(zhí)行它們。例如,在實現(xiàn)一個多層撤銷命令時,這是很有用的。

現(xiàn)實生活的例子:
當(dāng)我們?nèi)ゲ宛^吃飯時,會叫服務(wù)員來點單。他們用來做記錄的賬單(通常是紙質(zhì)的)就是命令模式的一個例子。在記錄好訂單后,服務(wù)員將其放入賬單隊列,廚師會照著單子去做。每個賬單都是獨立的,并且可用來執(zhí)行許多不同命令。

軟件的例子:
PyQt是QT工具包的Python綁定。PyQt包含一個QAction類,將一個動作建模為一個命令。對每個動作都支持額外的可選信息,比如,描述、工具提示、快捷鍵和其他。
git-cola是使用Python語言編寫的一個Git GUI,它使用命令 模式來修改模型、變更一次提交、應(yīng)用一個差異選擇、簽出。

應(yīng)用案例:
許多開發(fā)人員以為撤銷例子是命令模式的唯一應(yīng)用案例。撤銷操作確實是命令模式的殺手級 特性,然而命令模式能做的實際上還有很多:
1. GUI按鈕和菜單項:前面提過的PyQt例子使用命令模式來實現(xiàn)按鈕和菜單項上的動作。
2. 其他操作:除了撤銷,命令模式可用于實現(xiàn)任何操作。其中一些例子包括剪切、復(fù)制、 粘貼、重做和文本大寫。
3. 事務(wù)型行為和日志記錄:事務(wù)型行為和日志記錄對于為變更記錄一份持久化日志是很重要的。操作系統(tǒng)用它來從系統(tǒng)崩潰中恢復(fù), 關(guān)系型數(shù)據(jù)庫用它來實現(xiàn)事務(wù),文件系統(tǒng)用 它來實現(xiàn)快照,而安裝程序(向?qū)С绦颍┯盟鼇砘謴?fù)取消的安裝。
4. 宏:在這里,宏是指一個動作序列,可在任意時間點按要求進(jìn)行錄制和執(zhí)行。流行的編輯器(比如,Emacs和Vim)都支持宏。

代碼實現(xiàn):

import os

verbose = True

class RenameFile:
    """
    重命名工具,使用RenameFile類來實現(xiàn)。__init__()方法接受源文件路徑(path_src)和目標(biāo)文件路徑(path_dest)作為參數(shù)。
    如果文件路徑未使用路徑分隔符,則在當(dāng)前目錄下創(chuàng)建文件。使用路徑分隔符的一個例子是傳遞字符串/tmp/file1作為path_src,
    字符串/home/user/file2作為path_dest。不使用路徑的例子則是傳遞file1作為path_src, file2作為path_dest。
    """
    def __init__(self, path_src, path_dest):
        self.src, self.dest = path_src, path_dest

    def execute(self):
        """
        execute()方法使用os.rename()完成實際的重命名。verbose是一個全局標(biāo)記,被激活時(默認(rèn)是激活的),能向用戶反饋執(zhí)行的操作。
        如果你傾向于靜默地執(zhí)行命令,則可以取消激活狀態(tài)。注意,雖然對于示例來說print()足夠好了,
        但通常會使用更成熟更強大的方式,例如,日志模塊
        """
        if verbose:
            print("[renaming '{}' to '{}']".format(self.src, self.dest))
        os.rename(self.src, self.dest)

    def undo(self):
        """
        我們的重命名工具通過undo()方法支持撤銷操作。在這里,撤銷操作再次使用os.rename() 將文件名恢復(fù)為原始值。
        :return:
        """
        if verbose:
            print("[renaming '{}' back to '{}']".format(self.dest, self.src))
        os.rename(self.dest, self.src)


class CreateFile:
    """
    再次回到使用類的方式。CreateFile類用于創(chuàng)建一個文件。__init__()函數(shù)接受熟悉的path參數(shù)和一個txt字符串,
    默認(rèn)向文件寫入hello world文本。通常來說,合理的默認(rèn)行為是創(chuàng)建一個空文件,但因這個例子的需要,
    我決定向文件寫個一個默認(rèn)字符串??梢愿鶕?jù)需要更改它
    """
    def __init__(self, path, txt='hello world\n'):
        self.path, self.txt = path, txt

    def execute(self):
        """
        execute()方法使用with語句和open()來打開文件(mode='w'意味著寫模式),并使用 write()來寫入txt字符串。
        :return:
        """
        if verbose:
            print("[creating file '{}']".format(self.path))
        with open(self.path, mode='w', encoding='utf-8') as out_file:
            out_file.write(self.txt)

    def undo(self):
        """
        創(chuàng)建一個文件的撤銷操作是刪除它。因此,undo()簡單地使用delete_file()來實現(xiàn)目的。
        :return:
        """
        delete_file(self.path)

class ReadFile:
    """
    ReadFile類的execute()方法再次使用with() 語句配合open(),這次是讀模式,并且只是使用print()來輸出文件內(nèi)容。
    """
    def __init__(self, path):
        self.path = path

    def execute(self):
        if verbose:
            print("[reading file '{}']".format(self.path))
        with open(self.path, mode='r', encoding='utf-8') as in_file:
            print(in_file.read(), end='')

def delete_file(path):
    """
    文件刪除功能實現(xiàn)為單個函數(shù),而不是一個類。我想讓你明白并不一定要為想要添加的每個命令(之后會涉及更多)都創(chuàng)建一個新類。
    delete_file()函數(shù)接受一個字符串類型的文件路 徑,并使用os.remove()來刪除它。
    """
    if verbose:
        print("deleting file '{}'".format(path))
    os.remove(path)

def main():
    # main()函數(shù)使用這些工具類/方法。參數(shù)orig_name和new_name是待創(chuàng)建文件的原始名稱以及重命名后的新名稱。
    orig_name, new_name = 'file1', 'file2'

    # commands列表用于添加(并配置)所有我們之后想要執(zhí)行的命令。注意,命令不會被執(zhí)行,除非我們顯式地調(diào)用每個命令的execute()。
    commands = []
    for cmd in CreateFile(orig_name), ReadFile(orig_name), RenameFile(orig_name, new_name):
        commands.append(cmd)

    # 循環(huán)執(zhí)行命令
    [c.execute() for c in commands]

    # 下一步是詢問用戶是否需要撤銷執(zhí)行過的命令。用戶選擇撤銷命令或不撤銷。
    answer = input('reverse the executed commands? [y/n] ')

    # 如果選擇不撤銷,則輸出結(jié)果
    if answer not in 'yY':
        print("the result is {}".format(new_name))
        exit()

    # 如果選擇撤銷,則執(zhí)行commands列表中所有命令的undo()。由于并不是所有命令都支持撤銷,
    # 因此在undo()方法不存在時產(chǎn)生的AttributeError異常要使用異常處理來捕獲。如果你不喜歡對這種情況使用
    # 異常處理,可以通過添加一個布爾方法(例如,supports_undo() 或 can_be_undone())來顯式地檢測命令是否支持撤銷操作。
    for c in reversed(commands):
        try:
            c.undo()
        except AttributeError as e:
            pass

if __name__ == "__main__":
    main()

命令模式總結(jié):
我們學(xué)習(xí)了命令模式。使用這種設(shè)計模式,可以將一個操作(比如,復(fù)制/粘貼) 封裝為一個對象。這樣能提供很多好處,如下所述:
.
1. 我們可以在任何時候執(zhí)行一個命令,而并不一定是在命令創(chuàng)建時。
2. 執(zhí)行一個命令的客戶端代碼并不需要知道命令的任何實現(xiàn)細(xì)節(jié)。
3. 可以對命令進(jìn)行分組,并按一定的順序執(zhí)行。
.
雖然至今命令模式最廣為人知的特性是撤銷操作,但它還有更多用處。一般而言,要在運行時按照用戶意愿執(zhí)行的任何操作都適合使用命令模式。命令模式也適用于組合多個命令。這有助于實現(xiàn)宏、多級撤銷以及事務(wù)。一個事務(wù)應(yīng)該:要么成功,這意味著事務(wù)中所有操作應(yīng)該都成功(提交操作);要么如果至少一個操作失敗,則全部失?。ɑ貪L操作)。

③.解釋器模式:

簡介:
對每個應(yīng)用來說,至少有以下兩種不同的用戶分類:
1. 基本用戶:這類用戶只希望能夠憑直覺使用應(yīng)用。他們不喜歡花太多時間配置或?qū)W習(xí)應(yīng)用的內(nèi)部。對他們來說,基本的用法就足夠了。 2. 高級用戶:這些用戶,實際上通常是少數(shù),不介意花費額外的時間學(xué)習(xí)如何使用應(yīng)用的。高級特性。如果知道學(xué)會之后能得到以下好處,他們甚至?xí)W(xué)習(xí)一種配置(或腳本)語言。
1. 能夠更好地控制一個應(yīng)用
2. 以更好的方式表達(dá)想法
3. 提高生產(chǎn)力
.
解釋器(Interpreter)模式僅能引起應(yīng)用的高級用戶的興趣。這是因為解釋器模式背后的主要思想是讓非初級用戶和領(lǐng)域?qū)<沂褂靡婚T簡單的語言來表達(dá)想法。
.
領(lǐng)域特定語言(Domain Specific Language,DSL)DSL是一種針對一個特定領(lǐng)域的有限表達(dá)能力的計算機語言。很多不同的事情都使用DSL,比如,戰(zhàn)斗模擬、記賬、可視化、配置、通信協(xié)議等。DSL分為內(nèi)部DSL和外部DSL:
.
1. 內(nèi)部DSL構(gòu)建在一種宿主編程語言之上。內(nèi)部DSL的一個例子是,使用Python解決線性方程組的一種語言。使用內(nèi)部DSL的優(yōu)勢是我們不必?fù)?dān)心創(chuàng)建、編譯及解析語法,因為這些已經(jīng)被宿主語言解決掉了。劣勢是會受限于宿主語言的特性。如果宿主語言不具備這些特性,構(gòu)建一種表達(dá)能力強、簡潔而且優(yōu)美的內(nèi)部DSL是富有挑戰(zhàn)性的
.
2. 外部DSL不依賴某種宿主語言。DSL的創(chuàng)建者可以決定語言的方方面面(語法、句法等),但也要負(fù)責(zé)為其創(chuàng)建一個解析器和編譯器。為一種新語言創(chuàng)建解析器和編譯器是一個非常復(fù)雜、長期而又痛苦的過程。
.
解釋器模式僅與內(nèi)部DSL相關(guān)。因此,我們的目標(biāo)是使用宿主語言提供的特性構(gòu)建一種簡單但有用的語言,在這里,宿主語言是Python。注意,解釋器根本不處理語言解析,它假設(shè)我們已 經(jīng)有某種便利形式的解析好的數(shù)據(jù),可以是抽象語法樹(abstract syntax tree,AST)或任何其他好用的數(shù)據(jù)結(jié)構(gòu)。

現(xiàn)實生活的例子:
音樂演奏者是現(xiàn)實中解釋器模式的一個例子。五線譜圖形化地表現(xiàn)了聲音的音調(diào)和持續(xù)時間。音樂演奏者能根據(jù)五線譜的符號精確地重現(xiàn)聲音。在某種意義上,五線譜是音樂的語言,音樂演奏者是這種語言的解釋器

軟件的例子:
內(nèi)部DSL在軟件方面的例子有很多。PyT是一個用于生成HTML的Python-DSL。PyT關(guān)注性能,并聲稱能與Jinja2的速度相媲美。當(dāng)然,我們不能假定在PyT。中必須使用解釋器模式。然而,PyT是一種內(nèi)部DSL,非常適合使用解釋器模式。
.
Chromium是一個自由開源的瀏覽器軟件,催生出了Google-Chrome瀏覽器。Chromium的Mesa庫Python綁定的一部分使用解釋器模式將C樣板參數(shù)翻譯成 Python對象并執(zhí)行相關(guān)的命令。

應(yīng)用案例:
在我們希望為領(lǐng)域?qū)<液透呒売脩籼峁┮环N簡單語言來解決他們的問題時,可以使用解釋器模式。
.
我們的目標(biāo)是為專家提供恰當(dāng)?shù)木幊坛橄螅蛊渖a(chǎn)力更高;這些專家通常不是程序員。理想情況下,他們使用我們的DSL并不需要了解高級Python知識,當(dāng)然了解一點Python基礎(chǔ)知識會更好,因為我們最終生成的是Python代碼,但不應(yīng)該要求了解Python高級概念。此外,DSL的性 能通常不是一個重要的關(guān)注點。重點是提供一種語言,隱藏宿主語言的獨特性,并提供人類更易讀的語法。誠然,Python已經(jīng)是一門可讀性非常高的語言,與其他編程語言相比,其古怪的語法更少。

代碼實現(xiàn):

from pyparsing import Word, OneOrMore, Optional, Group, Suppress, alphanums

class Gate:
    def __init__(self):
        self.is_open = False

    def __str__(self):
        return 'open' if self.is_open else 'closed'

    def open(self):
        print('opening the gate')
        self.is_open = True

    def close(self):
        print('closing the gate')
        self.is_open = False

class Garage:
    def __init__(self):
        self.is_open = False

    def __str__(self):
        return 'open' if self.is_open else 'closed'

    def open(self):
        print('opening the garage')
        self.is_open = True

    def close(self):
        print('closing the garage')
        self.is_open = False

class Aircondition:
    def __init__(self):
        self.is_on = False

    def __str__(self):
        return 'on' if self.is_on else 'off'

    def turn_on(self):
        print('turning on the aircondition')
        self.is_on = True

    def turn_off(self):
        print('turning off the aircondition')
        self.is_on = False

class Heating:
    def __init__(self):
        self.is_on = False

    def __str__(self):
        return 'on' if self.is_on else 'off'

    def turn_on(self):
        print('turning on the heating')
        self.is_on = True

    def turn_off(self):
        print('turning off the heating')
        self.is_on = False

class Boiler:
    """
    Boiler類。一個鍋爐的默認(rèn)溫度為83攝氏度。類有兩個方法來分別提高和降低當(dāng)前的溫度。
    """
    def __init__(self):
        self.temperature = 83  # in celsius

    def __str__(self):
        return 'boiler temperature: {}'.format(self.temperature)

    def increase_temperature(self, amount):
        print("increasing the boiler's temperature by {} degrees".format(amount))
        self.temperature += amount

    def decrease_temperature(self, amount):
        print("decreasing the boiler's temperature by {} degrees".format(amount))
        self.temperature -= amount

class Fridge:
    def __init__(self):
        self.temperature = 2  # 單位為攝氏度

    def __str__(self):
        return 'fridge temperature: {}'.format(self.temperature)

    def increase_temperature(self, amount):
        print("increasing the fridge's temperature by {} degrees".format(amount))
        self.temperature += amount

    def decrease_temperature(self, amount):
        print("decreasing the fridge's temperature by {} degrees".format(amount))
        self.temperature -= amount

def main():
    # 我們使用巴科斯-諾爾形式(Backus-Naur Form,BNF)表示法來定義語法。
    # 這個語法告訴我們的是一個事件具有 command -> receiver -> arguments 的形式,
    # 并且命令、接收者及參數(shù)也具有相同的形式,
    word = Word(alphanums)
    command = Group(OneOrMore(word))
    token = Suppress("->")
    device = Group(OneOrMore(word))
    argument = Group(OneOrMore(word))
    event = command + token + device + Optional(token + argument)

    # 實例化類對象
    gate = Gate()
    garage = Garage()
    airco = Aircondition()
    heating = Heating()
    boiler = Boiler()
    fridge = Fridge()

    # 事件語法
    tests = ('open -> gate',
             'close -> garage',
             'turn on -> aircondition',
             'turn off -> heating',
             'increase -> boiler temperature -> 5 degrees',
             'decrease -> fridge temperature -> 2 degrees')

    # 開啟操作字典
    open_actions = {'gate': gate.open,
                    'garage': garage.open,
                    'aircondition': airco.turn_on,
                    'heating': heating.turn_on,
                    'boiler temperature': boiler.increase_temperature,
                    'fridge temperature': fridge.increase_temperature
                    }

    # 關(guān)閉操作字典
    close_actions = {'gate': gate.close,
                     'garage': garage.close,
                     'aircondition': airco.turn_off,
                     'heating': heating.turn_off,
                     'boiler temperature': boiler.decrease_temperature,
                     'fridge temperature': fridge.decrease_temperature
                     }

    for t in tests:
        # 判斷解析結(jié)果,2個結(jié)果表示事件 沒有參數(shù)
        if len(event.parseString(t)) == 2:
            cmd, dev = event.parseString(t)
            cmd_str, dev_str = ' '.join(cmd), ' '.join(dev)
            if 'open' in cmd_str or 'turn on' in cmd_str:
                open_actions[dev_str]()
            elif 'close' in cmd_str or 'turn off' in cmd_str:
                close_actions[dev_str]()

        # 判斷解析結(jié)果,3個結(jié)果表示事件 有參數(shù)
        elif len(event.parseString(t)) == 3:
            cmd, dev, arg = event.parseString(t)
            cmd_str, dev_str, arg_str = ' '.join(cmd), ' '.join(dev), ' '.join(arg)
            num_arg = 0
            try:
                num_arg = int(arg_str.split()[0])  # 抽取數(shù)值部分
            except ValueError as err:
                print("expected number but got: '{}'".format(arg_str[0]))
            if 'increase' in cmd_str and num_arg > 0:
                open_actions[dev_str](num_arg)
            elif 'decrease' in cmd_str and num_arg > 0:
                close_actions[dev_str](num_arg)

if __name__ == '__main__':
    main()

解析器模式總結(jié):
解釋器模式用于為高級用戶和領(lǐng)域?qū)<姨峁┮粋€類編程的框架,但沒有暴露出編程語言那樣的復(fù)雜性。這是通過實現(xiàn)一個DSL來達(dá)到目的的
.
DSL是一種針對特定領(lǐng)域、表達(dá)能力有限的計算機語言。DSL有兩類,分別是內(nèi)部DSL和外部DSL。內(nèi)部DSL構(gòu)建在一種宿主編程語言之上,依 賴宿主編程語言,外部DSL則是從頭實現(xiàn),不依賴某種已有的編程語言。解釋器模式僅與內(nèi)部DSL相關(guān)。
.
樂譜是一個非軟件DSL的例子。音樂演奏者像一個解釋器那樣,使用樂譜演奏出音樂。從軟件的視角來看,許多Python模板引擎都使用了內(nèi)部DSL。PyT是一個高性能的生成(X)HTML的Python-DSL。我們也看到Chromium的Mesa庫是如何使用解釋器模式將圖形相關(guān)的C代碼翻譯成 Python可執(zhí)行對象的。

④.觀察者模式:

簡介:
有時,我們希望在一個對象的狀態(tài)改變時更新另外一組對象。在MVC模式中有這樣一個非常常見的例子,假設(shè)在兩個視圖(例如,一個餅圖和一個電子表格)中使用同一個模型的數(shù)據(jù),無論何時更改了模型,都需要更新兩個視圖。這就是觀察者設(shè)計模式要處理的問題。
.
觀察者模式描述單個對象(發(fā)布者,又稱為主持者或可觀察者)與一個或多個對象(訂閱者,又稱為觀察者)之間的發(fā)布—訂閱關(guān)系。在MVC例子中,發(fā)布者是模型,訂閱者是視圖。
.
觀察者模式背后的思想等同于MVC和關(guān)注點分離原則背后的思想,即降低發(fā)布者與訂閱者之間的耦合度,從而易于在運行時添加或刪除訂閱者。此外,發(fā)布者不關(guān)心它的訂閱者是誰。它只是將通知發(fā)送給所有訂閱者。

現(xiàn)實生活的例子:
現(xiàn)實中,拍賣會類似于觀察者模式。每個拍賣出價人都有一些拍牌,在他們想出價時就可以舉起來。不論出價人在何時舉起一塊拍牌,拍賣師都會像主持者那樣更新報價,并將新的價格廣 播給所有出價人(訂閱者)。

軟件的例子:
大體上,所有利用MVC模式的系統(tǒng)都是基于事件的。
.
django-observer源代碼包是一個第三方Django包,可用于注冊回調(diào)函數(shù),之后在某些Django模型字段發(fā)生變化時執(zhí)行。它支持許多不同類型的模型字段 (CharField、IntegerField等)。
.
RabbitMQ可用于為應(yīng)用添加異步消息支持,支持多種消息協(xié)議(比如,HTTP和AMQP),可在Python應(yīng)用中用于實現(xiàn)發(fā)布—訂閱模式,也就是觀察者設(shè)計模式。

應(yīng)用案例:
當(dāng)我們希望在一個對象(主持者/發(fā)布者/可觀察者)發(fā)生變化時通知或更新另一個或多個對象的時候,通常會使用觀察者模式。觀察者的數(shù)量以及誰是觀察者可能會有所不同,也可以(在運行時)動態(tài)地改變。
.
同樣的概念也存在于社交網(wǎng)絡(luò)。如果你使用社交網(wǎng)絡(luò)服務(wù)關(guān)聯(lián)了另一個人,在關(guān)聯(lián)的人更新某些內(nèi)容時,你能收到相關(guān)通知,不論這個關(guān)聯(lián)的人是你關(guān)注的一個Twitter用戶,F(xiàn)acebook上的 一個真實朋友,還是LinkdIn上的一位同事。
.
事件驅(qū)動系統(tǒng)是另一個可以使用(通常也會使用)觀察者模式的例子。在這種系統(tǒng)中,監(jiān)聽者被用于監(jiān)聽特定事件。監(jiān)聽者正在監(jiān)聽的事件被創(chuàng)建出來時,就會觸發(fā)它們。這個事件可以是鍵入(鍵盤的)某個特定鍵、移動鼠標(biāo)或者其他。事件扮演發(fā)布者的角色,監(jiān)聽者則扮演觀察者的角色。在這里,關(guān)鍵點是單個事件(發(fā)布者)可以關(guān)聯(lián)多個監(jiān)聽者(觀察者)

代碼實現(xiàn):

class Publisher:
    """
    Publisher類。觀察者們保存在列表observers中。
    add()方法注冊一個新的觀察者,或者在該觀察者已存在時引發(fā)一個錯誤。
    remove()方法注銷一個已有觀察者,或者在該觀察者尚未存在時引發(fā)一個錯誤。
    notify()方法則在變化發(fā)生時通知所有觀察者
    """
    def __init__(self):
        self.observers = []

    def add(self, observer):
        if observer not in self.observers:
            self.observers.append(observer)
        else:
            print('Failed to add: {}'.format(observer))

    def remove(self, observer):
        try:
            self.observers.remove(observer)
        except ValueError:
            print('Failed to remove: {}'.format(observer))

    def notify(self):
        [o.notify(self) for o in self.observers]

class DefaultFormatter(Publisher):
    """
    __init__()做的第一件事情就是調(diào)用基類的__init__() 方法,因為這在Python中沒法自動完成。
    DefaultFormatter實例有自己的名字,這樣便于我們跟蹤其狀態(tài)。
    """
    def __init__(self, name):
        Publisher.__init__(self)
        self.name = name
        self._data = 0

    def __str__(self):
        """
        __str__()方法返回關(guān)于發(fā)布者名稱和_data值的信息。
        type(self).__name是一種獲取類名的方便技巧,避免硬編碼類名。這降低了代碼的可讀性,卻提高了可維護(hù)性
        """
        return "{}: '{}' has data = {}".format(type(self).__name__, self.name, self._data)

    @property
    def data(self):
        """
        對于_data變量,我們使用了property裝飾器來將data方法變成屬性來得到私有屬性_data的值。
        """
        return self._data

    @data.setter
    def data(self, new_value):
        """
        data()更有意思。它使用了@setter修飾器,該修飾器會在每次使用賦值操作符(=)為_data變量賦新值時被調(diào)用。
        該方法也會嘗試把新值強制類型轉(zhuǎn)換為一個整數(shù),并在類型轉(zhuǎn)換失敗時處理異常
        """
        try:
            self._data = int(new_value)
        except ValueError as e:
            print('Error: {}'.format(e))
        else:
            self.notify()

class HexFormatter:
    """觀察者類1(十六進(jìn)制)"""
    def notify(self, publisher):
        print("{}: '{}' has now hex data = {}".format(type(self).__name__, publisher.name, hex(publisher.data)))

class BinaryFormatter:
    """觀察者類2(二進(jìn)制)"""
    def notify(self, publisher):
        print("{}: '{}' has now bin data = {}".format(type(self).__name__, publisher.name, bin(publisher.data)))

def main():
    """
    main()函數(shù)一開始創(chuàng)建一個名為test1的Default- Formatter實例,并在之后關(guān)聯(lián)了兩個可用的觀察者。
    也使用了異常處理來確保在用戶輸入問題數(shù)據(jù)時應(yīng)用不會崩潰。此外,
    諸如兩次添加相同的觀察者或刪除尚不存在的觀察者之類的事情也不應(yīng)該導(dǎo)致崩潰。
    """
    df = DefaultFormatter('test1')
    print(df)

    # 添加第一個觀察者
    print()
    hf = HexFormatter()
    df.add(hf)
    df.data = 3
    print(df)

    #參加第二個觀察者
    print()
    bf = BinaryFormatter()
    df.add(bf)
    df.data = 21
    print(df)

    # 刪除一個觀察者
    print()
    df.remove(hf)
    df.data = 40
    print(df)

    # 刪除另一個觀察者后再添加一個觀察者
    print()
    df.remove(hf)
    df.add(bf)

    # 異常捕獲
    df.data = 'hello'
    print(df)

    print()
    df.data = 15.8
    print(df)

if __name__ == '__main__':
    main()

觀察者模式總結(jié):
若希望在一個對象的狀態(tài)變化時能夠通知或提醒所有相關(guān)者(一個對象或一組對象),則可以使用觀察者模式。觀察者模式的一個重要特性是,在運行時,訂閱者/觀察者的數(shù)量以及觀察者是誰可能會變化,也可以改變。

⑤.狀態(tài)模式:

簡介:
面向?qū)ο缶幊讨τ谠趯ο蠼换r改變它們的狀態(tài)。在很多問題中,有限狀態(tài)機(通常名為狀態(tài)機)是一個非常方便的狀態(tài)轉(zhuǎn)換建模(并在必要時以數(shù)學(xué)方式形式化)工具。
.
狀態(tài)機是一個抽象機器,有兩個關(guān)鍵部分,狀態(tài)和轉(zhuǎn)換。狀態(tài)是指系統(tǒng)的當(dāng)前(激活)狀況。。轉(zhuǎn)換是指從一個狀態(tài)切換到另一個狀態(tài),因某個事件或條件的觸發(fā)而開始,通常,在一次轉(zhuǎn)換發(fā)生之前或之后會執(zhí)行一個或一組動作。
.
進(jìn)程一開始由用戶創(chuàng)建好,就進(jìn)入“已創(chuàng)建或新建”狀態(tài)。這個狀態(tài)只能切換到“等待”狀態(tài),這個狀態(tài)轉(zhuǎn)換發(fā)生在調(diào)度器將進(jìn)程加載進(jìn)內(nèi)存并添加到“等待/預(yù)備執(zhí)行”的進(jìn)程隊列之時。一個“等待”進(jìn)程有兩個可能的狀態(tài)轉(zhuǎn)換:可被選擇而執(zhí)行(切換到“運行”狀態(tài)),或被更高優(yōu)先級的進(jìn)程所替代(切換到“換 出并等待”狀態(tài))。

現(xiàn)實生活的例子:
自動售貨機有不同的狀態(tài),并根據(jù)我們放入的錢幣數(shù)量作出不同反應(yīng)。根據(jù)我們的選 擇和放入的錢幣,機器會執(zhí)行以下操作。
1. 拒絕我們的選擇,因為請求的貨物已售罄。
2. 拒絕我們的選擇,因為放入的錢幣不足。
3. 遞送貨物,且不找零,因為放入的錢幣恰好足夠。
4. 遞送貨物,并找零。

軟件的例子:
使用狀態(tài)模式本質(zhì)上相當(dāng)于實現(xiàn)一個狀態(tài)機來解決特定領(lǐng)域的一個軟件問題。django-fsm程序包是一個第三方程序包,用于Django框架中簡化狀態(tài)機的實現(xiàn)和使用。
.
另一個值得一提的項目是狀態(tài)機編譯器(State Machine Compiler,SMC)。使用SMC,你可以使用一種簡單的領(lǐng)域特定語言在文本文件中描述你的狀態(tài)機,SMC會自動生成狀態(tài)機的代碼。該項目聲稱這種DSL非常簡單,寫起來就像一對一地翻譯一個狀態(tài)圖。

應(yīng)用案例:
1. 狀態(tài)模式適用于許多問題。所有可以使用狀態(tài)機解決的問題都是不錯的狀態(tài)模式應(yīng)用案例。 我們已經(jīng)見過的一個例子是操作系統(tǒng)/嵌入式系統(tǒng)的進(jìn)程模型。
.
2. 編程語言的編譯器實現(xiàn)是另一個好例子。詞法和句法分析可使用狀態(tài)來構(gòu)建抽象語法樹。
.
3. 事件驅(qū)動系統(tǒng)也是另一個例子。在一個事件驅(qū)動系統(tǒng)中,從一個狀態(tài)轉(zhuǎn)換到另一個狀態(tài)會觸發(fā)一個事件或消息。許多計算機游戲都使用 這一技術(shù)。例如,怪獸會在主人公接近時從防御狀態(tài) 轉(zhuǎn)換到攻擊狀態(tài)

代碼實現(xiàn):

from state_machine import State, Event, acts_as_state_machine, after, before, InvalidStateTransition

# 每個創(chuàng)建好的進(jìn)程都有自己的狀態(tài)機。使用state_machine模塊
# 創(chuàng)建狀態(tài)機的第一個步驟是使用@acts_as_state_machine修飾器。
@acts_as_state_machine
class Process:
    """
    定義狀態(tài)機的狀態(tài)。這是我們在狀態(tài)圖中看到的節(jié)點的映射。
    唯一的區(qū)別是應(yīng)指定 狀態(tài)機的初始狀態(tài)。這可通過設(shè)置inital=True來指定。
    """
    created = State(initial=True)
    waiting = State()
    running = State()
    terminated = State()
    blocked = State()
    swapped_out_waiting = State()
    swapped_out_blocked = State()

    # 接著定義狀態(tài)轉(zhuǎn)換。在state_machine模塊中,一個狀態(tài)轉(zhuǎn)換就是一個Event。
    # 我們使用參數(shù)from_states和to_state來定義一個可能的轉(zhuǎn)換。from_states可以是單個狀態(tài)或一組狀態(tài)(元組)。
    wait = Event(from_states=(created, running, blocked, swapped_out_waiting), to_state=waiting)
    run = Event(from_states=waiting, to_state=running)
    terminate = Event(from_states=running, to_state=terminated)
    block = Event(from_states=(running, swapped_out_blocked), to_state=blocked)
    swap_wait = Event(from_states=waiting, to_state=swapped_out_waiting)
    swap_block = Event(from_states=blocked, to_state=swapped_out_blocked)

    # 每個進(jìn)程都有一個名稱。正式的應(yīng)用場景中,一個進(jìn)程需要多得多的信息才能發(fā)揮其作用(例 如,ID、優(yōu)先級和狀態(tài)等),
    # 但為了專注于模式本身,我們進(jìn)行一些簡化,只初始化對應(yīng)的name
    def __init__(self, name):
        self.name = name

    # 在發(fā)生狀態(tài)轉(zhuǎn)換時,如果什么影響都沒有,那轉(zhuǎn)換就沒什么用了。state_machine模塊提 供@before和@after修飾器,
    # 用于在狀態(tài)轉(zhuǎn)換之前或之后執(zhí)行動作。為了達(dá)到示例的目的,這 里的動作限于輸出進(jìn)程狀態(tài)轉(zhuǎn)換的信息。
    @after('wait')
    def wait_info(self):
        print('{} entered waiting mode'.format(self.name))

    @after('run')
    def run_info(self):
        print('{} is running'.format(self.name))

    @after('terminate')
    def terminate_info(self):
        print('{} terminated'.format(self.name))

    @after('block')
    def block_info(self):
        print('{} is blocked'.format(self.name))

    @after('swap_wait')
    def swap_wait_info(self):
        print('{} is swapped out and waiting'.format(self.name))

    @after('swap_block')
    def swap_block_info(self):
        print('{} is swapped out and blocked'.format(self.name))

def transition(process, event, event_name):
    """
    transition()函數(shù)接受三個參數(shù):process、event和event_name。在嘗試執(zhí)行event時,如果發(fā)生錯誤,則會輸出事件的名稱。
    :param process:是一個Process類實例,
    :param event: event是一個Event類(wait、run和terminate等)實例,
    :param event_name: 而event_name 是事件的名稱。
    """
    try:
        event()
    except InvalidStateTransition as err:
        print('Error: transition of {} from {} to {} failed'.format(process.name, process.current_state, event_name))

def state_info(process):
    """
    state_info()函數(shù)展示進(jìn)程當(dāng)前(激活)狀態(tài)的一些基本信息。
    """
    print('state of {}: {}'.format(process.name, process.current_state))

def main():
    # 在main()函數(shù)的開始,我們定義了一些字符串常量,作為event_name參數(shù)值傳遞。
    RUNNING = 'running'
    WAITING = 'waiting'
    BLOCKED = 'blocked'
    TERMINATED = 'terminated'

    # 我們創(chuàng)建兩個Process實例,并輸出它們的初始狀態(tài)信息。
    p1, p2 = Process('process1'), Process('process2')
    [state_info(p) for p in (p1, p2)]

    # 其余部分將嘗試不同的狀態(tài)轉(zhuǎn)換。回憶一下本章之前提到的狀態(tài)圖。允許的狀態(tài)轉(zhuǎn)換應(yīng)與狀態(tài)圖一致。
    # 例如,從狀態(tài)“運行”轉(zhuǎn)換到狀態(tài)“阻塞”是可能的,但從狀態(tài)“阻塞”轉(zhuǎn)換 到狀態(tài)“運行”則是不可能的。
    print()
    transition(p1, p1.wait, WAITING)
    transition(p2, p2.terminate, TERMINATED)
    [state_info(p) for p in (p1, p2)]

    print()
    transition(p1, p1.run, RUNNING)
    transition(p2, p2.wait, WAITING)
    [state_info(p) for p in (p1, p2)]

    print()
    transition(p2, p2.run, RUNNING)
    [state_info(p) for p in (p1, p2)]

    print()
    [transition(p, p.block, BLOCKED) for p in (p1, p2)]
    [state_info(p) for p in (p1, p2)]

    print()
    [transition(p, p.terminate, TERMINATED) for p in (p1, p2)]
    [state_info(p) for p in (p1, p2)]

if __name__ == '__main__':
    main()

狀態(tài)模式總結(jié):
狀態(tài)模式是一個或多個有限狀態(tài)機(簡稱狀態(tài)機)的實 現(xiàn),用于解決一個特定的軟件工程問題。
.
狀態(tài)機是一個抽象機器,具有兩個主要部分:狀態(tài)和轉(zhuǎn)換。狀態(tài)是指一個系統(tǒng)的當(dāng)前狀況。一個狀態(tài)機在任意時間點只會有一個激活狀態(tài)。轉(zhuǎn)換是指從當(dāng)前狀態(tài)到一個新狀態(tài)的切換。在一個轉(zhuǎn)換發(fā)生之前或之后通常會執(zhí)行一個或多個動作。狀態(tài)機可以使用狀態(tài)圖進(jìn)行視覺上的展現(xiàn)。
.
狀態(tài)機用于解決許多計算機問題和非計算機問題,其中包括交通燈、停車計時器、硬件設(shè)計和編程語言解析等。我們也看到零食自動販賣機是如何與狀態(tài)機的工作方式相關(guān)聯(lián)的。
.
許多現(xiàn)代軟件提供庫/模塊來簡化狀態(tài)機的實現(xiàn)與使用。Django提供第三方包django-fsm,Python也有許多大家貢獻(xiàn)的模塊。實際上,在14.4節(jié)就使用了其中的一個模塊(state_machine)。 狀態(tài)機編譯器是另一個有前景的項目,提供許多編程語言的綁定(包括Python)。
.
狀態(tài)模式是一個或多個有限狀態(tài)機(簡稱狀態(tài)機)的實 現(xiàn),用于解決一個特定的軟件工程問題。
.
狀態(tài)機是一個抽象機器,具有兩個主要部分:狀態(tài)和轉(zhuǎn)換。狀態(tài)是指一個系統(tǒng)的當(dāng)前狀況。一個狀態(tài)機在任意時間點只會有一個激活狀態(tài)。轉(zhuǎn)換是指從當(dāng)前狀態(tài)到一個新狀態(tài)的切換。在一 個轉(zhuǎn)換發(fā)生之前或之后通常會執(zhí)行一個或多個動作。狀態(tài)機可以使用狀態(tài)圖進(jìn)行視覺上的展現(xiàn)。
.
狀態(tài)機用于解決許多計算機問題和非計算機問題,其中包括交通燈、停車計時器、硬件設(shè)計和編程語言解析等。我們也看到零食自動販賣機是如何與狀態(tài)機的工作方式相關(guān)聯(lián)的。

⑥.策略模式:

簡介:
大多數(shù)問題都可以使用多種方法來解決。以排序問題為例,對于以一定次序把元素放入一個列表,排序算法有很多。通常來說,沒有公認(rèn)適合所有場景的算法。 一些不同的評判標(biāo)準(zhǔn)能幫助我們?yōu)椴煌膱鼍斑x擇不同的排序算法,其中應(yīng)該考慮的有以下幾個:
.
1. 需要排序的元素數(shù)量:這被稱為輸入大小。當(dāng)輸入較少時,幾乎所有排序算法的表現(xiàn)都很好,但對于大量輸入,只有部分算法具有不錯 的性能。
2. 算法的最佳/平均/最差時間復(fù)雜度:時間復(fù)雜度是算法運行完成所花費的(大致)時間長短,不考慮系數(shù)和低階項。這是選擇算法的常 見標(biāo)準(zhǔn),但這個標(biāo)準(zhǔn)并不總是那么充分。
3. 算法的空間復(fù)雜度:空間復(fù)雜度是充分地運行一個算法所需要的(大致)物理內(nèi)存量。在我們處理大數(shù)據(jù)或在嵌入式系統(tǒng)(通常內(nèi)存有 限)中工作時,這個因素非常重要。
4. 算法的穩(wěn)定性:在執(zhí)行一個排序算法之后,如果能保持相等值元素原來的先后相對次序, 則認(rèn)為它是穩(wěn)定的。
5. 算法的代碼實現(xiàn)復(fù)雜度:如果兩個算法具有相同的時間或空間復(fù)雜度,并且都是穩(wěn)定的,那么知道哪個算法更易于編碼實現(xiàn)和維護(hù)也是 很重要的。
.
策略模式的目的:
可能還有更多的評判標(biāo)準(zhǔn)值得考慮,但重要的是,我們真的只能使用單個排序算法來應(yīng)對所有情況嗎?答案當(dāng)然不是。一個更好的方案是把所有排序算法納為己用,然后使用上面提到的標(biāo)準(zhǔn)針對當(dāng)前情況選擇好的算法。這就是策略模式的目的。
.
策略模式(Strategy pattern)
鼓勵使用多種算法來解決一個問題,其殺手級特性是能夠在運行時透明地切換算法(客戶端代碼對變化無感知)。因此,如果你有兩種算法,并且知道其中一種對少量輸入效果更好,另一種對大量輸入效果更好,則可以使用策略模式在運行時基于輸入數(shù)據(jù)決定使用哪種算法。

現(xiàn)實生活的例子:
去機場趕飛機是現(xiàn)實中使用策略模式的一個恰當(dāng)例子。
.
1. 如果想省錢,并且早點出發(fā),那么可以坐公交車/地鐵。
2. 如果不介意支付停車費,并且有自己的汽車,那么可以開車去。
3. 如果沒有自己的車,又比較急,則可以打車。
這是費用、時間、便利性等因素之間的一個折中權(quán)衡。

軟件的例子:
Python 的 sorted() 和 list.sort()函數(shù)是策略模式的例子。兩個函數(shù)都接受一個命名參數(shù)key,這個參數(shù)本質(zhì)上是實現(xiàn)了一個排序策略的函數(shù)的名稱.

應(yīng)用案例:
策略模式是一種非常通用的設(shè)計模式,可應(yīng)用的場景很多。一般來說,不論何時希望動態(tài)、透明地應(yīng)用不同算法,策略模式都是可行之路。這里所說不同算法的意思是,目的相同但實現(xiàn)方案不同的一類算法。這意味著算法結(jié)果應(yīng)該是完全一致的,但每種實現(xiàn)都有不同的性能和代碼復(fù)雜性(舉例來說,對比一下順序查找和二分查找)。
.
策略模式并不限于排序問題,也可用于創(chuàng)建各種不同的資源過濾器(身份驗證、日志記錄、數(shù)據(jù)壓縮和加密等)
.
策略模式的另一個應(yīng)用是創(chuàng)建不同的樣式表現(xiàn),為了實現(xiàn)可移植性(例如,不同平臺之間斷行的不同)或動態(tài)地改變數(shù)據(jù)的表現(xiàn)。
.
另一個值得一提的應(yīng)用是模擬;例如模擬機器人,一些機器人比另一些更有攻擊性,一些機器人速度更快,等等。機器人行為中的所有不同之處都可以使用不同的策略來建模。

代碼實現(xiàn):

以下代碼展示了如何用以下方式使用兩種不同的策略對編程語 言進(jìn)行排序。 

import pprint
from collections import namedtuple
from operator import attrgetter

# pprint模塊用于美化輸出一個數(shù)據(jù)結(jié)構(gòu), attrgetter用于通過屬性名訪問class或namedtuple的屬性。
# 也可以使用一個lambda函數(shù)來 替代使用attrgetter,但我覺得attrgetter的可讀性更高。
if __name__ == '__main__':
    ProgrammingLang = namedtuple('ProgrammingLang', ("name", "ranking"))

    stats = (('Ruby', 14), ('Javascript', 8), ('Python', 7), ('Scala', 31), ('Swift', 18), ('Lisp', 23))

    lang_stats = [ProgrammingLang(n, r) for n, r in stats]
    pp = pprint.PrettyPrinter(indent=5)  # indent 每個嵌套層要縮進(jìn)的空格數(shù)量
    pp.pprint(sorted(lang_stats, key=attrgetter('name')))
    print()
    pp.pprint(sorted(lang_stats, key=attrgetter('ranking')))
策略模式的實現(xiàn)代碼

import time

SLOW = 3  # 單位為秒
LIMIT = 5  # 字符數(shù)
WARNING = 'too bad, you picked the slow algorithm :('

def pairs(seq):
    """
    它會返回所有相鄰字符對的一個序列seq
    """
    n = len(seq)
    for i in range(n):
        yield seq[i], seq[(i + 1) % n]

def allUniqueSort(s):
    """
    它接受一個字符串參數(shù)s,如果該字符串中所有字符 都是唯一的,則返回True;否則,返回False。
    為演示策略模式,我們進(jìn)行一些簡化,假設(shè)這個算法的伸縮性不好,對于不超過5個字符的字符串才能工作良好。
    對于更長的字符串,通過插入 一條sleep語句來模擬速度減緩。
    """
    if len(s) > LIMIT:
        print(WARNING)
        time.sleep(SLOW)
    srtStr = sorted(s)
    for (c1, c2) in pairs(srtStr):
        if c1 == c2:
            return False
    return True

def allUniqueSet(s):
    """
    我們使用一個集合來實現(xiàn)算法。 如果正在檢測的字符已經(jīng)被插入到集合中,則意味著字符串中并非所有字符都是唯一的
    """
    if len(s) < LIMIT:
        print(WARNING)
        time.sleep(SLOW)
    return True if len(set(s)) == len(s) else False

def allUnique(s, strategy):
    return strategy(s)

def main():
    """
    用main()函數(shù)可以執(zhí)行以下操作。
    1. 輸入待檢測字符唯一性的單詞
    2. 選擇要使用的策略
    該函數(shù)還進(jìn)行了一些基本的錯誤處理,并讓用戶能夠正常退出程序
    """
    while True:
        word = None
        while not word:
            word = input('Insert word (type quit to exit)> ')
            if word == 'quit':
                print('bye')
                return

            strategy_picked = None
            strategies = {'1': allUniqueSet, '2': allUniqueSort}
            while strategy_picked not in strategies.keys():
                strategy_picked = input('Choose strategy: [1] Use a set, [2] Sort and pair> ')
            try:
                strategy = strategies[strategy_picked]
                print('allUnique({}): {}'.format(word,
                                                 allUnique(word, strategy)))
            except KeyError as err:
                print('Incorrect option: {}'.format(strategy_picked))
            print()

if __name__ == '__main__':
    main()

策略模式總結(jié):
策略模式通常用在我們希望對同一個問題透明地使用多種方案時。如果并不存在針對所有輸入數(shù)據(jù)和所有情況的完美算法,那么我們可以使用策略模式, 動態(tài)地決定在每種情況下應(yīng)使用哪種算法?,F(xiàn)實中,在我們想趕去機場乘飛機時會使用策略模式。
.
Python使用策略模式讓客戶端代碼決定如何對一個數(shù)據(jù)結(jié)構(gòu)中的元素進(jìn)行排序。我們看到了一個例子,基于TIOBE指數(shù)排行榜對編程語言進(jìn)行排序。
.
策略設(shè)計模式的使用并不限于排序領(lǐng)域。加密、壓縮、日志記錄及其他資源處理的領(lǐng)域都可以使用策略模式來提供不同的數(shù)據(jù)處理方式。可移植性是策略模式的另一個用武之地。模擬也是 另一個策略模式適用的領(lǐng)域。
.
通過實現(xiàn)兩種不同算法來檢測一個單詞中所有字符的唯一性,我們學(xué)習(xí)了Python如何因其具 有一等函數(shù)而簡化了策略模式的實現(xiàn)。

⑦.模板模式:

簡介:
編寫優(yōu)秀代碼的一個要素是避免冗余。在面向?qū)ο缶幊讨?,方法和函?shù)是我們用來避免編寫冗余代碼的重要工具。
.
模板設(shè)計模式(Template design pattern)。這個模式關(guān)注的是消除代碼冗余,其思想是我們應(yīng)該無需改變算法結(jié)構(gòu)就能重新定義一個算法的某些部分。為了避免重復(fù)而進(jìn)行必要的重構(gòu)。

現(xiàn)實生活的例子:
工人的日程,特別是對于同一個公司的工人而言,非常接近于模板設(shè)計模式。所有工人都遵從或多或少相同的例行流程,但例行流程的某些特定部分區(qū)別又很大。

軟件的例子:
Python在cmd模塊中使用了模板模式,該模塊用于構(gòu)建面向行的命令解釋器。具體而言, cmd.Cmd.cmdloop()實現(xiàn)了一個算法,持續(xù)地讀取輸入命令并將命令分發(fā)到動作方法。每次循環(huán)之前、之后做的事情以及命令解析部分始終是相同的。這也稱為一個算法的不變部分。變化的 是實際的動作方法(易變的部分)。
.
Python的asyncore模塊也使用了模板模式,該模塊用于實現(xiàn)異步套接字服務(wù)客戶端、服務(wù)器。其中諸如asyncore.dispatcher.handle_connect_event和asyncore.dispatcher. handle_write_event()之類的方法僅包含通用代碼。要執(zhí)行特定于套接字的代碼,這兩個方 法會執(zhí)行handle_connect()方法。注意,執(zhí)行的是一個特定于套接字的handle_connect(),不是asyncore.dispatcher.handle_connect()。后者僅包含一條警告。可以使用inspect模塊來查看。

應(yīng)用案例:
模板設(shè)計模式旨在消除代碼重復(fù)。如果我們發(fā)現(xiàn)結(jié)構(gòu)相近的(多個)算法中有重復(fù)代碼,則可以把算法的不變(通用)部分留在一個模板方法/函數(shù)中,把易變(不同)的部分移到動作/鉤 子方法/函數(shù)中。
.
頁碼標(biāo)注是一個不錯的模板模式應(yīng)用案例。一個頁碼標(biāo)注算法可以分為一個抽象(不變的)部分和一個具體(易變的)部分。不變的部分關(guān)注的是大行號/頁號這部分內(nèi)容。易變的部分 則包含用于顯示某個已分頁特定頁面的頁眉和頁腳的功能。
.
所有應(yīng)用框架都利用了某種形式的模板模式。在使用框架來創(chuàng)建圖形化應(yīng)用時,通常是繼承自一個類,并實現(xiàn)自定義行為。然而,在執(zhí)行自定義行為之前,通常會調(diào)用一個模板方法,該方法實現(xiàn)了應(yīng)用中一定相同的部分,比如繪制屏幕、處理事件循環(huán)、調(diào)整窗口大小并居中等等

代碼實現(xiàn):

from cowpy import cow

"""
我們將實現(xiàn)一個橫幅生成器。想法很簡單,將一段文本發(fā)送給一個函數(shù),該函數(shù)要 生成一個包含該文本的橫幅。
橫幅有多種風(fēng)格,比如點或虛線圍繞文本。橫幅生成器有一個默認(rèn)風(fēng)格,但應(yīng)該能夠使用我們自己提供的風(fēng)格。 
"""
def dots_style(msg):
    """
    dots_style()簡單地將msg首字母大寫,并在其之前和之后輸出10個點
    """
    msg = msg.capitalize()
    msg = '.' * 10 + msg + '.' * 10
    return msg

def admire_style(msg):
    msg = msg.upper()
    return '!'.join(msg)

def generate_banner(msg, style=dots_style):
    """
    數(shù)generate_banner()是我們的模板函數(shù)。它接受一個輸入?yún)?shù)(msg,希望橫幅包含的文本)和一個可選參數(shù)(style,希望使用的風(fēng)格)。
    默認(rèn)風(fēng)格是dots_style,我們馬上就能 看到。generate_banner()以一個簡單的頭部和尾部來包裝帶樣式的文本。
    實際上,這個頭部和尾部可以復(fù)雜得多,但在這里調(diào)用可以生成頭部和尾部的函數(shù)來替代僅僅輸出簡單字符串也無不可。
    """
    print('-- start of banner --')
    print(style(msg))
    print('-- end of banner --\n\n')

def cow_style(msg):
    """
    cow_style()風(fēng)格使用cowpy模塊生成隨機ASCII碼藝 術(shù)字符,夸張地表現(xiàn)文本
    """
    msg1 = cow.milk_random_cow(msg)
    msg2 = cow.milk_random_cow(msg1)
    return msg2

def main():
    """
    main()函數(shù)向橫幅發(fā)送文本“關(guān)關(guān)之舟,在河之洲!”,并使用所有可用風(fēng)格將橫幅輸出到標(biāo)準(zhǔn)輸出。
    """
    msg = '關(guān)關(guān)之舟,在河之洲!'
    [generate_banner(msg, style) for style in (dots_style, admire_style, cow_style)]

if __name__ == '__main__':
    main()

模板模式總結(jié):
我們學(xué)習(xí)了模板設(shè)計模式。在實現(xiàn)結(jié)構(gòu)相近的算法時,可以使用模板模式來消除冗 余代碼。具體實現(xiàn)方式是使用動作/鉤子方法/函數(shù)來完成代碼重復(fù)的消除,它們是Python中的一 等公民。我們學(xué)習(xí)了一個實際的例子,即使用模板模式來重構(gòu)BFS和DFS算法的代碼。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容