2021

11

21

GoFデザインパターンを復習してやんよ!!!(生成関連パターン)

タグ: |

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

オブジェクト指向について復習していくと、行き着く先はデザインパターン
という事でデザパタについても掘り下げていきます。

GoFデザインパターン

ソフトウェア開発におけるデザインパターンまたは設計パターン(英: design pattern)とは、過去のソフトウェア設計者が発見し編み出した設計ノウハウを蓄積し、名前をつけ、再利用しやすいように特定の規約に従ってカタログ化したものである。

Wikipediaより

つまり大体の設計パターンは先人が発見したノウハウのいずれかのパターンに当てはまるので、そのコンセプトに沿って開発を進めるための解決策がもう既に用意されている

書籍『オブジェクト指向における再利用のためのデザインパターン』において、GoF (Gang of Four) と呼ばれる4人の共著者は、デザインパターンという用語を初めてソフトウェア開発に導入した。
GoFは、エーリヒ・ガンマ、リチャード・ヘルム、ラルフ・ジョンソン、ジョン・ブリシディースの4人である。
彼らは、その書籍の中で23種類のパターンを取り上げた。

Wikipediaより

その先人となる書籍の著者4人をGang of Fourと総称してGoFデザインパターンと呼ばれている

主要なデザインパターン

パターンカテゴリ各パターン名
生成に関するパターンFactory Method, Abstract Factory, Builder, Prototype, Singleton
構造に関するパターンAdapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy
振る舞いに関するパターンChain of Responsibility, Command, Interpreter, Iterator, Mediator, Mement, Observer, State, Strategy, Visitor

上記3カテゴリの23種類のパターンがメインだが、近年のマイクロサービス向けのマルチスレッドプログラミングに関するパターンがさらに14種類もあるがとりあえず古くから伝わる23種を軸に今回は生成に関するパターンを掘り下げる


Factory Methodパターン

  • 共通インターフェイスの具体的な実装を作成するために使用される作成デザインパターン
  • インスタンス作成をサブクラスにまかせることでコードから分離する

お題

  • ファクトリーメソッドを使わずに各コース情報のクラスを作成したケース
# 週末コースクラス
class DEV:
    def price(self):
        return 300000
		def schedule(self):
				return "土日"
    def __str__(self):
        return "DEV"

# 平日コースクラス
class LAB:
    def price(self):
        return 500000
		def schedule(self):
				return "月〜金"
    def __str__(self):
        return "LAB"


if __name__ == "__main__":
    # 各コースのクラスをインスタンス化
    dev = DEV()   
    lav = LAB()    

    print(f'{dev}コースの授業料は{dev.price()}円で、授業は毎週{dev.schedule()}に行われます')  
		# --> DEVコースの授業料は300000円で、授業は毎週土日に行われます
    print(f'{lav}コースの授業料は{lav.price()}円で、授業は毎週{lav.schedule()}に行われます') 
		# --> LABコースの授業料は500000円で、授業は毎週火〜金に行われます
# 各コース情報の出力用クラス
class Factory:
    def __init__(self, courses_factory = None):
        self.course_factory = courses_factory
 
    def show_course(self):
        course = self.course_factory()
        print(f'コース名: {course}')
        print(f'授業料:¥{course.price()}')
        print(f'授業日程: {course.schedule()}')
        print(f'--------------------------------------')

class DEV:
    def price(self):
        return 300000
    def schedule(self):
        return "土日"
    def __str__(self):
        return "DEV"

class LAB:
    def price(self):
        return 500000
    def schedule(self):
        return "火〜金"
    def __str__(self):
        return "LAB"

class BIZ:
    def price(self):
        return 150000
    def schedule(self):
        return "水"
    def __str__(self):
        return 'BIZ'


if __name__ == "__main__":
    # 各コースのリスト
    courses = [DEV, LAB, BIZ]
 
    # 配列に含んだコースの情報を表示
    for i in courses:
      Factory(i).show_course()
