はじめに
この記事は個人的な勉強メモです。inputしたものはoutputしなくてはという強迫観念に駆られて記事を書いています。
あわよくば詳しい人に誤りの指摘やアドバイスを頂ければいいなという思いを込めてQiitaの記事にしています。
エンジニアとして社会人生活を送っていますが、デザインパターンについてちゃんと学んだことがなかったので勉強してみました。
ここに記載している内容は
https://github.com/ck-fm0211/notes_desigh_pattern
にuploadしています。
過去ログ
デザインパターンについて勉強してみた(個人的メモ)その1
デザインパターンについて勉強してみた(個人的メモ)その2
デザインパターンについて勉強してみた(個人的メモ)その3
デザインパターンについて勉強してみた(個人的メモ)その4
デザインパターンについて勉強してみた(個人的メモ)その5
デザインパターンについて勉強してみた(個人的メモ)その6
Observerパターン
- 状態の変化を観察することを目的としたものであるが、どちらかというと「観察」よりも「通知」に重点の置かれたものになっている
- あるインスタンスの状態が変化した際に、そのインスタンス自身が、「観察者」に状態の変化を「通知」する仕組み。
- 例
- 飛行機の搭乗券について考える。
- 飛行機の搭乗券を購入した人がキャンセルする必要に駆られた場合、航空会社に連絡してキャンセルする旨を伝える。
- 各搭乗者を管理しているのは「航空会社」で、キャンセルを伝えるのは「搭乗者」となる。
- 「搭乗者」は、チケットが欲しくなったり、いらなくなったりしたら「航空会社」に連絡する。
- このような仕組みによって、航空会社は全てのユーザを常に観察する必要がなくなる。
実際に使ってみる
題材
- 上司と部下たち
- 部下たちに成果物の作成を複数依頼
- その完成報告を上司に伝える
- Observerインタフェースを用意するところがキモ
- Observer インタフェースでは、観察対象が変化した際に、観察対象からの通知を受ける窓口となる update メソッドを定義する
- 観察対象となる Subject 抽象クラスを作成
- Observer インスタンスを自由に追加するためのadd_observer メソッドと、保持している Observer インスタンスの全てに変更を通知する notify_observers メソッドを実装する
Observerパターンのまとめ
Mementoパターン
- Memento: 記念品、形見
- インスタンスのあるときの状態をスナップショットとして保存しておくことで、 その時のインスタンスの状態を復元することを可能にするもの
- インスタンスの状態が、プログラム実行中にどんどん変化することが考えられる
- 一度変化してしまったインスタンスを、「少し前の状態に戻したい」「ある時点の状態に戻したい」などの要求が発生しうる
- Memento パターンを使うと、インスタンスのある時の状態を、簡単にスナップショットとして残すことができ、 さらに、そこからの復元も可能になる
- インスタンス全ての状態を覚えておくために、 clone を作成することもあるが、Memento パターンでは、必要な情報のみを保持しておき、必要なデータのみを復元することを考える
実際に使ってみる
題材
- 足し算
- 1〜5を足すと・・・
1+2+3+4+5=15
- 次に、1~10を足すと・・・?
- パターン1:
1+2+3+4+5+6+7+8+9+10=55
- パターン2:
15+6+7+8+9+10=55
- パターン1:
- パターン2の方がMementoパターン
- これをコードにする
# -*- coding:utf-8 -*-
class Memento:
"""途中経過を保持する Memento クラス"""
result = -1 # 計算の途中経過を表す
def __init__(self, temp):
"""計算経過を引数に受け取るコンストラクタ"""
Memento.result = temp
class Calc:
"""ひとつの計算を表すクラス。"""
temp = 0
@staticmethod
def plus(plus):
"""足し算を実行するメソッド"""
Calc.temp += plus
@staticmethod
def create_memento():
"""途中経過を Memento として取得するメソッド"""
return Memento(Calc.temp)
@staticmethod
def set_memento(memento: Memento):
"""Memento から計算経過を取得して、temp にセットする"""
Calc.temp = memento.result
@staticmethod
def get_temp():
"""計算結果を取得するメソッド"""
return Calc.temp
class Calculator:
def __init__(self):
self.result_dict = {}
def run(self):
c = Calc()
# 1回目の計算
for i in range(1, 6):
c.plus(i)
print(f"5までの足し算: {c.get_temp()}")
self.result_dict["5までの足し算"] = c.create_memento()
# 2回目の計算
c2 = Calc()
c2.set_memento(self.result_dict["5までの足し算"])
# 1回目の計算
for i in range(6, 11):
c2.plus(i)
print(f"10までの足し算: {c2.get_temp()}")
self.result_dict["10までの足し算"] = c2.get_temp()
if __name__ == '__main__':
c = Calculator()
c.run()
- ある段階のものを「スナップショット」として残しておくことで、その時の状態にすばやく戻すことができる
- Memento パターンでは、何の値を Memento として残すべきか、Originator(ここでは Calc) に委ねられている。
- Originator は、必要と思われる情報を Memento として残し、Memento から状態を復元する
Mementoパターンのまとめ
Stateパターン
- オブジェクト指向設計では、モノをクラスとして表現することが多くある
- モノではなく、「状態」をクラスとして表現するパターン
実際に使ってみる
題材
- 職場での会話
- 上司の機嫌によってタスクの振り方が変わる(そんな職場嫌だ...)
- 機嫌がいいとき
部下:おはようございます
上司:おはよう
部下:今日はxxxやります
上司:いいね、がんばって
- 機嫌が悪いとき
部下:おはようございます
上司:おう
部下:今日はxxxやります
上司:おい、oooやれって言ったよな?やったのか?
部下:(聞いてないっす・・・)すみません、すぐやります!!!!!!!!
- コードにする
# -*- coding:utf-8 -*-
class Boss:
STATE_ORDINARY = 0 # 通常時の上司
STATE_IN_BAD_MOOD = 1 # 機嫌の悪い上司
def __init__(self):
self.state = -1 # 上司の状態を表す
def change_state(self, state):
"""上司の状態を変更する"""
self.state = state
def morning_greet(self):
"""朝の挨拶を返す"""
if self.state == Boss.STATE_ORDINARY:
return "おはよう"
elif self.state == Boss.STATE_IN_BAD_MOOD:
return "おう"
else:
pass
def assign_task(self):
"""タスクを振る"""
if self.state == Boss.STATE_ORDINARY:
return "いいね、がんばって"
elif self.state == Boss.STATE_IN_BAD_MOOD:
return "おい、oooやれって言ったよな?やったのか?"
else:
pass
- 上司の上司から指摘が入り、もっとちゃんとしたマネジメントをするようになったとする → 新たなパターンが生まれた
- if分岐に手を入れていくのはイケてない
- State パターンでは、「状態」を表すクラスを用意し、この「状態」を入れ替え可能にしておく。
- サンプルケースであれば、まずは、「ご機嫌斜め状態」「普通の状態」が必要となる。
- State の変更は、どのクラスが行っても良いのですが、今回のケースでは、内部のどこかから変更されるものとしてみる。
# -*- coding:utf-8 -*-
from abc import ABCMeta, abstractmethod
class State(metaclass=ABCMeta):
@staticmethod
def morning_greet():
"""朝の挨拶"""
pass
@staticmethod
def assign_task():
"""タスクを振る"""
pass
class BadMoodState(State):
@staticmethod
def morning_greet():
return "おう"
@staticmethod
def assign_task():
return "おい、oooやれって言ったよな?やったのか?"
class OrdinaryState(State):
@staticmethod
def morning_greet():
return "おはよう"
@staticmethod
def assign_task():
return "いいね、がんばって"
class StatePatternBoss:
def __init__(self):
self.state = None
def change_state(self, state: State):
self.state = state
def morning_greet(self):
return self.state.morning_greet()
def assign_task(self):
return self.state.assign_task()
if __name__ == '__main__':
boss_state = StatePatternBoss()
print("===== 1日目:機嫌よし =====")
boss_state.change_state(OrdinaryState())
print("部下:おはようございます")
print(f"上司:{boss_state.morning_greet()}")
print("部下:今日はxxxやります")
print(f"上司:{boss_state.assign_task()}")
print("===== 2日目:機嫌悪い =====")
boss_state.change_state(BadMoodState())
print("部下:おはようございます")
print(f"上司:{boss_state.morning_greet()}")
print("部下:今日はxxxやります")
print(f"上司:{boss_state.assign_task()}")
===== 1日目:機嫌よし =====
部下:おはようございます
上司:おはよう
部下:今日はxxxやります
上司:いいね、がんばって
===== 2日目:機嫌悪い =====
部下:おはようございます
上司:おう
部下:今日はxxxやります
上司:おい、oooやれって言ったよな?やったのか?
- こうすることで、ご機嫌パターンが増えても対応が楽
(前略)
class GoodMoodState(State):
@staticmethod
def morning_greet():
return "おはよう!今日も頑張ろう!"
@staticmethod
def assign_task():
return "いいね!こないだのoooもいい感じだったよ!この調子で頑張ってね!"
(中略)
print("===== 3日目:機嫌良い =====")
boss_state.change_state(GoodMoodState()) # ここを変更するだけ
print("部下:おはようございます")
print(f"上司:{boss_state.morning_greet()}")
print("部下:今日はxxxやります")
print(f"上司:{boss_state.assign_task()}")
===== 3日目:機嫌良い =====
部下:おはようございます
上司:おはよう!今日も頑張ろう!
部下:今日はxxxやります
上司:いいね!こないだのoooもいい感じだったよ!この調子で頑張ってね!
Stateパターンのまとめ
Flyweightパターン
- Flyweight:英語で「フライ級」を意味
- インスタンスを共有することでリソースを無駄なく使うことに焦点を当てたパターン
- 例:
ホームページの背景に使う小さな画像は、背景で表示される回数分ネットワークごしにやり取りされるわけではなく、通常、画像を一回取得し、「その画像」を並べて表示する - 同じインスタンスを共有することで、無駄なインスタンスを生成しないようにして、 プログラム全体を軽くすることを目的としたパターン
実際に使ってみる
題材
- 「人文字」を使ったメッセージをつくってみる
- 「あいはあおよりもあおい」
- 一文字ずつ「人文字」で作って屋上から撮影し写真に残していく。
- 一文字の人文字を表すクラスを HumanLetter クラスとすると、 人文字でメッセージを生成するクラスは、以下のようになる
# -*- coding:utf-8 -*-
class HumanLetter:
def __init__(self, letter):
self._letter = letter
def get_letter(self):
return self._letter
class Main:
@staticmethod
def take_a_photo(letter: HumanLetter):
"""写真を取る"""
print(letter.get_letter())
def main(self):
"""一文字を作成する"""
a = HumanLetter("あ")
self.take_a_photo(a)
i = HumanLetter("い")
self.take_a_photo(i)
ha = HumanLetter("は")
self.take_a_photo(ha)
a2 = HumanLetter("あ")
self.take_a_photo(a2)
o = HumanLetter("お")
self.take_a_photo(o)
yo = HumanLetter("よ")
self.take_a_photo(yo)
ri = HumanLetter("り")
self.take_a_photo(ri)
mo = HumanLetter("も")
self.take_a_photo(mo)
a3 = HumanLetter("あ")
self.take_a_photo(a3)
o2 = HumanLetter("お")
self.take_a_photo(o2)
i2 = HumanLetter("い")
self.take_a_photo(i2)
if __name__ == '__main__':
h = Main()
h.main()
- 同じ文字が何度も出てくる。
- 写真を取るだけなら使いまわしできるので...
# -*- coding:utf-8 -*-
class HumanLetter:
def __init__(self, letter):
self._letter = letter
def get_letter(self):
return self._letter
class Main:
@staticmethod
def take_a_photo(letter: HumanLetter):
"""写真を取る"""
print(letter.get_letter())
def main(self):
"""一文字を作成する"""
a = HumanLetter("あ")
self.take_a_photo(a)
i = HumanLetter("い")
self.take_a_photo(i)
ha = HumanLetter("は")
self.take_a_photo(ha)
self.take_a_photo(a)
o = HumanLetter("お")
self.take_a_photo(o)
yo = HumanLetter("よ")
self.take_a_photo(yo)
ri = HumanLetter("り")
self.take_a_photo(ri)
mo = HumanLetter("も")
self.take_a_photo(mo)
self.take_a_photo(a)
self.take_a_photo(o)
self.take_a_photo(i)
if __name__ == '__main__':
h = Main()
h.main()
- コストのかかるインスタンス化(ここでは人を並び替えて文字を形成するコンストラクタ。 本来ならDBアクセスなどが考えられれる。)の回数を少しへらすことができた。 当然インスタンスの数も減っている。
- Flyweight パターンで更に減らす
- 軽量化されるべきインスタンスの生成や管理を行う Factory クラスを作成し、 軽量化されるべきインスタンスは、この Factory クラスを介して取得するようにする
# -*- coding:utf-8 -*-
class HumanLetter:
def __init__(self, letter):
self._letter = letter
def get_letter(self):
return self._letter
class HumanLetterFactory:
__singleton = None
__human_letter = None
def __new__(cls, *args, **kwargs):
if cls.__singleton is None:
cls.__singleton = super(HumanLetterFactory, cls).__new__(cls)
return cls.__singleton
def __init__(self):
self.dic = {}
def get_human_letter(self, letter: str):
try:
human_letter = self.dic[letter]
except KeyError:
human_letter = HumanLetter(letter)
self.dic[letter] = human_letter
return human_letter
class Main:
@staticmethod
def take_a_photo(letter: HumanLetter):
"""写真を取る"""
print(letter.get_letter())
def main(self):
"""一文字を作成する"""
# singleton作成
hlf = HumanLetterFactory()
a = hlf.get_human_letter("あ")
self.take_a_photo(a)
print(a)
i = hlf.get_human_letter("い")
self.take_a_photo(i)
print(i)
ha = hlf.get_human_letter("は")
self.take_a_photo(ha)
a2 = hlf.get_human_letter("あ")
self.take_a_photo(a2)
print(a2)
o = hlf.get_human_letter("お")
self.take_a_photo(o)
print(o)
yo = hlf.get_human_letter("よ")
self.take_a_photo(yo)
ri = hlf.get_human_letter("り")
self.take_a_photo(ri)
mo = hlf.get_human_letter("も")
self.take_a_photo(mo)
a3 = hlf.get_human_letter("あ")
self.take_a_photo(a3)
print(a3)
o2 = hlf.get_human_letter("お")
self.take_a_photo(o2)
print(o2)
i2 = hlf.get_human_letter("い")
self.take_a_photo(i2)
print(i2)
if __name__ == '__main__':
h = Main()
h.main()
あ
<__main__.HumanLetter object at 0x1039c4da0> #「あ」で共通のインスタンスになっている
い
<__main__.HumanLetter object at 0x1039c4dd8>
は
あ
<__main__.HumanLetter object at 0x1039c4da0> #「あ」で共通のインスタンスになっている
お
<__main__.HumanLetter object at 0x1039c4e48>
よ
り
も
あ
<__main__.HumanLetter object at 0x1039c4da0> #「あ」で共通のインスタンスになっている
お
<__main__.HumanLetter object at 0x1039c4e48>
い
<__main__.HumanLetter object at 0x1039c4dd8>
- Factory クラスを Singleton にすることで、複数の Factory クラスが生成されてしまうことを防ぐ
- HumanLetterFactory クラスの get_human_letter メソッドでは、管理している dict に照会し、すでに持っている 文字を表すインスタンスが必要な際は、わざわざ新しく作り直すことはない
- もっていない文字を求められた場合は、新たにインスタンス化し、map に登録の上、生成したインスタンスを返す
- Flyweight パターンを利用することで、どのインスタンスを持っているのかなどを、利用者(Main クラスのような呼び出し側) で把握しておく必要がなくなる。また、複数の呼び出し先からの要求にも無駄なく応えることが可能となる。
- 注意
- Flyweight パターンは、ひとつのインスタンスを各所で共有することになりため、 共有されるインスタンスが本質的(intrinsic)な情報のみを持つ場合にのみ利用するべき。
- DBのコネクションとか
- 非本質的(extrinsic)な情報は、どこかの誰かが変更する可能性のあるものであり、 共有するインスタンスを勝手に誰かが変更することで、そこかしこに影響を与えることになりかねない。
- Flyweight パターンは、ひとつのインスタンスを各所で共有することになりため、 共有されるインスタンスが本質的(intrinsic)な情報のみを持つ場合にのみ利用するべき。