2021

12

30

GoFデザインパターンを復習してやんよ!!!(構造パターン②)

タグ: |

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

GoFデザインパターンを復習中。
前回は構造パターンの4つ(Adapter, Bridge, Composite, Decorator)を掘り下げたので、今回は残りの3つ(Facade, Flyweight, Proxy)を掘り下げ

Facadeパターン

  • 既存のクラスを複数組み合わせて使う手順を、「窓口」となるクラスを作ってシンプルに利用できるようにするパターン
  • 複雑なサブシステムに対して外側で限定的でわかりやすいインターフェースが必要な場合に機能へのショートカットを提供する役割
  • 読みはファサードでフランス語を語源とする単語で「建物の正面」という意味。

推奨されるケース

  • 複雑に絡み合ってごちゃごちゃしたサブシステム同士とコンポーネントへの直接的な依存を最小限に抑えたい(または既に提供しているものよりも単純な物にしたい)
  • 複数のサブシステムと直接依存するインスタンスを唯一のものに限定させたい

サンプル

在庫確認→決済処理→口座入金をまとめて「決済ボタン」を窓口にさせる

from __future__ import annotations

# サブシステム窓口クラス
class Facade:
    def __init__(self, subsystem1: Subsystem1, subsystem2: Subsystem2, subsystem3: Subsystem3) -> None:
        self._subsystem1 = subsystem1 or Subsystem1()
        self._subsystem2 = subsystem2 or Subsystem2()
        self._subsystem3 = subsystem3 or Subsystem3()

    def operation(self) -> str:
        results = []
        results.append("facade管理:サブシステムを初期化")
        results.append(self._subsystem1.operation1())
        results.append(self._subsystem2.operation1())
        results.append(self._subsystem3.operation1())
        results.append("facade管理:サブシステム実行")
        results.append(self._subsystem1.operation_n())
        results.append(self._subsystem2.operation_z())
        results.append(self._subsystem3.operation_z())
        return "\n".join(results)

# 在庫管理クラス
class Subsystem1:
    def operation1(self) -> str:
        return "在庫確認:準備"

    def operation_n(self) -> str:
        return "在庫確認:実行"

# 決済処理クラス
class Subsystem2:
    def operation1(self) -> str:
        return "決済処理:準備"

    def operation_z(self) -> str:
        return "決済処理:実行"

# 口座入金クラス
class Subsystem3:
    def operation1(self) -> str:
        return "口座入金:準備"

    def operation_z(self) -> str:
        return "口座入金:実行"

# 「決済ボタン」
def client_code(facade: Facade) -> None:
    print(facade.operation(), end="")


if __name__ == "__main__":
    subsystem1 = Subsystem1()   # 在庫管理サブシステムのインスタンス
    subsystem2 = Subsystem2()   # 決済処理サブシステムのインスタンス
    subsystem3 = Subsystem3()   # 口座入金サブシステムのインスタンス
    facade = Facade(subsystem1, subsystem2, subsystem3)   # facadeクラスでサブシステムのインターフェース管理
    print("---------------------------------------")
    client_code(facade)     # クライアントが「決済ボタン」を実行
---------------------------------------
facade管理:サブシステムを初期化
在庫確認:準備
決済処理:準備
入金処理:準備
facade管理:サブシステム実行
在庫確認:実行
決済処理:実行
入金処理:実行

メリット

  • サブシステムのコードとインターフェースを分離できるのでインターフェース分離の原則を保てる
  • 紐づくクラスの拡張が柔軟にできるようになるのでOpen/Closedの原則を保てる

デメリット

  • インターフェースとロジックが分離されていない既存コードに挿入する場合は実装が複雑になる
  • サブシステムがFacadeクラスに集中しすぎるとクラス同士の依存度が一気に高くなるので、1つのFacadeクラスに必要以上に機能を詰め込まない

Adapterパターンとの違い

  • Adapter → 互換性のないインターフェイスを持つオブジェクト同士をクライアントが期待する別のインターフェースへの変換先としてラップする手段
  • Facade → オブジェクトのサブシステム全体を処理する統合インターフェースのみを提供する

Flyweightパターン

  • 等価なインスタンスを別々の箇所で使用する際に、一つのインスタンスを再利用することによってプログラムを省リソース化することを目的
  • 多くの小さなオブジェクト実行に使用されるメモリ消費を減らし最適化する役割
  • ボクシングの「フライ級」の様に重いインスタンスを軽する意味合いで、複数のインスタンス作成を制限させるSingletonの応用に近い

推奨されるケース

  • アプリケーションが多数のオブジェクトを使用しメモリ割り当てを考慮する必要がある場合
  • メモリを大量に消費する重いオブジェクトが繰り返し作成される場合
  • オブジェクトの多くのグループをいくつかの共有オブジェクトに置き換えることができる場合

