從生活中領悟監聽模式——坑爹的熱水器
【故事劇情】
剛剛大學畢業的Tony隻身來到北京這個碩大的城市,開始了北漂的生活。但剛剛畢業的他身無絕技、包無分文,為了生活只能住在沙河鎮一個偏僻的村子裡,每天坐著程式設計師專線(13號線)來回穿梭於昌平區與西城區……
在一個寒冷的冬天,下班之後要坐2個小時的地鐵+公交才能回到住處,Tony拖著疲憊的身體回到家。準備洗一個熱水澡暖暖身體,耐何簡陋的房子中用的還是90年代的熱水器。因為熱水器沒有警報更沒有自動切換模式的功能,所以燒熱水必須得守著;不然時間長了成殺豬燙,時間短了又冷成狗。無奈的Tony背靠著牆,頭望著天花板,深夜中做起了白日夢:一定要努力工作,過兩個月我就可以自己買一個智慧熱水器了:水燒好了就發一個警報,我就可以直接去洗操。還要能自己設定模式,既可以燒開了用來喝,可以燒暖了用來洗澡……
用程式來模擬生活
Tony陷入白日夢中……他的夢雖然不能在現實世界中立即實現,但在程式世界裡可以。程式來源於生活,下面我們就用程式碼來模擬Tony白日夢,哈哈……
原始碼示例:
class WaterHeater:
"熱水器:戰勝寒冬的有利武器"
def __init__(self):
self.__observers = []
self.__temperature = 25
def getTemperature(self):
return self.__temperature
def setTemperature(self, temperature):
self.__temperature = temperature
print("current temperature is:", self.__temperature)
self.notifies()
def addObserver(self, observer):
self.__observers.append(observer)
def notifies(self):
for o in self.__observers:
o.update(self)
class Observer:
"洗澡模式和飲用模式的父類"
def update(self, waterHeater):
pass
class WashingMode(Observer):
"該模式用於洗澡用"
def update(self, waterHeater):
if waterHeater.getTemperature() >= 50 and waterHeater.getTemperature() < 70:
print("水已燒好,溫度正好!可以用來洗澡了。")
class DrinkingMode(Observer):
"該模式用於飲用"
def update(self, waterHeater):
if waterHeater.getTemperature() >= 100:
print("水已燒開!可以用來飲用了。")
測試程式碼:
def testWaterHeater():
heater = WaterHeater()
washingObser = WashingMode()
drinkingObser = DrinkingMode()
heater.addObserver(washingObser)
heater.addObserver(drinkingObser)
heater.setTemperature(40)
heater.setTemperature(60)
heater.setTemperature(100)
輸出結果:
current temperature is: 40
current temperature is: 60
水已燒好,溫度正好!可以用來洗澡了。
current temperature is: 100
水已燒開!可以用來飲用了。
從劇情中思考監聽模式
這個程式碼非常簡單,水燒到50-70度時,會發出警告:可以用來洗澡了!燒到100度也會發出警告:可以用來喝了!在這裡洗澡模式和飲用模式扮演了監聽的角色,而熱水器則是被監聽的物件。一旦熱水器中的水溫度發生變化,監聽者都能及時知道並做出相應的判斷和動作。其實這就是程式設計中監聽模式的生動展現。
監聽模式
監聽模式又名觀察者模式,顧名思意就是觀察與被觀察的關係,比如你在燒開水得時時看著它開沒開,你就是觀察者,水就是被觀察者;再比如說你在帶小孩,你關注她是不是餓了,是不是喝了,是不是撒尿了,你就是觀察者,小孩就被觀察者。觀察者模式是物件的行為模式,又叫釋出-訂閱(Publish/Subscribe)模式、模型-檢視(Model/View)模式、源-監聽器(Source/Listener)模式或從屬者(Dependents)模式。當你看這些模式的時候,不要覺得陌生,它們就是觀察者模式。
觀察者模式一般是一種一對多的關係,可以有任意個(一個或多個)觀察者物件同時監聽某一個物件。監聽的物件叫觀察者(後面提到監聽者,其實就指觀察者,兩者是等價的),被監聽的物件叫被觀察者(Observable,也叫主題Subject)。被觀察者物件在狀態或內容發生變化時,會通知所有觀察者物件,使它們能夠做出相應的變化(如自動更新自己的資訊)。
監聽模式的模型抽象
程式碼框架
上面的示例程式碼還是相對比較粗糙,我們可以對它進行進一步的重構和優化,抽象出監聽模式的框架模型。
class Observer:
"觀察者的基類"
def update(self, observer, object):
pass
class Observable:
"被觀察者的基類"
def __init__(self):
self.__observers = []
def addObserver(self, observer):
self.__observers.append(observer)
def removeObserver(self, observer):
self.__observers.remove(observer)
def notifyObservers(self, object = 0):
for o in self.__observers:
o.update(self, object)
類圖
上面的程式碼框架可用類圖表示如下:
addObserver、removeObserver分別用於新增和刪除觀察者,notifyObservers用於內容或狀態變化時通知所有的觀察者。因為Observable的notifyObservers會呼叫Observer的update方法,所有觀察者不需要關心被觀察的物件什麼時候會發生變化,只要有變化就是自動呼叫update,只需要關注update實現就可以了。
基於框架的實現
有了上面的程式碼框架之後,我們要實現示例程式碼的功能就會更簡單了。最開始的示例程式碼我們假設它為version 1.0,那麼再看看基於框架的version 2.0吧。
class WaterHeater(Observable):
"熱水器:戰勝寒冬的有利武器"
def __init__(self):
super().__init__()
self.__temperature = 25
def getTemperature(self):
return self.__temperature
def setTemperature(self, temperature):
self.__temperature = temperature
print("current temperature is:", self.__temperature)
self.notifyObservers()
class WashingMode(Observer):
"該模式用於洗澡用"
def update(self, observable, object):
if isinstance(observable,
WaterHeater) and observable.getTemperature() >= 50 and observable.getTemperature() < 70:
print("水已燒好,溫度正好!可以用來洗澡了。")
class DrinkingMode(Observer):
"該模式用於飲用"
def update(self, observable, object):
if isinstance(observable, WaterHeater) and observable.getTemperature() >= 100:
print("水已燒開!可以用來飲用了。")
測試程式碼不用變。自己跑一下,會發現輸出結果和之前的是一樣的。
模型說明
設計要點
在設計觀察者模式的程式時要注意以下幾點:
1. 要明確誰是觀察者誰是被觀察者,只要明白誰是關注物件,問題也就明白了。一般觀察者與被觀察者之間的是多對一的關係,一個被觀察物件可以有多個監聽物件(觀察者)。如一個編輯框,有滑鼠點選的監聽者,也有鍵盤的監聽者,還有內容改變的監聽者。
2. Observable在傳送廣播通知的時候,無須指定具體的Observer,Observer可以自己決定是否要訂閱Subject的通知。
3. 被觀察者至少需要有三個方法:新增監聽者、移除監聽者、通知Observer的方法;觀察者至少要有一個方法:更新方法,更新當前的內容,作出相應的處理。
4. 新增監聽者、移除監聽者在不同的模型稱謂中可能會有不同命名,如觀察者模型中一般,addObserver、removeObserver;在源-監聽器(Source/Listener)模型中一般是attach/detach,應用在桌面程式設計的視窗中,還可能是attachWindow/detachWindow,或Register/UnRegister。不要被名稱迷糊了,不管他們是什麼名稱,其實功能都是一樣的,就是新增/刪除觀察者。
推模型和拉模型
觀察者模式根據其側重的功能還可以分為推模型和拉模型。
推模型:被觀察者物件向觀察者推送主題的詳細資訊,不管觀察者是否需要,推送的資訊通常是主題物件的全部或部分資料。一般這種模型的實現中,會把被觀察者物件中的全部或部分資訊通過update的引數傳遞給觀察者[update(Object obj) ,通過obj引數傳遞]。
如某應用App的服務要在凌晨1:00開始進行維護,1:00-2:00期間所有服務將會暫停,這裡你就需要向所有的App客戶端推送完整的通知訊息:“本服務將在凌晨1:00開始進行維護,1:00-2:00期間所有服務將會暫停,感謝您的理解和支援!” 不管使用者想不想知道,也不管使用者會不會在這段期間去訪問,訊息都需要被準確無誤地通知到。這就是典型的推模型的應用。
拉模型:被觀察者在通知觀察者的時候,只傳遞少量資訊。如果觀察者需要更具體的資訊,由觀察者主動到被觀察者物件中獲取,相當於是觀察者從被觀察者物件中拉資料。一般這種模型的實現中,會把被觀察者物件自身通過update方法傳遞給觀察者[update(Observable observable ),通過observable 引數傳遞 ],這樣在觀察者需要獲取資料的時候,就可以通過這個引用來獲取了。
如某應用App有新的版本推出,則需要傳送一個版本升級的通知訊息,而這個通知訊息只會簡單地列出版本號和下載地址,如果你需要升級你的App還需要呼叫下載介面去下載安裝包完成升級。這其實也可以理解成是拉模型。
推模型和拉模型其實更多的是語義和邏輯上的區別。我們上面的程式碼框架,從介面[update(self, observer, object)]上你應該知道是可以同時支援推模型和拉模型的。推模型時,observer可以傳空,推送的資訊全部通常object傳遞;拉模型時,observer和object都傳遞資料,或只傳遞observer,需要更具體的資訊時通過observer引用去取資料。
應用場景
- 對一個物件狀態或資料的更新需要其他物件同步更新;,或者一個物件的更新需要依賴另一個物件的更新;
- 物件僅需要將自己的更新通知給其他物件而不需要知道其他物件的細節,如訊息推送。
學習設計模式,更應該領悟其設計思想,不應該應該侷限於程式碼的層面。 觀察者模式還可以用於網路中的客戶端和伺服器,比如手機中的各種App的訊息推送,服務端是被觀察者,各個手機App是觀察者,一旦伺服器上的資料(如App升級資訊)有更新,就會被推送到手機客戶端。在這個應用中你會發現伺服器程式碼和App客戶端程式碼其實是兩套完全不一樣的的程式碼,它們是通過網路介面進行通迅的,所以如果你只是停留在程式碼層面是無法理解的!
更多更有趣的文章
想獲得更多更有趣的設計模式嗎?一起來閱讀以下系列文章吧!
程式原始碼
引導篇
基礎篇
進階篇
經驗篇
長按或掃碼二維碼,在手機端閱讀更多內容