コース名: DEV
授業料: ¥300000
授業日程: 毎週 土日
--------------------------------------
コース名: LAB
授業料: ¥500000
授業日程: 毎週 月〜金
--------------------------------------
コース名: BIZ
授業料: ¥150000
授業日程: 毎週 水
--------------------------------------
  • ファクトリーメソッドを利用して、新規コースのBIZを追加
    • Factoryクラスが各コースのインターフェースだけを使ってインスタンスの結合と出力を同時にまとめることができる
    • これによりさらに新コースが追加されたり、出力インターフェースの構造に変更が加わる際にもFactoryクラスを修正すればメンテが楽になり、インスタンス化の一括操作もしやすくなる

メリット

  • クラス同士の密結合を回避できる
  • 抽象クラスでインターフェースとなる製品作成コードを1つの場所に限定することで単一責任の原則が適用される
  • 既存コードを壊すことなく新規のクラス(製品やオプション項目)を追加できるのでオープン/クローズド原則も保てる

デメリット

  • パターンを実装するために多くの新しいサブクラスを導入する必要があるため、コードが冗長になり長期的に複雑化する可能性がある

Abstract Factory パターン

  • Abstract Factoryは、抽象的な工場という意味
  • 部品となる具象クラスのインスタンス化を直接行わず、工場となる関数にお任せすること
  • 具体的なクラスを指定せずに、関連オブジェクトまたは依存オブジェクトの関係を生成できるようにする創造的なデザインパターン

お題

  • 家具メーカーの生産工場を作ると過程
  • 取り扱う家具の種類は「ソファー」「テーブル」「椅子」の3種類
  • 各種類にはモデル、パターン、バリエーションの抽象的な組み合わせがあり、今後も増えることが予想される
クラス種類ソファーテーブル椅子
モデル・ソファベッド
・リクライニング
・フロアソファー
・ダイニング
・こたつ / 座卓
・オフィス
・デッキ・ダイニング
・ロッキング
・オフィス・ワーク
パターン・人数
・肘掛け 有 / 無
・脚 有 / 無
・四角 / 丸形
・脚の本数
・折りたたみ式
・肘掛け 有 / 無
・背もたれ 有/ 無
・キャスター 有 / 無
バリエーション・カバー色 / 柄
・カバー生地
・素材(木材 / 鉄)
・色 / 柄
・カバー色
・素材 / 生地

1. 工場全体の抽象クラス

  • 工場全体のラインで生産できる家具の種類を定義する抽象クラス
  • 現在この工場で生産可能な家具(インターフェース)は「ソファー」「椅子」「テーブル」の3種類
# 抽象ファクトリー
class AbstractFactory(ABC):
    @abstractmethod
    def create_sofa(self) -> AbstractProductSofa:   # ソファー
        pass
    @abstractmethod
    def create_chiar(self) -> AbstractProductChiar:   # 椅子
        pass
    @abstractmethod
    def create_table(self) -> AbstractProductTable:   # テーブル
        pass

2. 生産ラインの具象クラス

  • 工場で生産できるラインを準備する具象クラス
  • 現在この工場で稼働できるラインは2つ
# 生産ライン1の具象クラス
class ConcreteFactory1(AbstractFactory):
    def create_sofa(self) -> AbstractProductSofa:    # ソファー
        return ConcreteProductSofa1()
    def create_chiar(self) -> AbstractProductChiar:    # 椅子
        return ConcreteProductChiar1()
    def create_table(self) -> AbstractProductTable:   # テーブル
        return ConcreteProductTable1()

# 生産ライン2の具象クラス
class ConcreteFactory2(AbstractFactory):
    def create_sofa(self) -> AbstractProductSofa:    # ソファー
        return ConcreteProductSofa2()
    def create_chiar(self) -> AbstractProductChiar:    # 椅子
        pass
		def create_table(self) -> AbstractProductTable:   # テーブル
        pass

3. ソファーのバリエーション具象クラス

  • ユーザーが現在ソファで選択できるバリエーション「カバー素材」「カバーの色」のインターフェースを定義する
  • 「カバー素材」は"綿"or"革"、「カバーの色」は"ブラック" or "ブルー"or "レッド"を準備
