オブザーバパターンとは何か
オブザーバとは、あるオブジェクトの変化を、登録した複数のオブジェクトに自動で知らせる型です。ステートとは、状態ごとにクラスを分けて、状態によって動きを変える型です。
どちらも、if の連続や、相手の名前の直書きをなくし、変更に強い作りにします。
このレッスンのゴール
メモ
変化を多くのオブジェクトに知らせるオブザーバと、状態で動きが変わるステートを書くことができる。
キーワード
| ことば | いみ | れい |
|---|---|---|
| オブザーバ | 変化の知らせを受け取る側のオブジェクト。観察者とも呼ぶ | |
| サブジェクト | 変化を知らせる側。観察される対象とも呼ぶ | Shelf |
| 登録と解除 | 知らせを受け取り始めることと、やめること | subscribe |
| 通知 | サブジェクトが、登録された全員の update を呼ぶこと | |
| ステート | 状態ごとにクラスを分けて、動きを切り替える型 | |
| 状態の遷移 | 次の状態へ移ること | Waiting から Running へ |
学生が入力するセルは、このプレビューでは空白です。
オブザーバは変化を登録した全員に知らせる
棚の在庫が減ったとき、補充の係にも、記録の係にも知らせたいとします。棚が、係を名前で直接呼ぶと、係が増えるたびに棚を直すことになります。
棚は、知らせる相手のリストだけを持ちます。変化があれば、全員の update を呼びます。これがオブザーバです。
在庫が減ると、見張り役に知らせる
class Shelf:
def __init__(self):
self.count = 10
# 知らせる相手を、リストに入れておきます。
self.watchers = []
def subscribe(self, watcher):
self.watchers.append(watcher)
def take(self, amount):
self.count = self.count - amount
# 相手が誰かは知らず、update と呼ぶだけです。
for watcher in self.watchers:
watcher.update(self.count)
class LowAlarm:
def update(self, count):
if count < 3:
print("補充が必要です。残りは", count)
shelf = Shelf()
shelf.subscribe(LowAlarm())
shelf.take(4)
shelf.take(5)
# take(5) を take(2) に変えて、もう一度実行しましょう。
例: 棚の在庫と通知
| 操作 | 残りの数 | 見張り役の動き |
|---|---|---|
| 最初の状態 | 10 | 知らせは来ない |
| take(4) | 6 | 3より多いので、何も出さない |
| take(5) | 1 | 3より少ないので、補充の知らせを出す |
学生が入力するセルは、このプレビューでは空白です。
ステートは状態ごとにクラスを分ける
ジョブには、待機中、実行中、完了の3つの状態があります。ボタンを押したときの動きは、状態で違います。
状態の文字列と if で分けると、状態が増えるたびに if が増えます。状態をクラスにして、press をそれぞれに書くのがステートです。
状態をクラスにして、次へ進める
class Waiting:
def press(self):
return Running()
def label(self):
return "待機中"
class Running:
def press(self):
return Done()
def label(self):
return "実行中"
class Done:
def press(self):
return self
def label(self):
return "完了"
# 状態を、状態のオブジェクトとして持ちます。
state = Waiting()
for step in range(3):
print(state.label())
state = state.press()
例: ジョブの状態の移り変わり
| 今の状態 | press の答え | 次の状態 |
|---|---|---|
| Waiting | Running を返す | 実行中 |
| Running | Done を返す | 完了 |
| Done | 自分自身を返す | 完了のまま |
学生が入力するセルは、このプレビューでは空白です。
ラボで2つのパターンを動かす
ラボの端末で、棚のファイルとジョブのファイルを作り、動かします。6つの Run を、上から順に押してください。
出力は英語にしてあります。端末の文字コードに、頼らないためです。
作業用のフォルダを作る
mkdir -p ~/oop/behaviourここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
そのフォルダへ移る
cd ~/oop/behaviourここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
棚のファイルを書く
printf 'class Shelf:\n def __init__(self):\n self.count = 10\n self.watchers = []\n\n def subscribe(self, watcher):\n self.watchers.append(watcher)\n\n def take(self, amount):\n self.count = self.count - amount\n for watcher in self.watchers:\n watcher.update(self.count)\n\n\nclass LowAlarm:\n def update(self, count):\n if count < 3:\n print("low stock", count)\n\n\nclass Recorder:\n def __init__(self):\n self.history = []\n\n def update(self, count):\n self.history.append(count)\n\n\nshelf = Shelf()\nrecorder = Recorder()\nshelf.subscribe(LowAlarm())\nshelf.subscribe(recorder)\nshelf.take(4)\nshelf.take(5)\nprint(recorder.history)\n' > shelf.pyここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
棚を動かす
python3 shelf.pyここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
知らせは在庫が3より少ないときだけ出る
出力は、low stock 1 と [6, 1] の2行です。
take(4) で残りが6のとき、補充の知らせは出ません。take(5) で1になると出ます。記録係は、どちらの回も受け取り、[6, 1] を覚えています。
ジョブのファイルを書く
printf 'class Waiting:\n def label(self):\n return "waiting"\n\n def press(self):\n return Running()\n\n\nclass Running:\n def label(self):\n return "running"\n\n def press(self):\n return Done()\n\n\nclass Done:\n def label(self):\n return "done"\n\n def press(self):\n return self\n\n\nclass Job:\n def __init__(self):\n self.state = Waiting()\n\n def press(self):\n self.state = self.state.press()\n\n\njob = Job()\nfor step in range(4):\n print(job.state.label())\n job.press()\n' > job.pyここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
ジョブを動かす
python3 job.pyここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
完了のあとは、何度押しても完了のまま
出力は、waiting、running、done、done の4行です。
Done の press は自分自身を返すので、4回目も完了のままです。状態の移り変わりは、クラスの中に入っています。
よくある間違い
注意
「知らせる側が、相手を名前で直接呼べばよい」と考える人が多くいます。それでは、相手が増えるたびに、知らせる側を書き換えます。リストに登録して、同じ update を呼べば、書き換えずに済みます。