2021

12

5

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

タグ: |

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

前回からGofデザインパターンを復習中。
今回は構造パターン7種のうちのAdapter, Bridge, Composite, Decoratorの4つを掘り下げる

Adapterパターン

  • 互換性のないインターフェイスを持つオブジェクトがコラボレーションできるようにする構造設計パターン
  • 自身では仕様変更ができない、またはそのままでは使えないサードパーティやレガシーのクラスを変換して利用するためWrapperパターンと呼ばれることもある
  • 継承を利用したパターンと委譲(コンポジション)を利用したパターンがある

継承によるパターン

# 自社アプリで使う通常の出力クラス(XML形式)
class ExportXML:
    def request(self) -> str:
        return "通常のXML形式で出力しますた"

# サードパーティから利用する出力クラス(JSON形式)
class ExportJSON:
    def specific_request(self) -> str:
        return "たすまし力出で式形NOSJの常通"

# サードパーティ出力を通常クラス用に変換するアダプタークラス
class Adapter(ExportXML, ExportJSON):
    def request(self) -> str:
        return f"{self.specific_request()[::-1]}"

# クライアントからの出力インターフェーズ(デフォはXMLのみ受け取り)
def client_code(target: "ExportXML") -> None:
    print(target.request(), end="\n\n")


if __name__ == "__main__":
    # XML出力インスタンス & 出力リクエスト
    xml = ExportXML()
    client_code(xml)
		# --> 「通常のXML形式で出力しますた」

    # JSON出力インスタンス& 直接実行
    json = ExportJSON()
    print(f"{json.specific_request()}", end="\n\n")
		# --> 「たすまし力出で式形NOSJの常通」

		# XMLクラスのみに限定しているのでエラー
	 client_code(json)

    # アダプター変換 & 出力リクエスト
    adapter = Adapter()
    client_code(adapter)
		# --> 「通常のJSON形式で出力しますた」
  • 通常のクライアント側でXML形式でしか出力できないインターフェースに対して AdapterクラスでJSON形式でも正しく出力できる様に整形する処理を継承させことで 既存のクライアントの出力インターフェースを修正することなく併用できる
  • ただこのパターンだと親と子で同じメソッド名を設定してしまうと競合して再帰エラーになってしまうのでユニークなメソッド名を持たせる手間が出てしまう

委譲によるパターン

class ExportXML:
    def request(self) -> str:
        return "通常のXML形式で出力しますた"


class ExportJSON:
    def specific_request(self) -> str:
        return "たすまし力出で式形NOSJの常通"

class ExportCSV:
    def specific_request(self) -> str:
        return "たすまし力出で式形VSCの常通"


class Adapter(ExportXML):
    def __init__(self, export) -> None:
        self.export = export

    def request(self) -> str:
        return f"{self.export.specific_request()[::-1]}"


def client_code(target: "ExportXML") -> None:
    print(target.request(), end="\n\n")


if __name__ == "__main__":
    # XML出力インスタンス&出力リクエスト
    xml = ExportXML()
    client_code(xml)
		# --> 「通常のXML形式で出力しますた」

    # JSON出力インスタンス& 直接実行
    json = ExportJSON()
    adapter = Adapter(json)
    client_code(adapter)
		# --> 「通常のJSON形式で出力しますた」

    # CSV形式でも出力
    csv = ExportCSV()
    adapter = Adapter(csv)
    client_code(adapter)
		# --> 「通常のJSON形式で出力しますた」

委譲パターンの場合はAdapterを実行するクラスを指定した上で同様の変換を処理できるので、
形式種類の追加も容易になる

メリット

  • インターフェイスまたはデータ変換コードを、プログラムの主要なビジネスロジックから分離できるので単一責任の原則を保てる
  • アダプタと連携する限り、既存のクライアントコードを壊すことなく、新しいタイプのアダプタをプログラムに導入できるのでオープン/クローズド原則も保てる

デメリット

  • 新しいインターフェイスとクラスのセットで導入する必要があるため、コードの全体的な複雑さが増してしまう

モヤモヤポイント

  • 委譲パターンの方が拡張性も高く、Adapterクラスに対する単一責務の原則に反するリスクがより少ないのでは?と思ったが、継承パターンである方が良いケースは思い浮かばなかった

Bridgeパターン

  • 機能部分(予定されているもの)と実装部分(実現する環境や手段)を分離して、それぞれを独立に変更、拡張、再利用することができるように工夫させる手法。別名Handle/Body パターン
  • Dockerデスクトップのように提供する機能は同じでも実行する環境がOS(Win、Mac、Linux)によって異なる場合に、同じコードではサポートしきれないケースに対応させるイメージ
  • Bridgeによって定義された一部の抽象化が特定の実装でのみ機能する関係をカプセル化し、クライアントコードから複雑さを隠せるAbstractFactory作成パターンと相性が良い