サンプル

とあるゲームでプレイヤーがキャラクターオブジェクトを自由に画面に配置できる機能

import json
from typing import Dict

# 共通部分の組み込みクラス
class Flyweight():
    def __init__(self, shared_state: str) -> None:
        self._shared_state = shared_state

    def operation(self, unique_state: str) -> None:
        s = json.dumps(self._shared_state, ensure_ascii=False)
        u = json.dumps(unique_state, ensure_ascii=False)
        print(f"【Flyweight作成】: 共有プロパティ ({s}) |ユニークプロパティ ({u})", end="")


# Flyweightオブジェクトの作成・管理クラス
class FlyweightFactory():
    _flyweights: Dict[str, Flyweight] = {}

    def __init__(self, initial_flyweights: Dict) -> None:
        for state in initial_flyweights:
            self._flyweights[self.get_key(state)] = Flyweight(state)
    # 追加オブジェクトのキー情報チェック
    def get_key(self, state: Dict) -> str:
        return "_".join(sorted(state))

    # 追加オブジェクトの作成or複製実行
    def get_flyweight(self, shared_state: Dict) -> Flyweight:
        key = self.get_key(shared_state)
        if not self._flyweights.get(key):
            print("=====================")
            print("【Flyweight管理】:同オブジェクトが見つからないため、新規作成")
            self._flyweights[key] = Flyweight(shared_state)
            print("=====================")
        else:
            print("=====================")
            print("【Flyweight管理】:既存オブジェクトのため、再利用")
            print("=====================")
        return self._flyweights[key]

    # 現在の総キャラ数チェック
    def list_flyweights(self) -> None:
        count = len(self._flyweights)
        print(f"現在、{count}個のFlyweightオブジェクトを所持")
        print("\n".join(map(str, self._flyweights.keys())), end="")

# クライアントの追加操作
def add_create_obj(
    factory: FlyweightFactory, plates: str, owner: str,
    brand: str, model: str, color: str
) -> None:
    print("\n\n【クライアント操作】:オブジェクトを追加リクエスト")
    flyweight = factory.get_flyweight([brand, model, color])
    flyweight.operation([plates, owner])


if __name__ == "__main__":
    # 画面に初期配置させているキャラクターオブジェクト
    factory = FlyweightFactory([
        ["通行人", "男", "成人"],
        ["通行人", "男", "子供"],
        ["店員", "女", "成人"],
        ["客", "男", "老人"],
        ["客", "女", "成人"],
    ])

お客さんの行動パターンの老人男性、名前「山田太郎」さん(プレイヤー操作可能)を追加

# 新規オブジェクトをゲーム画面に追加
add_create_obj(factory, "true", "山田太郎", "客", "男", "老人")

# Flyweightオブジェクト一覧
factory.list_flyweights()
【クライアント操作】:オブジェクトを追加リクエスト
【Flyweight管理】:既存オブジェクトのため、再利用
【Flyweight作成】: 共有プロパティ (["客", "男", "老人"]) |ユニークプロパティ (["true", "山田太郎"])

--------------------------------------
現在、5個のFlyweightオブジェクトを所持
成人_男_通行人
子供_男_通行人
女_店員_成人
客_男_老人
女_客_成人
--------------------------------------
  • 「客」「老人」「男性」のキャラは既に画面に生成済みなのでオブジェクトは再利用
  • 新キャラは再利用したままなのでFlyweightオブジェクトは5個のまま

さらに通行人の行動パターンの成人女性、名前「佐藤花子」さん(プレイヤー操作不可)を追加

# 新規オブジェクトをゲーム画面に追加
add_create_obj(factory, "false", "佐藤花子", "通行人", "女", "成人")

# Flyweightオブジェクト一覧
factory.list_flyweights()
【クライアント操作】:オブジェクトを追加リクエスト
【Flyweight管理】:同オブジェクトが見つからないため、新規作成
【Flyweight作成】: 共有プロパティ (["通行人", "女", "成人"]) |ユニークプロパティ (["false", "佐藤花子"])

--------------------------------------
現在、6個のFlyweightオブジェクトを所持
成人_男_通行人
子供_男_通行人
女_店員_成人
客_男_老人
女_客_成人
女_成人_通行人
--------------------------------------
  • 「通行人」「成人」「女性」のキャラはまだ画面に生成されていないので新規追加
  • 新キャラを追加したのでFlyweightオブジェクトは6個の増加

メリット

  • 重いオブジェクトを共有することでメモリ使用量を削減&データキャッシュの改善
  • 重いオブジェクトの数を減らせるので、パフォーマンスが向上する

