Python 設計模式——觀察者模式

z正小歪發表於2017-03-26

觀察者模式

定義

定義物件間的一種一對多的依賴關係 ,當一個物件的狀態發生改變時 , 所有依賴於它的物件都得到通知並被自動更新。

動機

將一個系統分割成一系列相互協作的類有一個常見的副作用:需要維護相關物件間的一致性。我們不希望為了維持一致性而使各類緊密耦合,因為這樣降低了它們的可重用性。

適用性

  • 當一個抽象模型有兩個方面 , 其中一個方面依賴於另一方面。將這二者封裝在獨立的物件中以使它們可以各自獨立地改變和複用。
  • 當對一個物件的改變需要同時改變其它物件 , 而不知道具體有多少物件有待改變。
  • 當一個物件必須通知其它物件,而它又不能假定其它物件是誰。換言之 , 你不希望這些物件是緊密耦合的。

優缺點

  • 目標和觀察者間的抽象耦合
  • 支援廣播通訊
  • 意外的更新

實現

有一個氣象站可以獲取溫度、溼度、氧氣的資料,和一些皮膚,每當資料更新時候要顯示在皮膚上 —— 《Head First 設計模式》

class AbstractObservable(object):
    def register(self):
        raise NotImplementedError(
            'register is a abstract method which must be implemente')

    def remove(self):
        raise NotImplementedError(
            'remove is a abstract method which must be implemente')複製程式碼

觀察者這觀察的物件,稱作可觀察物件,該抽象類需要實現具體的註冊和刪除觀察者管理方法。

class AbstractDisplay(object):
    def update(self):
        raise NotImplementedError(
            'update is a abstract method which must be implemente')

    def display(self):
        raise NotImplementedError(
            'display is a abstract method which must be implemente')複製程式碼

觀察者抽象類,需要實現 update 方法,讓可觀察物件可以通知觀察者。

class Subject(object):
    def __init__(self, subject):
        self.subject = subject
        self._observers = []

    def register(self, ob):
        self._observers.append(ob)

    def remove(self, ob):
        self._observers.remove(ob)

    def notify(self, data=None):
        for ob in self._observers:
            ob.update(data)複製程式碼

此外還實現了一個 Subject 用於管理多個事件的通知,可以稱作可觀察物件管理者。

class WeatherData(AbstractObservable):
    def __init__(self, *namespaces):
        self._nss = {}
        self._clock = None
        self._temperature = None
        self._humidity = None
        self._oxygen = None

        for ns in namespaces:
            self._nss[ns] = Subject(ns)

    def register(self, ns, ob):
        if ns not in self._nss:
            raise Exception('this {} is invalid namespace'.format(ns))
        self._nss[ns].register(ob)

    def remove(self, ns, ob):
        return self._nss[ns].remove(ob)

    def set_measurement(self, data):
        # 此處實現可以更加緊湊,但是為了表達更簡單,採用如下方式
        self._clock = data['clock']
        self._temperature = data['temperature']
        self._humidity = data['humidity']
        self._oxygen = data['oxygen']

        for k in self._nss.keys():
            if k != 'all':
                data = self

            self._nss[k].notify(data)

    # 以下 property 為了實現 pull 模式

    @property
    def clock(self):
        return self._clock

    @property
    def temperature(self):
        return self._temperature

    @property
    def humidity(self):
        return self._humidity
    @property
    def oxygen(self):
        return self._oxygen複製程式碼

觀察者模式的可觀察物件實現可以分成兩種實現方案:

  • push 模式
  • pull 模式

push 模式能保證所有的觀察者可以接收到全部的資料,無論需要不需要,頻繁更新會影響效能。

pull 模式需要觀察者自己拉去資料,實現起來比較容易出錯,但是能按需獲取資訊。

class OverviewDisplay(AbstractDisplay):
    def __init__(self):
        self._data = {}

    def update(self, data):
        self._data = data
        self.display()

    def display(self):
        print(u'總覽顯示皮膚:')
        for k, v in self._data.items():
            print(k + ': ' + str(v))複製程式碼

這是一個總覽的 Display ,採用 push 模式更新,獲取當前能獲取的所有資料,並且顯示出來。

class TemperatureDisplay(AbstractDisplay):
    def __init__(self):
        self._storage = []

    def update(self, data):
        dt = data.clock
        temperature = data.temperature
        self._storage.append((dt, temperature))
        self.display()

    def display(self):
        print(u'溫度顯示皮膚:')
        for storey in self._storage:
            print(storey[0] + ': ' + str(storey[1]))複製程式碼

一個只會顯示溫度的 Display,能觀察到時間和溫度變化,由於只關心溫度資料,所以採用 pull 模式更加合適。

if __name__ == '__main__':
    import time

    # 生成一個可觀察物件,支援('all', 'temperature', 'humidity', 'oxygen')的資料通知
    wd = WeatherData('all', 'temperature', 'humidity', 'oxygen')

    # 兩個觀察者物件
    od = OverviewDisplay()
    td = TemperatureDisplay()

    # 註冊到可觀察物件中,能獲取資料更新
    wd.register('all', od)
    wd.register('temperature', td)

    # 更新資料,可觀察物件將會自動更新資料
    wd.set_measurement({
        'clock': time.strftime("%Y-%m-%d %X", time.localtime()),
        'temperature': 20,
        'humidity': 60,
        'oxygen': 10
    })

    # 一秒後再次更新資料
    time.sleep(1)
    print('\n')
    wd.set_measurement({
        'clock': time.strftime("%Y-%m-%d %X", time.localtime()),
        'temperature': 21,
        'humidity': 58,
        'oxygen': 7
    })複製程式碼

執行的結果如下:

總覽顯示皮膚:
humidity: 60
temperature: 20
oxygen: 10
clock: 2017-03-26 18:08:41
溫度顯示皮膚:
2017-03-26 18:08:41: 20

總覽顯示皮膚:
humidity: 58
temperature: 21
oxygen: 7
clock: 2017-03-26 18:08:42
溫度顯示皮膚:
2017-03-26 18:08:41: 20
2017-03-26 18:08:42: 21複製程式碼

一秒後資料更新,兩個皮膚會自動更新資料。

Python 設計模式相關程式碼可以 github.com/zhengxiaowa… 獲得。

該模式的程式碼可以從 raw.githubusercontent.com/zhengxiaowa… 獲得

當需要一個溼度皮膚時候也是隻需要生成這個皮膚、並且實現你所需要的 update、display 方法然後再註冊到可觀察物件中即可,無須修改其他部分,實現了結構的解耦。

觀察者模式在很多軟體和框架中經常出現,比如 MVC 框架,事件的迴圈等應用場景。若希望在一個物件的狀態變化時能夠通知/提醒所有相關者(一個物件或一組物件),則可以使用觀察者模式。觀察者模式的一個重要特性是,在執行時,訂閱者/觀察者的數量以及觀察者是誰可能會變化,也可以改變。

參考資料

相關文章