Observerパターン


Rubyによるデザインパターン』(ラス・オルセン著 ピアソン・エデュケーション刊)をPythonで書いています。


目次


ソフトウェアでは、ある値が変わるとそれに呼応して別の部分も変化する、というものがよくあります。

例えば、表計算ソフトでセルの値を変更すると、グラフのバーが伸び縮みする等。

Observerパターンでは、
あるオブジェクトに何かがおきたとき、
登録されたオブザーバーにそれを通知します。



例えば、会計システムを考えます。
従業員が昇給・減給したとき、それを経理部に通知しなくてはなりません。


しかも、通知するべき相手といえば経理部に限りません。新たに増えるかも。
通知する相手を集めて管理してしまいましょう。

#observer1.py
class Employee(object):
    def __init__(self, name, title, salary):
        self.name = name
        self.title = title
        self.salary = salary
        self.observers = set()
    
    def set_salary(self, new_salary):
        self.salary = new_salary
        for observer in self.observers:
            observer(self)
    
    def add_observer(self, observer):
        self.observers.add(observer)
    
    def remove_observer(self, observer):
        self.observers.remove(observer)
    
class Payroll(object):
    def update(self, changed_employee):
        print "%sのために小切手を切ります!"%(changed_employee.name)
        print "%sの給料は現在%s円、肩書きは%sです"%(changed_employee.name, 
                                                    changed_employee.salary,
                                                    changed_employee.title)

class Taxman(object):
    def update(self, changed_employee):
        print "%sに新しい税金の請求書を送ります。"%(changed_employee.name)

if __name__ == "__main__":
    payroll = Payroll()
    taxman  = Taxman()
    fred = Employee("フレッド", "クレーン技師", 300*10000)
    fred.add_observer(payroll.update)
    fred.add_observer(taxman.update)
    fred.set_salary(350*10000)
    print
    fred.remove_observer(taxman.update)
    fred.set_salary(400*10000)


このような何かのニュースを通知するパターンを、Observerパターンと言います。
通知する側(今回はEmployee)はSubject
通知される側(TaxmanやPayroll)はObserverと呼ばれます。


ところで上のソースでは、

observer(self)

と、オブザーバーにサブジェクト自身を渡し、
オブザーバーが自分で詳細情報を引っ張り出していますが(Pull型)、

observer(self.name, self.title, self.salary)

のように、サブジェクトの側で詳細情報をオブザーバーに渡す(Push型
やり方もあります。




Push型は、オブザーバーの側が監視のために一々情報を引っ張りだす必要がないので楽ですが、反面、サブジェクト側の負担になります。


Rubyによるデザインパターン』では、この後Observer機能を抜き出したMixinを作るのですが、

ちょっと違った方法を取ってみます。

Python Package Indexで検索してみた所、
py-notifyというライブラリが見つかりました。
observer2.py

from notify.all import *

class Employee(object):
    def __init__(self, name, title, salary):
        self.name = name
        self.title = title
        self.salary = salary
        self.signal =  Signal()
    
    def set_salary(self, new_salary):
        self.salary = new_salary
        self.signal(self)
    
    def add_observer(self, observer):
        self.signal.connect(observer)
    
    def remove_observer(self, observer):
        self.signal.disconnect(observer)

オブザーバーの登録や通知はself.signalに委譲します。

継承ではなく、委譲を使うことで柔軟性が増す場合があります。
通知するニュース毎に、オブザーバーを別のsignalに登録しするとか。
Strategyパターンを使うとか。

注意1.本当に変更が起きたときに通知すること
set_salaryでセットされた値が以前と同じだった時は通知するべきではありません。

注意2.変更が完了してから通知すること

fred = Employee("フレッド", "クレーン技師", 300*10000)
fred.add_observer(payroll.update)

fred.set_salary(1000*10000)
#ここで不正状態が発生!

fred.set_title("営業担当副社長")

一瞬、高給取りのクレーン技師が生まれてしまいます。
一連の変更が完了するまで、オブザーバーに通知しない仕組みを作る必要があります。

py-notifyのSignalではなく、Variableを使えば上の2問題に対処できます。
observer3.py

from notify.all import *
class Employee(object):
    def __init__(self, name, title, salary):
        self.name   = name
        self.title  =  title
        self.salary = salary
        self.var    = Variable()
        self.var.value = (self.name, self.title, self.salary)
    
    def set_salary(self, new_salary):
        self.salary = new_salary
        self.var.value = (self.name, self.title, self.salary)
    
    def set_title(self, new_title):
        self.title = new_title
        self.var.value = (self.name, self.title, self.salary)
    
    def connect(self, observer):
        self.var.changed.connect(observer)
    
    def disconnect(self, observer):
        self.var.changed.disconnect(observer)
    
    def with_changes_frozen(self, *a, **kw):
        return self.var.with_changes_frozen(*a, **kw)
    
class Payroll(object):
    def update(self, a):
        (name, title, salary) = a
        print "%sのために小切手を切ります!"%(name)
        print "%sの給料は現在%s円、肩書きは%sです"%(name, salary, title)

if __name__ == "__main__":
    payroll = Payroll()
    taxman  = Taxman()
    fred = Employee("フレッド", "クレーン技師", 300*10000)
    fred.connect(payroll.update)
    
    fred.set_salary(350*10000)
    fred.set_salary(350*10000)
    
    def do_chages():
        fred.set_salary(1000*10000)
        fred.set_title("営業部長")
    fred.with_changes_frozen(do_chages)

Variable.changed.connectすると、値が変更されたときだけ通知されます。

ただし、オブザーバーにはVariable.valueの値が渡されるので、Push型として使うのが基本です。

self.var.value = (self, self.name, self.title, self.salary)

とすれば、Subjectを渡せないこともないですが・・・

詳しくは、py-notifyのチュートリアルをご覧ください。