# ソファー素材の具象クラス
class ConcreteProductSofa1(AbstractProductMaterial):

    def valiation_material_cotton(self) -> str:
        return "素材:綿"

    def valiation_material_leather(self) -> str:
        return "素材:革"


# ソファーカラーの具象クラス
class ConcreteProductSofa2(AbstractProductColor):

    def valiation_color_black(self) -> str:
        return "カラー:ブラック"

    def valiation_color_blue(self) -> str:
        return "カラー:ブルー"

		def valiation_color_red(self) -> str:
        return "カラー:レッド"

4. ソファーのバリエーションの抽象クラス

  • ファーのバリエーションので選択可能なインターフェースを定義する
  • カラーバリエーションのレッドはまだ生産ラインの準備ができていないのでここでは未実装
# ソファー生地の抽象クラス
class AbstractProductMaterial(ABC):

    @abstractmethod
    def valiation_material_cotton(self) -> str:
        pass

    @abstractmethod
    def valiation_material_leather(self) -> str:
        pass

# ソファーカラーの抽象クラス
class AbstractProductColor(ABC):

    @abstractmethod
    def valiation_color_black(self) -> str:
        pass

    @abstractmethod
    def valiation_color_blue(self) -> str:
        pass

5. 稼働ラインのクライアント側の実行処理

  • 実際に稼働させる生産物と各ラインで稼働させるオプションのオーダーを指定
# 生産ラインの設定
def factory_setting(factory: AbstractFactory, item) -> None:
    # オーダーによって生産ラインのインターフェースを分岐
    if "ソファー" in item:
      order = factory.create_sofa()
    elif "テーブル" in item:
      order = factory.create_table()
    elif "椅子" in item:
      order = factory.create_chiar()
    return order

    
if __name__ == "__main__":
    # 生産ライン1&2を「ソファー」の生産で稼働
    line_1 = factory_setting(ConcreteFactory1(), "ソファー")
    line_2 = factory_setting(ConcreteFactory2(), "ソファー")

    # ライン1は「綿素材」で稼働
    print(line_1.valiation_material_cotton())  # --> 素材:綿
    # ライン2は「カバー黒」で稼働
    print(line_2.valiation_color_black())  # --> カラー:ブラック

メリット

  • 具体的な製品クラスとクライアントコードの間の密結合を回避できる
  • Factory Methodと同様に単一責任の原則 と オープン/クローズド原則も保てる

デメリット

  • パターンとともに多くの新しいインターフェイスとクラスが導入されるため、コードは本来よりも複雑になる可能性がある
  • 抽象クラスのコースで新たにメソッドを定義した場合に、抽象クラスのコース情報を継承する全てのコースで変更が必要になる

基本クラスとそれを拡張するサブクラスにインスタンスの作成メソッドが含まれる、またはその類が含まれそうなこう場合は、ファクトリメソッド


Builderパターン

  • 複雑なオブジェクトの構築をその表現から分離して、同じ構築プロセスで異なる表現を作成できるようにする
  • 段階的に複雑なオブジェクトを構築することを可能にして、同じ構築コードを使用して、オブジェクトのさまざまなタイプと表現を簡単に作成できる
  • 部屋ごとのドアや窓の数など異なる内部が複雑な構成を部分的に構築しながらマイホームを完成させていくイメージ

お題

実行コマンドのオプションによって、コンテキストの出力パターンを振り分ける

plainモード実行

$ python Main.py plain

======================
Greeting

*** From the morning to the afternoon ***
- Good morning
- Hello
*** In the evening ***
- Good evening
- Good night
- Good bye
======================

htmlモード実行

$ python Main.py html

[Greeting.html] was created.

「依頼」「監督」「内装」「建築」といった個別の構造を持ったインスタンスを段階的に分離して組み上げることで複雑なモノを構築させる

依頼(クライアント)側の実行クラス