推奨されるケース

  • 抽出されたクラスとその実装を永続的に結合することを避けたい場合
  • 抽出されたクラスとその実装の両方を,サブクラスの追加により拡張可能にすべき場合
  • 抽出されたクラスの実装における変更が,クライアントに影響を与えるべきではない場合

サンプル

import random

class Display():
    """ 
		表示を扱う機能側の上位クラス(Bridgeの役目)
		環境側のインスタンスを受け取りそれを使って,呼んだクラスの固有の動作をする
		"""
    def __init__(self, impl) -> None:
        self.__impl = impl
    def open(self) -> None:
        self.__impl.raw_open()
    def print(self) -> None:
        self.__impl.raw_print()
    def close(self) -> None:
        self.__impl.raw_close()
    def display(self) -> None:
        self.open()
        self.print()
        self.close()


class CountDisplay(Display):
    """
		表示回数を指定した機能側のサブクラス
		"""
    def multi_display(self, times: int) -> None:
        self.open()
        for _ in range(times):
            self.print()
        self.close()


class RandomDisplay(CountDisplay):
    """
		表示回数がランダムに指定される機能側のサブクラス
		"""
    def random_display(self, times: int) -> None:
        self.multi_display(random.randint(0, times))


class DisplayImp(metaclass=ABCMeta):
    """実装側の表示管理クラス"""
    @abstractmethod
    def raw_open(self) -> None:
        pass
    @abstractmethod
    def raw_print(self) -> None:
        pass
    @abstractmethod
    def raw_close(self) -> None:
        pass


class StringDisplay(DisplayImp):
    """
		文字列を使用した表示を行うクラス
		実際に文字を出力している環境側の具象クラス
		"""
    def __init__(self, string: str) -> None:
        self.string = string
        self.width = len(string)
    def raw_open(self) -> None:
        self.print_line()
    def raw_print(self) -> None:
        print(f'|{self.string}|')
    def raw_close(self) -> None:
        self.print_line()
    def print_line(self) -> None:
        print('+', end='')
        for i in range(self.width):
            print('-', end='')
        print('+')


if __name__=='__main__':
		# 文字列出力クラスを継承した表示クラスで標準出力
    display = Display(StringDisplay('通常の表示'))
		display.display()
		
		# 文字列出力クラスを継承した表示回数クラスで5回出力
    count_display = CountDisplay(StringDisplay('回数指定で表示'))
    count_display.multi_display(5)

		# 文字列出力クラスを継承したランダム表示クラスで1〜10の間でランダム出力「
    random_count_display = RandomDisplay(StringDisplay('ランダム表示'))
    random_count_display.random_display(10)
  • 実装側にクラスが追加されたときは,呼んでいるクラス StringDisplay だけを替えればよい
  • 機能側にメソッドが追加されたときは,呼んでいるクラス CountDisplayRandomDisplay だけを替えればよい
+-----+
|通常の表示|
+-----+

+-------+
|回数指定で表示|
|回数指定で表示|
|回数指定で表示|
|回数指定で表示|
|回数指定で表示|
+-------+

+------+
|ランダム表示|
|ランダム表示|
+------+

メリット

  • プラットフォームに依存しないクラスとアプリを作成できるので単一責任の原則が保てる
  • 新しい抽象化と実装を互いに独立して導入できるのでオープン/クローズド原則も保てる

デメリット

  • ラッパースタックから特定のラッパーを削除するのは困難
  • その動作がデコレータスタック内の順序に依存しないような方法でデコレータを実装することは困難

Adapterパターンとの違い

関係のないクラス同士をつなぐことが目的という意味ではAdapterと似ているが、Adapterは設計が終わった後で適用させ、Bridgeは抽出されたクラスと実装を独立に変更可能にするために設計の前段階で使われる


Compositeパターン

  • オブジェクトをツリー構造に構成し、それらが個々のオブジェクトであるかのようにこれらの構造を操作できるようにするパターン
  • 推奨されるケース
    • ツリーのようなオブジェクト構造を実装する必要がある場合
    • クライアントコードで単純な要素も複雑な要素も両方を均一に処理する場合
  • ディレクトリ(フォルダ) > ファイルなどの多重の入れ子構造を作るイメージ

サンプル

class DataObject():
    """
		コンポーネント
    コンポジットとリーフの間の共通インターフェース
    """
    def read(self): pass

