1. 程式人生 > 實用技巧 >python設計模式之觀察者模式

python設計模式之觀察者模式

python設計模式之觀察者模式

有時,我們希望在一個物件的狀態改變時更新另外一組物件。在MVC模式中有這樣一個非
常常見的例子,假設在兩個檢視(例如,一個餅圖和一個電子表格)中使用同一個模型的資料,
無論何時更改了模型,都需要更新兩個檢視。這就是觀察者設計模式要處理的問題。

觀察者模式描述單個物件(釋出者,又稱為主持者或可觀察者)與一個或多個物件(訂閱者,
又稱為觀察者)之間的釋出—訂閱關係。在MVC例子中,釋出者是模型,訂閱者是檢視。然而,
MVC並非是僅有的釋出—訂閱例子。資訊聚合訂閱(比如, RSS或Atom)是另一種例子。許多讀
者通常會使用一個資訊聚合閱讀器訂閱資訊流,每當增加一條新資訊時,他們就能自動地獲取到
更新。

觀察者模式背後的思想等同於MVC和關注點分離原則背後的思想,即降低釋出者與訂閱者
之間的耦合度,從而易於在執行時新增/刪除訂閱者。此外,釋出者不關心它的訂閱者是誰。它
只是將通知傳送給所有訂閱者。

1. 現實生活的例子

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

2. 軟體的例子

django-observer原始碼包是一個第三方Django包,可用於註冊回撥函式,之後在某些Django模型欄位發生變化時執行。它支援許多不同型別的模型欄位( CharField、 IntegerField等)。

RabbitMQ可用於為應用新增非同步訊息支援,支援多種訊息協議(比如, HTTP和AMQP),可
在Python應用中用於實現釋出—訂閱模式,也就是觀察者設計模式。

3. 應用案例

當我們希望在一個物件(主持者/釋出者/可觀察者)發生變化時通知/更新另一個或多個物件的時候,通常會使用觀察者模式。觀察者的數量以及誰是觀察者可能會有所不同,也可以(在執行時)動態地改變。

可以想到許多觀察者模式在其中有用武之地的案例。本章開頭已提過這樣的一個案例,就是資訊聚合。無論格式為RSS、 Atom還是其他,思想都一樣:你追隨某個資訊源,當它每次更新時,你都會收到關於更新的一個通知。

同樣的概念也存在於社交網路。如果你使用社交網路服務關聯了另一個人,在關聯的人更新某些內容時,你能收到相關通知,不論這個關聯的人是你關注的一個Twitter使用者, Facebook上的一個真實朋友,還是LinkdIn上的一位同事。

事件驅動系統是另一個可以使用(通常也會使用)觀察者模式的例子。在這種系統中,監聽者被用於監聽特定事件。監聽者正在監聽的事件被創建出來時,就會觸發它們。這個事件可以是鍵入(鍵盤的)某個特定鍵、移動滑鼠或者其他。事件扮演釋出者的角色,監聽者則扮演觀察者的角色。在這裡,關鍵點是單個事件(釋出者)可以關聯多個監聽者(觀察者)。

4. 實現

我們將實現一個數據格式化程式,預設格式化程式是以十進位制格式展示一個數值,然而,我們可以新增/註冊更多的格式化程式。這個例子中將新增一個十六進位制格式化程式和一個二進位制格式化程式。每次更新預設格式化程式的值時,已註冊的格式化程式就會收到通知,並採取行動。在這裡,行動就是以相關的格式展示新的值。

在一些模式中,繼承能體現自身價值,觀察者模式是這些模式中的一個。我們可以實現一個基類Publisher,包括新增、刪除及通知觀察者這些公用功能。 DefaultFormatter類繼承自Publisher,並新增格式化程式特定的功能。我們可以按需動態地新增刪除觀察者。下面的類圖展示了一個使用兩個觀察者( HexFormatter和BinaryFormatter)的示例。注意,因為類圖是靜態的,所以無法展示系統的整個生命週期,只能展示某個特定時間點的系統狀態。

從Publisher類開始說起。觀察者們儲存在列表observers中。 add()方法註冊一個新的觀察者,或者在該觀察者已存在時引發一個錯誤。 remove()方法登出一個已有觀察者,或者在該觀察者尚未存在時引發一個錯誤。最後, notify()方法則在變化發生時通知所有觀察者。

class Publisher:
    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]

接著是DefaultFormatter類。 init()做的第一件事情就是呼叫基類的__init__()方法,因為這在Python中沒法自動完成。 DefaultFormatter例項有自己的名字,這樣便於我們跟蹤其狀態。對於data變數,我們使用了名稱改編來宣告不能直接訪問該變數。注意, Python中直接訪問一個變數始終是可能的,不過資深開發人員沒有藉口這樣做,因為程式碼已經宣告不應該這樣做。這裡使用名稱改編是有一個嚴肅理由的。請繼續往下看。DefaultFormatter把_data變數用作一個整數,預設值為零。