実行コマンドの引数によってテキスト出力モード(plain) または HTML出力モード(html)によって実行パターンを分ける

import sys

from builder.director import Director
from builder.textbuilder.text_builder import TextBuilder
from builder.htmlbuilder.html_builder import HTMLBuilder

def startMain(opt):
    if opt == "plain":
        builder = TextBuilder()
        director = Director(builder)
        director.construct()
        result = builder.getResult()
        print(result)
    elif opt == "html":
        builder = HTMLBuilder()
        director = Director(builder)
        director.construct()
        result = builder.getResult()
        print("[" + result + "]" + " was created.")


if __name__ == "__main__":
    startMain(sys.argv[1])

各インスタンスの建築役の抽象クラス

インスタンスを作成するための共通のインタフェース

from abc import ABCMeta, abstractmethod

class Builder(metaclass=ABCMeta):
    @abstractmethod
    def makeTitle(self, title):
        pass

    @abstractmethod
    def makeString(self, str):
        pass

    @abstractmethod
    def makeItems(self, items):
        pass

    @abstractmethod
    def close(self):
        pass

各パーツの構築監督役クラス

  • 構築ステップが実行される順序を定義
  • 建築役(Builder)のインタフェースを使って、各インスタンスを生成
  • 「タイトル」「本文」「リスト」「文末」ごとのパーツを監視
class Director(object):
    def __init__(self, builder):
        self.__builder = builder

    def construct(self):
        self.__builder.makeTitle("Greeting")
        self.__builder.makeString("From the morning to the afternoon")
        self.__builder.makeItems(["Good morning", "Hello"])
        self.__builder.makeString("In the evening")
        self.__builder.makeItems(["Good evening", "Good night", "Good bye"])
        self.__builder.close()

オブジェクトの内装役クラス

テキスト出力させるインスタンス作成で呼び出されるメソッド
from builder.builder import Builder

class TextBuilder(Builder):
    def __init__(self):
        self.buffer = []

    def makeTitle(self, title):
        self.buffer.append("======================\n")
        self.buffer.append(title + "\n")
        self.buffer.append("\n")

    def makeString(self, str):
        self.buffer.append("*** " + str + " ***" + "\n")

    def makeItems(self, items):
        for i in items:
            self.buffer.append("- " + i + "\n")

    def close(self):
        self.buffer.append("======================\n")

    def getResult(self):
        return ''.join(self.buffer)
HTMLで出力させるインスタンス作成で呼び出されるメソッド

「タイトル」にはhtmlタグとbodyタグとtitleタグ、「文末」には各閉じタグで囲むような構成

from builder.builder import Builder

class HTMLBuilder(Builder):
    def __init__(self):
        self.buffer = []
        self.filename = ""
        self.f = None
        self.makeTitleCalled = False

    def makeTitle(self, title):
        self.filename = title+".html"
        self.f = open(self.filename, "w")
        self.f.write("<html><head><title>"+title+"</title></head></html>")
        self.f.write("<h1>"+title+"</h1>")
        self.makeTitleCalled = True

    def makeString(self, str):
        if not self.makeTitleCalled:
            raise RuntimeError
        self.f.write("<p>"+str+"</p>")

    def makeItems(self, items):
        if not self.makeTitleCalled:
            raise RuntimeError
        self.f.write("<ul>")
        for i in items:
            self.f.write("<li>"+i+"</li>")
        self.f.write("</ul>")

    def close(self):
        if not self.makeTitleCalled:
            raise RuntimeError
        self.f.write("</body></html>")
        self.f.close()

    def getResult(self):
        return self.filename

メリット

  • 明示的なメソッド呼び出しにより、ユーザーは何を渡しているかがわかるため、エラーが発生しにくい
  • オブジェクトを段階的に構築したり、構築ステップを延期したり、ステップを再帰的に実行したりできます
  • オブジェクトの複雑な構造がこのオブジェクトのビジネスロジックから分離されているため、単一責任原則が適用される

デメリット

  • 構造によってはコードの複雑さが増す、または冗長なコードが重複しやすくなり必要なクラスの数が増える

