觀察者模式(有時又被稱爲模型-視圖(View)模式、源-收聽者(Listener)模式或從屬者模式)是軟件設計模式的一種。在此種模式中,一個目標物件管理全部相依於它的觀察者物件,而且在它自己的狀態改變時主動發出通知。這一般透過呼叫各觀察者所提供的方法來實現。此種模式一般被用來實現事件處理系統。設計模式
觀察者模式(Observer)完美的將觀察者和被觀察的對象分離開。舉個例子,用戶界面能夠做爲一個觀察者,業務數據是被觀察者,用戶界面觀察業務數據的變化,發現數據變化後,就顯示在界面上。面向對象設計的一個原則是:系統中的每一個類將重點放在某一個功能上,而不是其餘方面。一個對象只作一件事情,而且將他作好。觀察者模式在模塊之間劃定了清晰的界限,提升了應用程序的可維護性和重用性。bash
觀察者設計模式定義了對象間的一種一對多的組合關係,以便一個對象的狀態發生變化時,全部依賴於它的對象都獲得通知並自動刷新。app
觀察者模式有不少實現方式,從根本上說,該模式必須包含兩個角色:觀察者和被觀察對象。在剛纔的例子中,業務數據是被觀察對象,用戶界面是觀察者。觀察者和被觀察者之間存在「觀察」的邏輯關聯,當被觀察者發生改變的時候,觀察者就會觀察到這樣的變化,而且作出相應的響應。ui
若是在用戶界面、業務數據之間使用這樣的觀察過程,能夠確保界面和數據之間劃清界限,假定應用程序的需求發生變化,須要修改界面的表現,只須要從新構建一個用戶界面,業務數據不須要發生變化。spa
實現觀察者模式的時候要注意,觀察者和被觀察對象之間的互動關係不能體現成類之間的直接調用,不然就將使觀察者和被觀察對象之間緊密的耦合起來,從根本上違反面向對象的設計的原則。不管是觀察者「觀察」觀察對象,仍是被觀察者將本身的改變「通知」觀察者,都不該該直接調用。設計
上面的文字太多了,咱們直接看圖吧3d
從圖上能夠看到,觀察者模式主要有 3 個角色:code
這個流程並不複雜,具體觀察者(好比嘉文四世、銳雯)經過觀察者提供的接口向主題註冊本身,每當主題狀態發生變化時,該主題都會使用觀察者(消息推送功能)提供的通知方式來告知全部的具體觀察者(趙信、嘉文、提莫、銳雯)發生了什麼。cdn
由於你們對英雄聯盟都熟悉啊,並且這不是 IG 爲 LPL 賽區奪得第一個 S 賽冠軍了嘛,我正好蹭一波熱度。server
能夠,只要你可以在你熟悉的領域找到合適的案例來理解,哪怕你用坦克大戰來作例子都是能夠的。
熟悉的臺詞均可以背得出來了,可你知道這些消息從產生到推給每一個召喚師的過程是怎麼樣的麼?
那咱們來整理一下順序吧:
思考:這個過程並不複雜,若是根據上方的流程圖和順序,你能夠寫出消息推送的代碼嗎?
蓋倫是英雄聯盟中最有特色也最使人映像深入的角色,一提到他,咱們想到的一定是他那超大號的大寶劍和開大招時候那一聲 『德瑪西亞』的怒吼。
德瑪西亞的是如何傳到各位召喚師耳朵裏的呢?
上面瞭解了觀察者模式的基本,咱們內心對代碼就會有一個大概的輪廓。好比編寫一個消息通知的類、一個消息隊列、一個觀察者和10個具體觀察者(英雄聯盟每局10個玩家)。
消息隊列有了,那麼如何在觸發事件(蓋倫開大招)的時候將那一聲『德瑪西亞』傳達到廣大英雄(召喚師)的耳朵裏呢?
你又如何肯定該傳到誰那裏,可是又要注意排除那些離得遠的英雄。
首先咱們新建一個消息類,這個消息類中須要提供一個供英雄使用的接口,可以讓觀察者來註冊和註銷,而且維護一個訂閱者隊列以及最後一條消息:
class NewsPublisher(object):
""" 消息主題類 """
def __init__(self):
self.__subscribers = []
self.__latest_news = None
def register(self, subcriber):
""" 觀察者註冊 """
self.__subscribers.append(subcriber)
def detach(self):
""" 觀察者註銷 """
return self.__subscribers.pop()
複製代碼
接着還須要什麼呢?隊列有了,那訂閱者列表和負責消息通知的方法尚未,並且消息建立和最新消息的接口也須要編寫,那麼就將消息類改成:
class NewsPublisher(object):
""" 消息主題類 """
def __init__(self):
self.__subscribers = []
self.__latest_news = None
def register(self, subcriber):
""" 觀察者註冊 """
self.__subscribers.append(subcriber)
def detach(self):
""" 觀察者註銷 """
return self.__subscribers.pop()
def subscribers(self):
""" 訂閱者列表 """
return [type(x).__name__ for x in self.__subscribers]
def notify_subscribers(self):
""" 遍歷列表,通知訂閱者 """
for sub in self.__subscribers:
sub.update()
def add_news(self, news):
""" 新增消息 """
self.__latest_news = news
def get_news(self):
""" 獲取新消息 """
return "收到新消息:", self.__latest_news
複製代碼
而後就要考慮觀察者接口了,觀察者接口是應該是一個抽象基類,具體觀察者(英雄)繼承觀察者。觀察者接口須要有一個監聽方法,只要有新消息發出,那麼全部符合條件的具體觀察者就能夠收到相應的消息:
from abc import ABCMeta, abstractmethod
class Subscriber(metaclass=ABCMeta):
""" 觀察者接口 """
@ abstractmethod
def update(self):
pass
複製代碼
英雄也要有一個 update() 方法,以便消息類能夠向英雄推送消息:
class Garen(object):
""" 蓋倫 """
def __init__(self, publisher):
self.publisher = publisher
self.publisher.register(self)
def update(self):
print(type(self).__name__, self.publisher.get_news())
class JarvanIV(object):
""" 嘉文四世 """
def __init__(self, publisher):
self.publisher = publisher
self.publisher.register(self)
def update(self):
print(type(self).__name__, self.publisher.get_news())
class Riven (object):
""" 銳雯 """
def __init__(self, publisher):
self.publisher = publisher
self.publisher.register(self)
def update(self):
print(type(self).__name__, self.publisher.get_news())
class Quinn(object):
""" 德瑪西亞之翼 """
def __init__(self, publisher):
self.publisher = publisher
self.publisher.register(self)
def update(self):
print(type(self).__name__, self.publisher.get_news())
class XinZhao (object):
""" 德邦總管 """
def __init__(self, publisher):
self.publisher = publisher
self.publisher.register(self)
def update(self):
print(type(self).__name__, self.publisher.get_news())
class AurelionSol(object):
""" 鑄星龍王 """
def __init__(self, publisher):
self.publisher = publisher
self.publisher.register(self)
def update(self):
print(type(self).__name__, self.publisher.get_news())
class Aatrox(object):
""" 暗裔劍魔 """
def __init__(self, publisher):
self.publisher = publisher
self.publisher.register(self)
def update(self):
print(type(self).__name__, self.publisher.get_news())
class Ryze(object):
""" 流浪法師 """
def __init__(self, publisher):
self.publisher = publisher
self.publisher.register(self)
def update(self):
print(type(self).__name__, self.publisher.get_news())
class Teemo(object):
""" 迅捷斥候 """
def __init__(self, publisher):
self.publisher = publisher
self.publisher.register(self)
def update(self):
print(type(self).__name__, self.publisher.get_news())
class Malzahar (object):
""" 瑪爾扎哈 """
def __init__(self, publisher):
self.publisher = publisher
self.publisher.register(self)
def update(self):
print(type(self).__name__, self.publisher.get_news())
複製代碼
召喚師峽谷如今站着十位英雄,意味着遊戲如今開始了。
if __name__ == "__main__":
news_publisher = NewsPublisher() # 實例化消息類
garen_position = (566, 300) # 設定蓋倫當前位置
# 各個英雄當前位置
role_position = [(JarvanIV, 220, 60), (Riven, 56, 235), (Ryze, 1090, 990),
(XinZhao, 0, 0), (Teemo, 500, 500), (Malzahar, 69, 200),
(Aatrox, 460, 371), (AurelionSol, 908, 2098), (Quinn, 1886, 709)]
def valid_position(role_a: int, role_b: int):
# 同屏幕範圍確認
if abs(role_a - role_b) < 200:
return True
return False
for sub in role_position:
if valid_position(sub[1], garen_position[0]) or valid_position(sub[2], garen_position[1]):
# 只發送給同屏幕的英雄
sub[0](news_publisher)
複製代碼
改定義的都定義好了,座標和同屏幕英雄也區分出來了,如何發送新消息呢?
print("在同一個屏幕的英雄有:", news_publisher.subscribers())
news_publisher.add_news("德瑪西亞!")
news_publisher.notify_subscribers()
複製代碼
首先經過訂閱者列表確認同屏幕的英雄,經過消息類中的 add_news() 方法發出蓋倫的怒吼『德瑪西亞』,接着使用消息類中的 notify_subscribers() 方法通知訂閱者列表中全部英雄。
看看輸出結果是什麼:
在同一個屏幕的英雄有: ['Riven', 'Teemo', 'Aatrox']
Riven ('收到新消息:', '德瑪西亞!')
Teemo ('收到新消息:', '德瑪西亞!')
Aatrox ('收到新消息:', '德瑪西亞!')
複製代碼
就這樣,『德瑪西亞』的聲音傳到了暗裔劍魔、迅捷斥候和放逐之刃那裏。
若是第一遍看不懂,能夠一邊看着 UML 圖一邊動手實踐一遍,就可以完全理解觀察者模式了。