觀察者模式
有時(shí),我們希望在一個(gè)對(duì)象的狀態(tài)改變時(shí)更新另外一組對(duì)象。在MVC模式中有這樣一個(gè)非常常見(jiàn)的例子丈牢,假設(shè)在兩個(gè)視圖(例如,一個(gè)餅圖和一個(gè)電子表格)中使用同一個(gè)模型的數(shù)據(jù)骤菠,無(wú)論何時(shí)更改了模型它改,都需要更新兩個(gè)視圖。這就是觀察者設(shè)計(jì)模式要處理的問(wèn)題(請(qǐng)參考[Eckel08商乎,第213頁(yè)])央拖。
觀察者模式描述單個(gè)對(duì)象(發(fā)布者,又稱(chēng)為主持者或可觀察者)與一個(gè)或多個(gè)對(duì)象(訂閱者鹉戚,又稱(chēng)為觀察者)之間的發(fā)布—訂閱關(guān)系鲜戒。在MVC例子中,發(fā)布者是模型抹凳,訂閱者是視圖遏餐。然而,MVC并非是僅有的發(fā)布—訂閱例子赢底。信息聚合訂閱(比如失都,RSS或Atom)是另一種例子。許多讀者通常會(huì)使用一個(gè)信息聚合閱讀器訂閱信息流颖系,每當(dāng)增加一條新信息時(shí)嗅剖,他們就能自動(dòng)地獲取到更新。
觀察者模式背后的思想等同于MVC和關(guān)注點(diǎn)分離原則背后的思想嘁扼,即降低發(fā)布者與訂閱者之間的耦合度信粮,從而易于在運(yùn)行時(shí)添加/刪除訂閱者。此外趁啸,發(fā)布者不關(guān)心它的訂閱者是誰(shuí)强缘。它只是將通知發(fā)送給所有訂閱者(請(qǐng)參考[GOF95,第327頁(yè)])不傅。
以下為來(lái)自于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
現(xiàn)實(shí)生活的例子
現(xiàn)實(shí)中旅掂,拍賣(mài)會(huì)類(lèi)似于觀察者模式。每個(gè)拍賣(mài)出價(jià)人都有一些拍牌访娶,在他們想出價(jià)時(shí)就可以舉起來(lái)商虐。不論出價(jià)人在何時(shí)舉起一塊拍牌,拍賣(mài)師都會(huì)像主持者那樣更新報(bào)價(jià)崖疤,并將新的價(jià)格廣播給所有出價(jià)人(訂閱者)秘车。
下圖展示了觀察者模式與拍賣(mài)會(huì)的關(guān)聯(lián),經(jīng)www.sourcemaking.com 允許使用(請(qǐng)參考網(wǎng)頁(yè)[t.cn/rqr1yxo])劫哼。
軟件的例子
django-observer源代碼包(請(qǐng)參考網(wǎng)頁(yè)[t.cn/rqr14oz])是一個(gè)第三方django包叮趴,可用于注冊(cè)回調(diào)函數(shù),之后在某些django模型字段發(fā)生變化時(shí)執(zhí)行权烧。它支持許多不同類(lèi)型的模型字段(charfield眯亦、integerfield等)伤溉。
rabbitmq可用于為應(yīng)用添加異步消息支持,支持多種消息協(xié)議(比如妻率,http和amqp)乱顾,可在python應(yīng)用中用于實(shí)現(xiàn)發(fā)布—訂閱模式,也就是觀察者設(shè)計(jì)模式(請(qǐng)參考網(wǎng)頁(yè)[t.cn/rqr1iix])舌涨。
應(yīng)用案例
當(dāng)我們希望在一個(gè)對(duì)象(主持者/發(fā)布者/可觀察者)發(fā)生變化時(shí)通知/更新另一個(gè)或多個(gè)對(duì)象的時(shí)候糯耍,通常會(huì)使用觀察者模式。觀察者的數(shù)量以及誰(shuí)是觀察者可能會(huì)有所不同囊嘉,也可以(在運(yùn)行時(shí))動(dòng)態(tài)地改變温技。
可以想到許多觀察者模式在其中有用武之地的案例。本章開(kāi)頭已提過(guò)這樣的一個(gè)案例扭粱,就是信息聚合舵鳞。尤論格式為RSS、Atom還是其他琢蛤,思想都一樣:你追隨某個(gè)信息源蜓堕,當(dāng)它每次更新時(shí),你都會(huì)收到關(guān)于更新的一個(gè)通知(請(qǐng)參考[Zlobin13博其,第60頁(yè)])套才。
同樣的概念也存在于社交網(wǎng)絡(luò)。如果你使用社交網(wǎng)絡(luò)服務(wù)關(guān)聯(lián)了另一個(gè)人慕淡,在關(guān)聯(lián)的人更新某些內(nèi)容時(shí)背伴,你能收到相關(guān)通知,不論這個(gè)關(guān)聯(lián)的人是你關(guān)注的一個(gè)Twitter用戶峰髓,F(xiàn)acebook上的一個(gè)真實(shí)朋友傻寂,還是LinkdIn上的一位同事。
事件驅(qū)動(dòng)系統(tǒng)是另一個(gè)可以使用(通常也會(huì)使用)觀察者模式的例子携兵。在這種系統(tǒng)中疾掰,監(jiān)聽(tīng)者被用于監(jiān)聽(tīng)特定事件。監(jiān)聽(tīng)者正在監(jiān)聽(tīng)的事件被創(chuàng)建出來(lái)時(shí)徐紧,就會(huì)觸發(fā)它們静檬。這個(gè)事件可以是鍵入(鍵盤(pán)的)某個(gè)特定鍵、移動(dòng)鼠標(biāo)或者其他并级。事件扮演發(fā)布者的角色拂檩,監(jiān)聽(tīng)者則扮演觀察者的角色。在這里死遭,關(guān)鍵點(diǎn)是單個(gè)事件(發(fā)布者)可以關(guān)聯(lián)多個(gè)監(jiān)聽(tīng)者(觀察者),請(qǐng)參考網(wǎng)頁(yè)[t.cn/Rqr1Xgj]凯旋。
實(shí)現(xiàn)
本節(jié)中呀潭,我們將實(shí)現(xiàn)一個(gè)數(shù)據(jù)格式化程序钉迷。這里描述的想法來(lái)源于ActiveState網(wǎng)站上觀察者模式用法的Python代碼實(shí)現(xiàn)(請(qǐng)參考網(wǎng)頁(yè)[t.cn/Rqr1SDO])。默認(rèn)格式化程序是以十進(jìn)制格式展示一個(gè)數(shù)值钠署。然而糠聪,我們可以添加/注冊(cè)更多的格式化程序。這個(gè)例子中將添加一個(gè)十六進(jìn)制格式化程序和一個(gè)二進(jìn)制格式化程序谐鼎。每次更新默認(rèn)格式化程序的值時(shí)舰蟆,已注冊(cè)的格式化程序就會(huì)收到通知,并采取行動(dòng)狸棍。在這里身害,行動(dòng)就是以相關(guān)的格式展示新的值。
在一些模式中草戈,繼承能體現(xiàn)自身價(jià)值塌鸯,觀察者模式是這些模式中的一個(gè)。我們可以實(shí)現(xiàn)一個(gè)基類(lèi)Publisher唐片,包括添加丙猬、刪除及通知觀察者這些公用功能。DefaultFormatter類(lèi)繼承自Publisher费韭,并添加格式化程序特定的功能茧球。我們可以按需動(dòng)態(tài)地添加刪除觀察者。下面的類(lèi)圖展示了一個(gè)使用兩個(gè)觀察者(HexFormatter和BinaryFormatter)的示例星持。注意抢埋,因?yàn)轭?lèi)圖是靜態(tài)的,所以尤法展示系統(tǒng)的整個(gè)生命周期钉汗,只能展示某個(gè)特定時(shí)間點(diǎn)的系統(tǒng)狀態(tài)羹令。
從Publisher類(lèi)開(kāi)始說(shuō)起。觀察者們保存在列表observers中损痰。add()方法注冊(cè)一個(gè)新的觀察者福侈,或者在該觀察者已存在時(shí)引發(fā)一個(gè)錯(cuò)誤。remove()方法注銷(xiāo)一個(gè)已有觀察者卢未,或者在該觀察者尚未存在時(shí)引發(fā)一個(gè)錯(cuò)誤肪凛。最后,notify()方法則在變化發(fā)生時(shí)通知所有觀察者辽社。
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類(lèi)伟墙。init()做的第一件事情就是調(diào)用基類(lèi)的init()方法,因?yàn)檫@在Python中沒(méi)法自動(dòng)完成滴铅。DefaultFormatter實(shí)例有自己的名字戳葵,這樣便于我們跟蹤其狀態(tài)。對(duì)于_data變量汉匙,我們使用了名稱(chēng)改編來(lái)聲明不能直接訪問(wèn)該變量拱烁。注意生蚁,Python中直接訪問(wèn)一個(gè)變量始終是可能的(請(qǐng)參考[Lott14,第54頁(yè)])戏自,不過(guò)資深開(kāi)發(fā)人員沒(méi)有借口這樣做邦投,因?yàn)榇a已經(jīng)聲明不應(yīng)該這樣做。這里使用名稱(chēng)改編是有一個(gè)嚴(yán)肅理由的擅笔。請(qǐng)繼續(xù)往下看志衣。DefaultFormatter把_data變量用作一個(gè)整數(shù),默認(rèn)值為零猛们。
class DefaultFormatter(Publisher):
def __init__(self, name):
Publisher.__init__(self)
self.name = name
self._data = 0
_str_()方法返回關(guān)于發(fā)布者名稱(chēng)和_data值的信息念脯。type(self). __name是一種獲取類(lèi)名的方便技巧,避免硬編碼類(lèi)名阅懦。這降低了代碼的可讀性和二,卻提高了可維護(hù)性。是否喜歡耳胎,要看你的選擇惯吕。
def str (self):
return "{}: '{}' has data = {}".format(type(self).__name__, self.name, self._data)
類(lèi)中有兩個(gè)data()方法。第一個(gè)使用@property修飾器來(lái)提供_data變量的讀訪問(wèn)方式怕午。這樣废登,我們就能使用object.data來(lái)替代object.data()。
@property
def data(self):
return self._data
第二個(gè)data()更有意思郁惜。它使用了@setter修飾器堡距,該修飾器會(huì)在每次使用賦值操作符(=)為_(kāi)data變量賦新值時(shí)被調(diào)用。該方法也會(huì)嘗試把新值強(qiáng)制類(lèi)型轉(zhuǎn)換為一個(gè)整數(shù)兆蕉,并在類(lèi)型轉(zhuǎn)換失敗時(shí)處理異常羽戒。
@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的功能非常相似虎韵。唯一的不同在于如何格式化從發(fā)布者那獲取到的數(shù)據(jù)值易稠,即分別以十六進(jìn)制和二進(jìn)制進(jìn)行格式化。
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)))
如果沒(méi)有測(cè)試數(shù)據(jù)包蓝,示例就不好玩了驶社。main()函數(shù)一開(kāi)始創(chuàng)建一個(gè)名為test1的Default-Formatter實(shí)例,并在之后關(guān)聯(lián)了兩個(gè)可用的觀察者测萎。也使用了異常處理來(lái)確保在用戶輸入問(wèn)題數(shù)據(jù)時(shí)應(yīng)用不會(huì)崩潰亡电。此外,諸如兩次添加相同的觀察者或刪除尚不存在的觀察者之類(lèi)的事情也不應(yīng)該導(dǎo)致崩潰硅瞧。
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
執(zhí)行observer.py會(huì)輸出以下內(nèi)容。
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
在輸出中我們看到,添加額外的觀察者或辖,就會(huì)出現(xiàn)更多(相關(guān)的)輸出拇勃;一個(gè)觀察者被刪除后,就再也不會(huì)被通知到孝凌。這正是我們想要的,能夠按需啟用/禁用運(yùn)行時(shí)通知月腋。
應(yīng)用的防護(hù)性編程方面看起來(lái)也工作得不錯(cuò)蟀架。嘗試玩一些花樣都是不會(huì)被允許的,比如榆骚,刪除一個(gè)不存在的觀察者或者兩次添加相同的觀察者片拍。不過(guò),顯示的信息還不太友好妓肢,就留給你作為練習(xí)吧捌省。在API要求一個(gè)數(shù)字參數(shù)時(shí)輸出一個(gè)字符串所導(dǎo)致的運(yùn)行時(shí)失敗,也能得到正確處理碉钠,不會(huì)造成應(yīng)用崩潰/終止纲缓。
如果是交互式的,這個(gè)例子會(huì)有趣得多喊废。即使只是以一個(gè)簡(jiǎn)單的菜單形式允許用戶在運(yùn)行時(shí)綁定/解綁觀察者或修改DefaultFormatter的值祝高,也是不錯(cuò)的,因?yàn)檫@樣能看到更多的運(yùn)行時(shí)方面的信息污筷。請(qǐng)隨意來(lái)做吧工闺。
另一個(gè)不錯(cuò)的練習(xí)是添加更多的觀察者。例如瓣蛀,可以添加一個(gè)八進(jìn)制格式化程序陆蟆、羅馬數(shù)字格式化程序或使用你最?lèi)?ài)展現(xiàn)形式的任何其他觀察者。發(fā)揮你的創(chuàng)意惋增,享受樂(lè)趣吧叠殷!
小結(jié)
本章中,我們學(xué)習(xí)了觀察者設(shè)計(jì)模式器腋。若希望在一個(gè)對(duì)象的狀態(tài)變化時(shí)能夠通知/提醒所有相關(guān)者(一個(gè)對(duì)象或一組對(duì)象)溪猿,則可以使用觀察者模式。觀察者模式的一個(gè)重要特性是纫塌,在運(yùn)行時(shí)诊县,訂閱者/觀察者的數(shù)量以及觀察者是誰(shuí)可能會(huì)變化,也可以改變措左。
為理解觀察者模式依痊,你可以想一想拍賣(mài)會(huì)的場(chǎng)景,出價(jià)人是訂閱者,拍賣(mài)師是發(fā)布者胸嘁。這一模式在軟件領(lǐng)域的應(yīng)用非常多瓶摆。大體上,所有利用MVC模式的系統(tǒng)都是基于事件的性宏。作為具體的例子群井,我們提到了以下兩項(xiàng)。
- django-observer毫胜,一個(gè)第三方Django庫(kù)书斜,用于注冊(cè)在模型字段變更時(shí)執(zhí)行的觀察者。
- RabbitMQ的Python綁定酵使。我們介紹了一個(gè)RabbitMQ的具體例子荐吉,用于實(shí)現(xiàn)發(fā)布—訂閱(即觀察者)模式。
在實(shí)現(xiàn)例子中口渔,我們看到了如何使用觀察者模式創(chuàng)建可在運(yùn)行時(shí)綁定/解綁的數(shù)據(jù)格式化程序样屠,以此增強(qiáng)對(duì)象的行為。希望你會(huì)覺(jué)得推薦的練習(xí)比較有趣缺脉。
第14章介紹狀態(tài)設(shè)計(jì)模式痪欲,該模式可用于實(shí)現(xiàn)一個(gè)核心的計(jì)算機(jī)科學(xué)概念:狀態(tài)機(jī)。