2022

2

13

GoFデザインパターンを復習してやんよ!!!(振る舞いパターン②)

タグ: |

Pocket
LINEで送る
Facebook にシェア
reddit にシェア
LinkedIn にシェア

現在、GoFデザインパターンを復習中。
前回までは生成パターンと構造パターンを掘り下げ

今回からは振る舞いパターンの残り6つ(Observer, State, Strategy, Template Method, Visitor)


Observerパターン

  • 監視しているオブジェクトに発生するイベントについて複数のオブジェクトに通知するサブスクリプションメカニズムを定義できる動作設計パターン
  • 「多対多」の密結合の相互作用を調整し、オブジェクトが同士に明示的な参照をさせない役割

推奨されるケース

  • 特定のオブジェクトの状態を長期的に監視したい、または変化を通知させたい場合

サンプル

通信販売サイトの商品状態をユーザーに通知する例

from __future__ import annotations
from abc import ABC, abstractmethod
from random import randrange
from typing import List

# 通知設定クラス
class Subject(ABC):
    # 設定ON
    @abstractmethod
    def attach(self, status, observer: Observer) -> None:
        pass
    # 設定OFF
    @abstractmethod
    def detach(self, status, observer: Observer) -> None:
        pass
    # ユーザー通知
    @abstractmethod
    def notify(self) -> None:
        pass

# 具象通知設定クラス
class ConcreteSubject(Subject):
    _state: int = None
    _observers: List[Observer] = []
    # 設定ON
    def attach(self, status, observer: Observer) -> None:
        print(f"{status}: 通知設定をON")
        self._observers.append(observer)
    # 設定OFF
    def detach(self, observer: Observer) -> None:
        self._observers.remove(observer)
    # ユーザー通知
    def notify(self) -> None:
        print("オブザーバーへ通知")
        for observer in self._observers:
            observer.update(self)
    # ビジネスロジックフラグ
    def some_business_logic(self, status) -> None:
        print("通販サイト稼働中…")
        self._state = status
        print(f"商品状態の変更: {self._state}")
        self.notify()


# オブザーバー(監視者)クラス
class Observer(ABC):
    @abstractmethod
    def update(self, subject: Subject) -> None:
        pass

# 新商品入荷クラス
class ConcreteObserverA(Observer):
    def update(self, subject: Subject) -> None:
        if subject._state == 0:
            print("【●通知】: 新商品が入荷しました\n")

# 在庫数クラス
class ConcreteObserverB(Observer):
    def update(self, subject: Subject) -> None:
        if subject._state == 1:
            print("【●通知】: 在庫が残り5点となりました\n")

# 完売商品再入荷クラス
class ConcreteObserverC(Observer):
    def update(self, subject: Subject) -> None:
        if subject._state == 2:
            print("【●通知】: 在庫切れの商品が入荷しました\n")


if __name__ == "__main__":
    print("------ ユーザー設定 -----")
    subject = ConcreteSubject()
    # 新商品通知をON
    observer_a = ConcreteObserverA()
    subject.attach("【新商品】", observer_a)
    # 在庫数通知をON
    observer_b = ConcreteObserverB()
    subject.attach("【在庫数】", observer_b)
    # 再入荷通知をON
    observer_c = ConcreteObserverC()
    subject.attach("【再入荷】", observer_c)

    print("\n")
    subject.some_business_logic(0)    # 新商品入荷
    subject.some_business_logic(1)    # 在庫数

    print("------ 【新商品】通知設定を解除 -----")
    subject.detach(observer_a)    #  【新商品】通知設定を解除
    subject.some_business_logic(0)    # 新商品入荷
    print("\n")

    subject.some_business_logic(2)    # 再入荷
------ ユーザー設定 -----
【新商品】: 通知設定をON
【在庫数】: 通知設定をON
【再入荷】: 通知設定をON

通販サイト稼働中…
商品状態の変更: 0
オブザーバーへ通知
【●通知】: 新商品が入荷しました

通販サイト稼働中…
商品状態の変更: 1
オブザーバーへ通知
【●通知】: 在庫が残り5点となりました

------ 【新商品】通知設定を解除 -----
通販サイト稼働中…
商品状態の変更: 0
オブザーバーへ通知

通販サイト稼働中…
商品状態の変更: 2
オブザーバーへ通知
【●通知】: 在庫切れの商品が入荷しました

メリット

  • 発信者クラスのコードを変更せずに新しい観察者クラスを導入できるのでオープン/クローズド原則を保てる
  • 状態を保持するクラスと、状態の通知を受け取るクラスを分離でき再利用性が上がる

