觀察者模式
有時,我們希望在一個對象的狀態改變時更新另外一組對象。在MVC模式中有這樣一個非常常見的例子,假設在兩個視圖(例如,一個餅圖和一個電子表格)中使用同一個模型的數據,無論何時更改了模型,都需要更新兩個視圖。這就是觀察者設計模式要處理的問題(請參考[Eckel08,第213頁])。
觀察者模式描述單個對象(發布者,又稱為主持者或可觀察者)與一個或多個對象(訂閱者,又稱為觀察者)之間的發布—訂閱關系。在MVC例子中,發布者是模型,訂閱者是視圖。然而,MVC并非是僅有的發布—訂閱例子。信息聚合訂閱(比如,RSS或Atom)是另一種例子。許多讀者通常會使用一個信息聚合閱讀器訂閱信息流,每當增加一條新信息時,他們就能自動地獲取到更新。
觀察者模式背后的思想等同于MVC和關注點分離原則背后的思想,即降低發布者與訂閱者之間的耦合度,從而易于在運行時添加/刪除訂閱者。此外,發布者不關心它的訂閱者是誰。它只是將通知發送給所有訂閱者(請參考[GOF95,第327頁])。
以下為來自于github的示例:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import print_function
"""http://code.activestate.com/recipes/131499-observer-pattern/"""
class Subject(object):
def __init__(self):
self._observers = []
def attach(self, observer):
if observer not in self._observers:
self._observers.append(observer)
def detach(self, observer):
try:
self._observers.remove(observer)
except ValueError:
pass
def notify(self, modifier=None):
for observer in self._observers:
if modifier != observer:
observer.update(self)
# Example usage
class Data(Subject):
def __init__(self, name=''):
Subject.__init__(self)
self.name = name
self._data = 0
@property
def data(self):
return self._data
@data.setter
def data(self, value):
self._data = value
self.notify()
class HexViewer:
def update(self, subject):
print(u'HexViewer: Subject %s has data 0x%x' %
(subject.name, subject.data))
class DecimalViewer:
def update(self, subject):
print(u'DecimalViewer: Subject %s has data %d' %
(subject.name, subject.data))
# Example usage...
def main():
data1 = Data('Data 1')
data2 = Data('Data 2')
view1 = DecimalViewer()
view2 = HexViewer()
data1.attach(view1)
data1.attach(view2)
data2.attach(view2)
data2.attach(view1)
print(u"Setting Data 1 = 10")
data1.data = 10
print(u"Setting Data 2 = 15")
data2.data = 15
print(u"Setting Data 1 = 3")
data1.data = 3
print(u"Setting Data 2 = 5")
data2.data = 5
print(u"Detach HexViewer from data1 and data2.")
data1.detach(view2)
data2.detach(view2)
print(u"Setting Data 1 = 10")
data1.data = 10
print(u"Setting Data 2 = 15")
data2.data = 15
if __name__ == '__main__':
main()
### OUTPUT ###
# Setting Data 1 = 10
# DecimalViewer: Subject Data 1 has data 10
# HexViewer: Subject Data 1 has data 0xa
# Setting Data 2 = 15
# HexViewer: Subject Data 2 has data 0xf
# DecimalViewer: Subject Data 2 has data 15
# Setting Data 1 = 3
# DecimalViewer: Subject Data 1 has data 3
# HexViewer: Subject Data 1 has data 0x3
# Setting Data 2 = 5
# HexViewer: Subject Data 2 has data 0x5
# DecimalViewer: Subject Data 2 has data 5
# Detach HexViewer from data1 and data2.
# Setting Data 1 = 10
# DecimalViewer: Subject Data 1 has data 10
# Setting Data 2 = 15
# DecimalViewer: Subject Data 2 has data 15
現實生活的例子
現實中,拍賣會類似于觀察者模式。每個拍賣出價人都有一些拍牌,在他們想出價時就可以舉起來。不論出價人在何時舉起一塊拍牌,拍賣師都會像主持者那樣更新報價,并將新的價格廣播給所有出價人(訂閱者)。
下圖展示了觀察者模式與拍賣會的關聯,經www.sourcemaking.com 允許使用(請參考網頁[t.cn/rqr1yxo])。
軟件的例子
django-observer源代碼包(請參考網頁[t.cn/rqr14oz])是一個第三方django包,可用于注冊回調函數,之后在某些django模型字段發生變化時執行。它支持許多不同類型的模型字段(charfield、integerfield等)。
rabbitmq可用于為應用添加異步消息支持,支持多種消息協議(比如,http和amqp),可在python應用中用于實現發布—訂閱模式,也就是觀察者設計模式(請參考網頁[t.cn/rqr1iix])。
應用案例
當我們希望在一個對象(主持者/發布者/可觀察者)發生變化時通知/更新另一個或多個對象的時候,通常會使用觀察者模式。觀察者的數量以及誰是觀察者可能會有所不同,也可以(在運行時)動態地改變。
可以想到許多觀察者模式在其中有用武之地的案例。本章開頭已提過這樣的一個案例,就是信息聚合。尤論格式為RSS、Atom還是其他,思想都一樣:你追隨某個信息源,當它每次更新時,你都會收到關于更新的一個通知(請參考[Zlobin13,第60頁])。
同樣的概念也存在于社交網絡。如果你使用社交網絡服務關聯了另一個人,在關聯的人更新某些內容時,你能收到相關通知,不論這個關聯的人是你關注的一個Twitter用戶,Facebook上的一個真實朋友,還是LinkdIn上的一位同事。
事件驅動系統是另一個可以使用(通常也會使用)觀察者模式的例子。在這種系統中,監聽者被用于監聽特定事件。監聽者正在監聽的事件被創建出來時,就會觸發它們。這個事件可以是鍵入(鍵盤的)某個特定鍵、移動鼠標或者其他。事件扮演發布者的角色,監聽者則扮演觀察者的角色。在這里,關鍵點是單個事件(發布者)可以關聯多個監聽者(觀察者),請參考網頁[t.cn/Rqr1Xgj]。
實現
本節中,我們將實現一個數據格式化程序。這里描述的想法來源于ActiveState網站上觀察者模式用法的Python代碼實現(請參考網頁[t.cn/Rqr1SDO])。默認格式化程序是以十進制格式展示一個數值。然而,我們可以添加/注冊更多的格式化程序。這個例子中將添加一個十六進制格式化程序和一個二進制格式化程序。每次更新默認格式化程序的值時,已注冊的格式化程序就會收到通知,并采取行動。在這里,行動就是以相關的格式展示新的值。
在一些模式中,繼承能體現自身價值,觀察者模式是這些模式中的一個。我們可以實現一個基類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中直接訪問一個變量始終是可能的(請參考[Lott14,第54頁]),不過資深開發人員沒有借口這樣做,因為代碼已經聲明不應該這樣做。這里使用名稱改編是有一個嚴肅理由的。請繼續往下看。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的Default-Formatter實例,并在之后關聯了兩個可用的觀察者。也使用了異常處理來確保在用戶輸入問題數據時應用不會崩潰。此外,諸如兩次添加相同的觀察者或刪除尚不存在的觀察者之類的事情也不應該導致崩潰。
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)
示例的完整代碼(observer.py)如下所示。
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 0x1068730f0>
Failed to add: <__main__.BinaryFormatter object at 0x106873160>
Error: invalid literal for int() with base 10: 'hello'
DefaultFormatter: 'test1' has data = 40
BinaryFormatter: 'test1' has now bin data = 0b1111
DefaultFormatter: 'test1' has data = 15
執行observer.py會輸出以下內容。
python3 observer.py
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要求一個數字參數時輸出一個字符串所導致的運行時失敗,也能得到正確處理,不會造成應用崩潰/終止。
如果是交互式的,這個例子會有趣得多。即使只是以一個簡單的菜單形式允許用戶在運行時綁定/解綁觀察者或修改DefaultFormatter的值,也是不錯的,因為這樣能看到更多的運行時方面的信息。請隨意來做吧。
另一個不錯的練習是添加更多的觀察者。例如,可以添加一個八進制格式化程序、羅馬數字格式化程序或使用你最愛展現形式的任何其他觀察者。發揮你的創意,享受樂趣吧!
小結
本章中,我們學習了觀察者設計模式。若希望在一個對象的狀態變化時能夠通知/提醒所有相關者(一個對象或一組對象),則可以使用觀察者模式。觀察者模式的一個重要特性是,在運行時,訂閱者/觀察者的數量以及觀察者是誰可能會變化,也可以改變。
為理解觀察者模式,你可以想一想拍賣會的場景,出價人是訂閱者,拍賣師是發布者。這一模式在軟件領域的應用非常多。大體上,所有利用MVC模式的系統都是基于事件的。作為具體的例子,我們提到了以下兩項。
- django-observer,一個第三方Django庫,用于注冊在模型字段變更時執行的觀察者。
- RabbitMQ的Python綁定。我們介紹了一個RabbitMQ的具體例子,用于實現發布—訂閱(即觀察者)模式。
在實現例子中,我們看到了如何使用觀察者模式創建可在運行時綁定/解綁的數據格式化程序,以此增強對象的行為。希望你會覺得推薦的練習比較有趣。
第14章介紹狀態設計模式,該模式可用于實現一個核心的計算機科學概念:狀態機。