デメリット

  • 複製による再利用が前提なので、Flyweightから作成されたオブジェクトの変更はできない前提としてクラスの設計が必要
  • 複製されたオブジェクトはメモリ上では保存されないため、各データの振る舞いを考慮する必要があり、保存や可変が必要な場合ははじめから共通的な情報を保持させたSingletonオブジェクトをProxyパターンで複製処理するか、別途外部に譲渡の必要がある
  • 近年のマシンスペック向上やブラウザやコンパイルツールでは既に最適化された同様の手法が実装されているため、コード内部レベルでFlyweightを採用するに値する”重いオブジェクト”かつ”フィールドの共有可能/不可能”の識別コストがかかる(場合によっては最初から重くならないオブジェクトを設計することに注力した方が早い)

Proxyパターン

  • 別のオブジェクトの代替またはプレースホルダー(仮の雛形)を提供できるパターン
  • オブジェクトを要求/アクセスしたときにのみ作成され、基になるオブジェクトへのプレースホルダーインターフェイスを提供させる役割

推奨されるケース

  • 実態オブジェクトの実行タイミングの前後で遅延初期化、ロギング、アクセス制御、キャッシングなどを行いたい場合
  • 実態オブジェクトの実行サーバーとリクエスト/レスポンス処理のサーバーがそれぞれ別かつ複数ある場合
  • 逆に1箇所への大量のシステムリソース消費を分散させるためにリクエストを委任させたい場合

サンプル

リクエスト処理前にプロキシクラスで身元確認&ログ保存を実装

from abc import ABC, abstractmethod

# 主体監視オブジェクトクラス
class Subject(ABC):
    @abstractmethod
    def request(self) -> None:
        pass

# 実際の主体オブジェクトクラス
class RealSubject(Subject):
    def request(self) -> None:
        print("実態オブジェクト:リクエストを処理")

# 代理プロキシクラス
class Proxy(Subject):
    def __init__(self, real_subject: RealSubject) -> None:
        self._real_subject = real_subject
    # リクエスト処理
    def request(self) -> None:
        if self.check_access():
            self._real_subject.request()
            self.log_access()
    # アクセス確認
    def check_access(self) -> bool:
        print("代理:実行する前にアクセス元情報をチェック")
        return True

    # アクセスログ
    def log_access(self) -> None:
        print("代理:アクセス時間をログに保存", end="")

# クライアントの実行クラス
def client_code(subject: Subject) -> None:
    subject.request()


if __name__ == "__main__":
    print("【クライアント:実際のコードで実行】")
    real_subject = RealSubject()
    client_code(real_subject)

    print("-------------------------------------------------")

    print("【クライアント:プロキシを使用して同じクライアントコードを実行】")
    proxy = Proxy(real_subject)
    client_code(proxy)
【クライアント:実際のコードで実行】
実態オブジェクト:リクエストを処理
-------------------------------------------------
【クライアント:プロキシを使用して同じクライアントコードを実行】
代理:実行する前にアクセス元情報をチェック
実態オブジェクト:リクエストを処理
代理:アクセス時間をログに保存

メリット

  • サービスオブジェクトをコントロールできることで健全なライフサイクルを管理できる
  • インターフェースやサブシステムを変更せずに、新しいプロキシを導入できるのでOpen/Closedの原則を保てる

デメリット

  • 代理処理が入るのでクラス実行の応答が必然的に遅くなるのでレスポンスファーストな機能への実装には向かない

構造パターンまとめ

パターン名採用シチュエーション
AdapterサードパーティーAPIなどの内部と互換性のないクラスとオブジェクトを何とかしたい
Bridge機能側と実装側の組み合わせパターンが複数あるクラスとオブジェクト間の関係を何とかしたい
Composite限定的な範囲を対象に大小関わらず再帰処理が必要なモノを何とかしたい
Decorator既に完成された(または変更ができない)クラスから実行されたオブジェクトを何とかしたい
Facade複数のサブシステムをまたがるショートカットまたはインターフェースが別途欲しい
Flyweightボトルネックになるモノを何とかしたい
Proxy実態クラスの実行前後に色々したい or 集中的な処理をコントロールしたい

Decorator以降は構造パターンという”手法”よりも使い所がかなり具体的なデコレータやラッパー的な”手段”に近いなという印象

実装先がWEBアプリの場合、この構造パターンのほとんどがFWのデフォ機能に内包されていると思うので、ATMや自動改札のような実装形態が予め完成されてない業務システム開発に柔軟なJAVAでの開発の際の先人の知恵であって、この手法を無理やりに現在のWEB系に導入させようとすると色んな無駄と混乱の巻き込み事故になりかねないと思われる。

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

トップへ