デメリット

  • 登録されたオブザーバーの数が非常に多い場合、多くの処理コストがかかる
  • どのオブザーバーへ通知されているかの順序やステータスを明らかにしないケースが多いので特定のオブザーバーへの不具合の対処が難しくなる場合がある

Mediatorとの違い

  • コンポーネント通信の分離系統のMediatorとObserverの違い
  • Mediatorは特定のクラスから作成されたオブジェクト間の直接的な通信の組み合わせを「仲介者」を通して一元化して間接的に参照させる
    • オブジェクト間の相互作用をカプセル化して相互の結合を緩くする
Mediator図
  • Observerは1つのオブジェクトの「発行者」と複数のイベント「観察者」を構成し、分離されたインターフェースを通して通信を分散させる
    • オブジェクト間の繋がりは一方的でインターフェースは分離されている
  • 通信先のコンポーネントがビジネスフローの一部となるかならないか
  • 異なるクラスのインスタンスと分離しているので再利用を前提とする場合はObserverの方が容易

Stateパターン

  • オブジェクトの内部状態が変化したときにオブジェクトの動作を変更できるようにする動作設計パターン
  • ライフサイクル中にさまざまな状態になる可能性のあるオブジェクトに振る舞いの変化を記述せずに振る舞いを変えやすくする用途

推奨されるケース

  • if / elseまたはswitch / case条件付きロジックへの依存などを使わずに異なるクラス内でオブジェクトの状態固有の動作を定義させたい場合

サンプル

天気の要求パターンによって傘と洗濯物の状態を変化させる例

class State():
    def umbrella(self):
        pass

    def laundry(self):
        pass

# 晴れの日クラス
class Sunnyday(State):
    def umbrella(self):
        print("傘は持たない")

    def laundry(self):
        print("屋外に干す")

# 曇りの日クラス
class Cloudyday(State):
    def umbrella(self):
        print("折りたたみ傘を持参")

    def laundry(self):
        print("屋内で乾かす")

# 雨の日クラス
class Rainyday(State):
    def umbrella(self):
        print("通常の傘を持参")

    def laundry(self):
        print("屋内で乾かす")


class Context:
    def __init__(self):
        self.sunny = Sunnyday()
        self.rainy = Rainyday()
        self.cloudy = Cloudyday()
        self.state = self.sunny

    def change_state(self, weather):
        if weather == "sunny":
            self.state = self.sunny
        elif weather == "rain":
            self.state = self.rainy
        elif weather == "cloud":
            self.state = self.cloudy
        else:
            raise ValueError("change_stateメソッドは次のようになっている必要があります {}".format(["sunny", "rain", "cloud"]))

    def umbrella(self):
        self.state.umbrella()

    def laundry(self):
        self.state.laundry()


if __name__ == "__main__":
    # インスタンス化
    obj = Context()

    print("---------------------------")
    # 雨の日パターンを実行
    obj.change_state("rain")
    obj.umbrella()
    obj.laundry()

    print("---------------------------")
    # 晴れの日パターンを実行
    obj.change_state("sunny")
    obj.umbrella()
    obj.laundry()

    print("---------------------------")
    # 嵐の日パターンを実行
    obj.change_state("storm")
    obj.umbrella()
    obj.laundry()
---------------------------
通常の傘を持参
屋内で乾かす
---------------------------
傘は持たない
屋外に干す
---------------------------
# 嵐の日(storm)はパターンに設定していないので
ValueError: change_stateメソッドは次のようになっている必要があります ['sunny', 'rain', 'cloud']

メリット

  • 特定の状態に関連するコードを個別のクラスに編成できるので単一責任の原則を保てる
  • 既存の状態クラスやコンテキストを変更せずに、新しい状態を導入できるのでオープン/クローズド原則を保てる

デメリット

  • 複数のstateがある場合は、stateごとにクラスが必要であり、プログラム全体が複雑になりすぎる可能性がある
  • 多くのオブジェクトがそれぞれが独自のstateを持っていてマルチスレッドで呼び出すような場合、メモリを大量に消費してしまう可能性がある

Strategyパターン

  • 戦略部分(アルゴリズム)をクラス単位で定義することで、切り替えや拡張を容易にする動作設計パターン
  • プログラムの動作を構成の詳細やユーザーの好みに応じて動的に変更させる役割

推奨されるケース

  • オブジェクト内で1つのアルゴリズム実行時に、別のアルゴリズムに切り替えをさせたい場合
  • 一部の動作の実行方法のみが異なる類似のクラスが多数ある場合
  • クラスのビジネスロジックをアルゴリズムの実装から分離させたい場合