Prototypeパターン

  • コードをクラスに依存させることなく既存のオブジェクトをコピーできるようにする
  • クラスからインスタンスをつくるのではなく、インスタンスをコピーすることで、インスタンスから別のインスタンスをつくる
  • 以下のシチュエーションに遭遇した場合はこのパターンを採用するのが好ましいとされる
    • 種類が多すぎてクラスにまとめられない場合
    • クラスからインスタンス作成が難しい場合
    • フレームワークと生成するインスタンスを分けたい場合
  • イラレなどでツールが元々持ってる機能の四角形(プロトタイプ)を五角形に変更(インスタンス化)した図形を複数コピーするイメージ

お題

実行処理クラス

文字列にデコレーション加工を加えるインスタンスを複製して「Hello World」をデコる

def startMain(managerObject):
    upen = UnderlinePen("-")
    mbox = MessageBox("*")
    sbox = MessageBox("/")
    managerObject.register("strong", upen)    # アンダーバー加工を登録
    managerObject.register("warning", mbox)   # アスタリスク加工を登録
    managerObject.register("slash", sbox)     # スラッシュ加工を登録

    p1 = managerObject.create("strong")   # アンダーバー加工インスタンスを複製
    p2 = managerObject.create("warning")   # アスタリスク加工インスタンスを複製
    p3 = managerObject.create("slash")   # スラッシュ加工インスタンスを複製
    p1.use("Hello World")
    p2.use("Hello World")
    p3.use("Hello World")

if __name__ == "__main__":
    startMain(Manager())
"Hello World"
---------------------

***************
* Hello World *
***************

///////////////
/ Hello World /
///////////////

Prototypeクラス

  • インスタンスをコピー(複製)して新しいインスタンスを作るためのインタフェース
  • インスタンスを複製して、新しいインスタンスを生成するメソッドを定める
  • 「利用」メソッドと「複製」メソッドを担当する
from abc import ABCMeta, abstractmethod
import copy


class Prototype(metaclass=ABCMeta):
    @abstractmethod
    def use(self, s):
        pass

    @abstractmethod
    def createClone(self):
        pass

ConcretePrototypeクラス

  • 複製したインスタンスに加工するためのクラス
  • 実際にインスタンスを複製して、新しいインスタンスを生成するメソッドを実装
  • 「アンダーライン」と 「スラッシュ」「アスタリスク」のデコレーションクラスの2種類
# アンダーライン加工クラス
class UnderlinePen(Prototype):
    def __init__(self, ulchar):
        self.__ulchar = ulchar

    def use(self, s):
        length = len(s)
        line = self.__ulchar * (length + 10)

        print("\"{0}\"".format(s))
        print("{0}\n".format(line))

    def createClone(self):
        clone = copy.deepcopy(self)
        return clone
# デコレーション加工クラス
class MessageBox(Prototype):
    def __init__(self, decochar):
        self.__decochar = decochar

    def use(self, s):
        length = len(s)
        line = self.__decochar * (length + 4)

        print("{0}".format(line))
        print("{0} {1} {2}".format(self.__decochar, s, self.__decochar))
        print("{0}\n".format(line))

    def createClone(self):
        clone = copy.deepcopy(self)
        return clone

Client(利用者)クラス

Prototypeからインスタンスをコピーするメソッドを利用して、実際に新しいインスタンスを作成

class Manager(object):
    def __init__(self):
        self.__showcase = {}

    def register(self, name, proto):
        self.__showcase[name] = proto

    def create(self, protoname):
        p = self.__showcase[protoname]
        return p.createClone()

メリット

  • 具象クラスに結合せずにオブジェクトのクローンを作成できる
  • 事前に作成されたプロトタイプのクローンを作成するために、繰り返される初期化コードを取り除くことができる
  • 複雑なオブジェクトの構成プリセットを処理する場合は、継承の代わりになる

デメリット

  • クローン元が複雑な循環構成のオブジェクトの場合は、クローン自体を作成するのが難しい場合が多い