class DataNode(DataObject):
    """
    リーフノード(子)
    コンポジットではないデータを含む
    """
    def __init__(self, data):
        self._data = data

    def read(self):
        print("  ┗ ファイル: ", self._data)


class DataComposite(DataObject):
    """
    コンポジットノード(親)
    子ノードを追加・削除するためのインターフェース
    """
    def __init__(self, data):
        self._meta_data = data
        self.sub_objects = []

    def read(self):
        print("ディレクトリ: ", self._meta_data)
        for data_object in self.sub_objects:
            data_object.read()

    def add(self, data_object):
        self.sub_objects.append(data_object)

    def remove(self, data_object):
        self.sub_objects.remove(data_object)


if __name__ == "__main__":
    # 個別ファイル定義
    book_A1 = DataNode("プログラミング言語 Ruby")
    book_A2 = DataNode("Python クックブック 第2版")
    book_B1 = DataNode("JavaScript 第7版")
    # ディレクトリ定義
    book_A = DataComposite("バックエンド")
    book_B = DataComposite("フロントエンド")
    # 構造ルートの定義
    book_root = DataComposite("オライリー参考書")
    
    # 子ノードに本を追加
    book_A.add(book_A1)
    book_A.add(book_A2)
    book_B.add(book_B1)
    
    # 親ノードに分類別で追加
    book_root.add(tree_A)
    book_root.add(tree_B)
    
    # 現在の構造を一括出力
    tree_root.read()
ディレクトリ:  オライリー参考書
┗ ディレクトリ:  バックエンド
  ┗ ファイル:  プログラミング言語 Ruby
  ┗ ファイル:  Python クックブック 第2版
┗ ディレクトリ:  フロントエンド
  ┗ ファイル:  JavaScript 第7版

メリット

  • 複雑なツリー構造をより便利に操作できるのでポリモーフィズムと再帰処理に有効
  • 既存のコードを壊すことなく、新しい要素タイプをアプリに導入できるのでオープン/クローズド原則を保てる

デメリット

  • 機能が大きく異なるクラスに共通のインターフェースを提供する事が難しくなる
  • 場合によってはリーフクラスを空の場合でも動作するメソッドの作成コストや、特定タイプのみに機能を制限する実装が困難

Decoratorパターン

  • オブジェクトに対してデコレート(飾り付け)を行う方式
  • 使用するオブジェクトのコードを壊すことなく、実行時にオブジェクトに追加の動作を割り当てる必要がある、または継承を使用してオブジェクトの動作を拡張するのが難しい場合や不可能な場合は、このパターンが推奨される

サンプル

寒ければセーターを着させて、雨が降っていればさらにレインコートを着させられるが、いつでも脱ぐことができるイメージ

# ベースコンポーネント
class Component():
    def operation(self) -> str:
        pass

# 裸クラス
class ConcreteComponent(Component):
    def operation(self) -> str:
        return "裸一貫"

# デコレータークラス
class Decorator(Component):
    _component: Component = None
    def __init__(self, component: Component) -> None:
        self._component = component
    @property
    def component(self) -> str:
        return self._component
    def operation(self) -> str:
        return self._component.operation()

# セータークラス
class sweaterDecorator(Decorator):
    def operation(self) -> str:
        return f"セーター【{self.component.operation()}】"

# コートクラス
class coatDecorator(Decorator):
    def operation(self) -> str:
        return f"コート【{self.component.operation()}】"

# 出力インターフェース
def client_code(component: Component) -> None:
    print(f"{component.operation()}", end="\n\n")


if __name__ == "__main__":
    simple = ConcreteComponent()  
    client_code(simple) 

    # セーター -> コートを着用
    sweater = sweaterDecorator(simple)  # セーター着用
    coat = coatDecorator(sweater)     # コート着用
    client_code(coat)

    # 逆順に着用
    coat = coatDecorator(simple) 
    sweater = sweaterDecorator(coat)
    client_code(sweater)
裸一貫
コート【セーター【裸一貫】】
セーター【コート【裸一貫】】

メリット

  • 新しいサブクラスを作成せずにオブジェクトの動作を拡張できる
  • 実行時にオブジェクトに責任を追加または削除できる
  • オブジェクトを複数のデコレータにラップすることで、いくつかの動作を組み合わせることができる

デメリット

  • ラッパースタックから特定のラッパーを削除するのは困難
  • その動作がデコレータスタック内の順序に依存しないような方法でデコレータを実装することは困難
Pocket
LINEで送る
Facebook にシェア
reddit にシェア
LinkedIn にシェア

トップへ