サンプル

アルゴリズムを切り替えるサンプル

from abc import ABCMeta, abstractmethod


class Strategy(metaclass=ABCMeta):
    @abstractmethod
    def run(self):
        pass

# アルゴリズム1パターンクラス
class StrategyAlgorithm1(Strategy):
    def run(self):
        print('アルゴリズム1での処理')

# アルゴリズム2パターンクラス
class StrategyAlgorithm2(Strategy):
    def run(self):
        print('アルゴリズム2での処理')


class Context:
    def __init__(self, strategy):
        self.strategy = strategy

    def set_strategy(self, strategy):
        self.strategy = strategy

    def do_strategy(self):
        self.strategy.run()


if __name__ == '__main__':
    # 初期アルゴリズムを設定
    c = Context(StrategyAlgorithm1())
    c.do_strategy()
    # アルゴリズムを切り替え
    c.set_strategy(StrategyAlgorithm2())
    c.do_strategy()
アルゴリズム1での処理
アルゴリズム2での処理

メリット

  • アルゴリズムを個別にカプセル化して、コンテキストに影響を与えずに実装できるのでオープン/クローズド原則を保てる
  • アルゴリズムをstrategyクラスにカプセル化してインターフェースと分離できるので、switchやif-elseのステートメントを使わずに実装しやすい

デメリット

  • 用いるアルゴリズムの数が少なく、ほとんど変更されない場合は無駄に複雑になる
  • クライアント側が実装された異なるアルゴリズムを理解している必要がある

Template Methodパターン

  • 処理の共通部分を抽象クラスに抽出し、固有の処理を具象クラスで実装する動作設計パターン
  • ただ共通化出来る処理を抽象化するのではなく、似たような処理の流れの処理を共通化させる役割

推奨されるケース

  • インターフェースとコンポーネント設計は異なるが、類似点が多い部分の再利用をしたい場合
  • 既にアルゴリズムが実装されているクラスの変更はできないが、選択的にオーバーライドさせたい場合
  • 同じような振る舞いを持つクラスを抜き出して共通のクラスに局所化させたい場合

サンプル

タイトルと本文をプレーンテキスト形式とHTML形式の2パターンのテンプレートで出力

from abc import ABCMeta, abstractmethod

class AbstractReport(metaclass=ABCMeta):

   def __init__(self, title, text_list):
       self.title = title
       self.text_list = text_list

   @abstractmethod
   def pprint(self):
       pass

   @abstractmethod
   def start(self):
       pass

   @abstractmethod
   def end(self):
       pass

   def display(self):
       self.start()
       self.pprint()
       self.end()


class PlaneTextReport(AbstractReport):
   def pprint(self):
       for text in self.text_list:
           print(text)

   def start(self):
       title = "**** #"+self.title+" ****"
       print(title)

   def end(self):
       pass


class HtmlReport(AbstractReport):
   def pprint(self):
       for text in self.text_list:
           print('<p>'+text+'</p>')

   def start(self):
       title = "<html><head>"
       title += "<title>"+self.title+"</title>"
       title += "</head><body>"
       print(title)

   def end(self):
       title = "</body></html>"
       print(title)

if __name__ == '__main__':
    title = "タイトル"
    text = [
        "1行目",
        "2行目",
        "3行目"
    ]

    # プレーンテキスト形式
    plane_report = PlaneTextReport(title, text)
    plane_report.display()
    print()

    # HTML形式
    html_report = HtmlReport(title, text)
    html_report.display()
    print()
**** #タイトル ****
1行目
2行目
3行目

<html><head><title>タイトル</title></head><body>
<p>1行目</p>
<p>2行目</p>
<p>3行目</p>
</body></html>

他パターンとの関係

Strategyとの違い

  • Template Methodは継承を使用してサブクラスでアルゴリズムの一部を変更して拡張する
  • Strategyは委任を使用してアルゴリズム全体を変更して実行時に動作を切り替える

Factory Methodとの違い

  • Factory Methodはオブジェクト生成自体を委譲する作成手段
    • オブジェクト生成のための抽象メソッドと、生成されたオブジェクトを利用する別のメソッドを定義し、生成メソッドの実装をサブクラスに任せる
    • 実際に作成するオブジェクトには依存せず、オブジェクトのインターフェースを提供
  • Template Methodはアルゴリズムを実装させる実行手段
    • 振る舞いのための抽象メソッドを定義し、別のメソッドがその抽象メソッドを呼び出して振る舞いを実行させるパターン