class DefaultFormatter(Publisher):
    def __init__(self, name):
        Publisher.__init__(self)
        self.name = name
        self._data = 0

str()方法返回關於釋出者名稱和_data值的資訊。 type(self).__name是一種獲取類名的方便技巧,避免硬編碼類名。這降低了程式碼的可讀性,卻提高了可維護性。是否喜歡,要看你的選擇。

    def __str__(self):
    return "{}: '{}' has data = {}".format(type(self).__name__, self.name, self._data)

類中有兩個data()方法。第一個使用@property修飾器來提供_data變數的讀訪問方式。這樣,我們就能使用object.data來替代object.data()。

	@property
    def data(self):
    	return self._data

第二個data()更有意思。它使用了@setter修飾器,該修飾器會在每次使用賦值操作符( =)為_data變數賦新值時被呼叫。該方法也會嘗試把新值強制型別轉換為一個整數,並在型別轉換失敗時處理異常。

    @data.setter
    def data(self, new_value):
        try:
        	self._data = int(new_value)
        except ValueError as e:
        	print('Error: {}'.format(e))
        else:
        	self.notify()

下一步是新增觀察者。 HexFormatter和BinaryFormatter的功能非常相似。唯一的不同在於如何格式化從釋出者那獲取到的資料值,即分別以十六進位制和二進位制進行格式化。

class HexFormatter:
    def notify(self, publisher):
        print("{}: '{}' has now hex data = {}".format(type(self).__name__,
        publisher.name, hex(publisher.data)))
class BinaryFormatter:
    def notify(self, publisher):
        print("{}: '{}' has now bin data = {}".format(type(self).__name__,
        publisher.name, bin(publisher.data)))

如果沒有測試資料,示例就不好玩了。 main()函式一開始建立一個名為test1的DefaultFormatter例項,並在之後關聯了兩個可用的觀察者。也使用了異常處理來確保在使用者輸入問題資料時應用不會崩潰。此外,諸如兩次新增相同的觀察者或刪除尚不存在的觀察者之類的事情也不應該導致崩潰。

def main():
    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)

完整程式碼如下:

class Publisher:
    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):
    def __init__(self, name):
        Publisher.__init__(self)
        self.name = name
        self._data = 0
    def __str__(self):
        return "{}: '{}' has data = {}".format(type(self).__name__, self.name,
        self._data)
    @property
    def data(self):
        return self._data
    @data.setter
    def data(self, new_value):
        try:
        	self._data = int(new_value)
        except ValueError as e:
        	print('Error: {}'.format(e))
        else:
        	self.notify()
class HexFormatter:
    def notify(self, publisher):
    	print("{}: '{}' has now hex data = {}".format(type(self).__name__,publisher.name, hex(publisher.data)))
class BinaryFormatter:
    def notify(self, publisher):
        print("{}: '{}' has now bin data = {}".format(type(self).__name__,
        publisher.name, bin(publisher.data)))
def main():
    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()                                                      

輸出如下:

DefaultFormatter: 'test1' has data = 0
HexFormatter: 'test1' has now hex data = 0x3
DefaultFormatter: 'test1' has data = 3
HexFormatter: 'test1' has now hex data = 0x15
BinaryFormatter: 'test1' has now bin data = 0b10101
DefaultFormatter: 'test1' has data = 21
BinaryFormatter: 'test1' has now bin data = 0b101000
DefaultFormatter: 'test1' has data = 40
Failed to remove: <__main__.HexFormatter object at 0x7f30a2fb82e8>
Failed to add: <__main__.BinaryFormatter object at 0x7f30a2fb8320>
Error: invalid literal for int() with base 10: 'hello'
BinaryFormatter: 'test1' has now bin data = 0b101000
DefaultFormatter: 'test1' has data = 40
BinaryFormatter: 'test1' has now bin data = 0b1111
DefaultFormatter: 'test1' has data = 15

在輸出中我們看到,新增額外的觀察者,就會出現更多(相關的)輸出;一個觀察者被刪除後,就再也不會被通知到。這正是我們想要的,能夠按需啟用/禁用執行時通知。

應用的防護性程式設計方面看起來也工作得不錯。嘗試玩一些花樣都是不會被允許的,比如,刪除一個不存在的觀察者或者兩次新增相同的觀察者。不過,顯示的資訊還不太友好,就留給你作為練習吧。在API要求一個數字引數時輸出一個字串所導致的執行時失敗,也能得到正確處理,不會造成應用崩潰/終止。

5. 小結

若希望在一個物件的狀態變化時能夠通知/提醒所有相關者(一個物件或一組物件),則可以使用觀察者模式。觀察者模式的一個重要特性是,在執行時,訂閱者/觀察者的數量以及觀察者是誰可能會變化,也可以改變。

為理解觀察者模式,你可以想一想拍賣會的場景,出價人是訂閱者,拍賣師是釋出者。這一模式在軟體領域的應用非常多。