Singletonパターン

  • 「あるクラスのインスタンスが常にたった1つしか存在していない」という状態を実現したいときに利用される
  • あるクラスについて「複数のインスタンスが作られると困る」場面での採用が望まれるパターン
  • 以下の場合はシングルトンパターンが望ましい
    • たくさんのインスタンスからアクセスされると困るとき
    • 毎回、インスタンス生成をするのが手間
    • 共通的な情報保持をして、メモリを節約したい
  • 図書館の貸出帳などの複数存在してしまうと、貸出中の本の現在の状態の管理ができなくなるもののイメージ

お題

from threading import Lock, Thread


class SingletonMeta(type):
    _instances = {}
    _lock: Lock = Lock()
    def __call__(cls, *args, **kwargs):
        with cls._lock:
            if cls not in cls._instances:
                instance = super().__call__(*args, **kwargs)
                cls._instances[cls] = instance
        return cls._instances[cls]


class Singleton(metaclass=SingletonMeta):
    value: str = None

    def __init__(self, value: str) -> None:
        self.value = value

    def some_business_logic(self):
        pass


def test_singleton(value: str) -> None:
    singleton = Singleton(value)
    print(singleton.value)


if __name__ == "__main__":
    # 初回発行
    process1 = Thread(target=test_singleton, args=("FOO",))
    process1.start()
		# --> FOO
    
    # 再利用
    process2 = Thread(target=test_singleton, args=("BAR",))
    process2.start()
		# --> FOO
  • 一度実行したインスタンスのはロックがかかり、上書くことができない様にできる
  • pythonはThreadモジュールで意図的に破棄しないインスタンスにロックをかけることができるので、ロジックや詳細なアルゴリズムなどはあまり気にしたことがない

メリット

  • クラスにインスタンスが1つしかないことが確認できる(または保証されている)

デメリット

シングルトンパターンの誘惑に負けない」によれば結構なネガキャン

  • 「必要なインスタンスは1つだけ」という要望は、多くの場合推測にすぎず、後で必要になった場合に破綻する
  • 理論的には独立しているはずのコード間に暗黙の依存関係を生んでしまう
  • グローバルにアクセス可能で、かつ永続化された状態=部分的なユニットテストの妨げになる
  • マルチスレッド環境ではシングルトンオブジェクトへのアクセスにはロックが必要になり、全体的な効率が下がり危険性が高まる

まとめ

  • ファクトリメソッド
    • サブクラスを介して拡張・カスタマイズすることを前提としているので最初から複雑な構造にするべきではない
    • 設計や戦略が不透明な場合はとりあえずこのパターンを入り口に他のパターンに変更or対応させていく
  • 抽象ファクトリ
    • 関連オブジェクトの関係性の作成することに重点を置いている
    • ゴールを複数選択する必要があるので、それぞれのルートの関係性を繋"何が作成されるか?"に焦点を当てるスタイル
  • ビルダー
    • 複雑なオブジェクトを段階的に構築することに重点を置いている
    • ゴールはひとつだが、そこまでの経緯が複雑な場合に、それが"どのように作られるか?"に焦点を当てるスタイル
  • プロトタイプ
    • 複製元の継承に基づいていないため、一時的な利用の点では利点が多い
    • 複製後に複雑な初期化が必要になる場合は抽象ファクトリに基づいて継承させた方が良い
  • シングルトン
    • 言わんとする事はわかるが具体的な採用シチュエーションと実装イメージは正直よくわからない

パターンの多くは、もはやあたり前に存在するという印象で意識しなくても実は使っているというケースも多い(自分の場合はビルダーパターンは好んでよくやっている)

初期段階は実装パターンがぼんやりしたまま開発に入ることも多いので、ドヤ顔で「今回はこのパターンで行こう!」みたいな事はめったに無いと思われる。
開発過程で不安になってきたときに「それぞれのパターンの志向に沿っているか?」「変更の必要がでてきてるんじゃないか?」などの確認時の際に言語化しやすくはなりそう

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

トップへ