メリット

  • アルゴリズムは変更のために閉じられていますが、サブクラスによる拡張のために開かれているのでオープン/クローズド原則を保てる
  • サブクラスによってオーバーライドさせるので他への影響が少なくなり、全体的な設計がクリーンになる

デメリット

  • 適用対象とする処理アルゴリズムのステップを理解している必要がある
  • サブクラスが処理の流れを継承してしまい、入力パターンが基本クラスよりも厳しくあるべき条件から外れてリスコフの置換原則に反する場合がある(”継承よりも委譲”というのがデザパタの全体的な推奨ケース)
  • 無意識な形で用いられる事が多いパターンのため、設計段階でTemplate Method頼みになると子クラスの数が膨大になってしまい、全体図がわかりくくなる場合がある

Visitorパターン

  • 動作するオブジェクトからアルゴリズムを分離できる動作設計パターン
  • データ構造の中をめぐり歩くVisitorクラスに処理を任せ、処理の追加を簡単にする用途

推奨されるケース

  • アプリケーション内にデータ構造があり、いくつかのアルゴリズムがそのデータ構造にアクセスする必要がある場合
  • データ構造の中に多くの要素が格納されており、その各要素に対して、何らかの処理をしたい場合

サンプル

社員が”会社へ通勤”と”取引先へ訪問”、またはその逆の”会社が社員を受け入れ”と”取引先が客を受け入れ”をまとめるサンプル

from abc import ABC, abstractmethod

# 訪問先抽象クラス
class Acceptor(ABC):
    @abstractmethod
    def accept(self, visitor):
        pass

# 会社クラス
class Company(Acceptor):
    def __init__(self):
        self.__name = "会社"
        self.__action = "出勤しました"

    def getName(self) -> str:
        return self.__name

    def getAction(self) -> str:
        return self.__action

    def accept(self, visitor) -> None:
        visitor.visit(self)

# 取引先クラス
class Suppliers(Acceptor):
    def __init__(self):
        self.__name = "取引先"
        self.__action = "営業に向かいました"

    def getName(self) -> str:
        return self.__name

    def getAction(self) -> str:
        return self.__action

    def accept(self, visitor) -> None:
        visitor.visit(self)


# 訪問者抽象クラス
class Visitor(ABC):
    @abstractmethod
    def visit(self, acceptor) -> None:
        pass


# 訪問者Aクラス
class VisitorA(Visitor):
    def __init__(self):
        self.__name = "佐藤さん"

    def visit(self, accept: Acceptor) -> None:
        print(f"{self.__name}が{accept.getName()}に{accept.getAction()}")


# 訪問者Bクラス
class VisitorB(Visitor):
    def __init__(self):
        self.__name = "鈴木さん"

    def visit(self, accept: Acceptor) -> None:
        print(f"{self.__name}が{accept.getName()}に{accept.getAction()}")



if __name__ == "__main__":
    Company().accept(VisitorA())    # "会社" に "訪問者A" が訪問
    VisitorA().visit(Company())    # "訪問者A" が "会社" に訪問
    
    Suppliers().accept(VisitorA())       # "取引先" に "訪問者A" が訪問
	  VisitorA().visit(Suppliers())      # "訪問者A" が "取引先" に訪問

    print("-" * 50)

    Company().accept(VisitorB())    # "会社" に "訪問者B" が訪問
    VisitorB().visit(Company())    # "訪問者B" が "会社" に訪問

    Suppliers().accept(VisitorB())       # "取引先" に "訪問者B" が訪問
    VisitorB().visit(Suppliers())      # "訪問者B" が "取引先" に訪問
佐藤さんが会社に出勤しました
佐藤さんが会社に出勤しました
佐藤さんが取引先に営業に向かいました
佐藤さんが取引先に営業に向かいました
--------------------------------------------------
鈴木さんが会社に出勤しました
鈴木さんが会社に出勤しました
鈴木さんが取引先に営業に向かいました
鈴木さんが取引先に営業に向かいました
  • 訪問するvisit()メソッドも、受け入れるaccept()メソッドも意味合いは異なるが、振る舞いは同じ
  • 会社となるCompanyクラスと、取引先となるSuppliersクラスのacceptメソッドでVisitorクラスの引数を持ち、visitメソッドを呼び出しす
  • Visitorクラスを実装する事で、訪問者の具象クラス(佐藤&鈴木)側はaccept()メソッド1つを実装するだけでvisit()メソッドと同じ結果を呼び出す事が可能になる
  • CompanyクラスとSuppliersクラス側も振る舞いに影響されなくなり、データ構造のみの構成が可能になる
Pocket
LINEで送る
Facebook にシェア
reddit にシェア
LinkedIn にシェア

トップへ