22
Help us understand the problem. What are the problem?

GOFデザインパターンをPythonで理解する ~ Creational Design Patterns編 ~

GOFとは?

GOFとはGang of Fourの略でオブジェクト指向プログラミングにおける再利用性の高いコーディングのパターン、デザインパターンをまとめた4人のプログラマー(Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides)のことを指します。
彼らは「Design Patterns: Elements of Reusable Object-Oriented Software(オブジェクト指向における再利用のためのデザインパターン)」の著者であり、今回はその本の中でまとめられているCreational Design Patterns(生成に関するデザインパターン)についてまとめていこうと思います!

Creational Design Patterns(生成に関するデザインパターン)は以下の通りです。

  • Abstract Factory
  • Builder
  • Factory Method
  • Prototype
  • Singleton

これらのパターンはその名の通り、クラスの生成に関するデザインパターンを紹介しています。
それでは順に見ていきましょう!

Abstract Factory

概要

Abstract Factoryは複雑パターンのクラスの作成を抽象的なクラスを作成することでシンプルにする設計方法です。

どんな時に使える?

  • 複数あるクラスを特定の組み合わせで利用したい時

作るもの

  • 車A、バイクAを作る工場Aと車B、バイクBを作る工場Bがあります。
  • 工場を指定することで対応する車とバイクを作れる関数を作成したいです。
  • 車にはstart・stop、バイクにはprocess・completeという関数があるとします。

特に何も考えずコードを書くと以下のようになるでしょうか。

ソースコード (Abstract Factoryなし)

  • Car, Bike classを設計
  • 工場AかBによって対象のCar ClassとBike Classを返却する。
class CarA:
    def start(self):
        # some process
        pass

    def stop(self):
        # some process
        pass


class CarB:
    def start(self):
        # some process
        pass

    def stop(self):
        # some process
        pass


class BikeA:
    def proceed(self):
        # some process
        pass

    def complete(self):
        # some process
        pass


class BikeB:
    def proceed(self):
        # some process
        pass

    def complete(self):
        # some process
        pass


def main(
    factory_type: str,
):
    if factory_type == "A":
        return CarA, BikeA
    elif factory_type == "B":
        return CarB, BikeB


if __name__ == "__main__":
    # 工場Aで作成する場合
    main("A")
    # 工場Bで作成する場合
    main("B")

一見問題ないように見えますがこれでは工場が増えるたびに条件分岐が増えていき、また工場で作るものが変更されるたびに返却する要素も変更する必要が出てきます。
その際組み合わせを間違える、返却する値が足りないなどの問題が出てくるためAbstract Factoryを用いて解決していきましょう。

Abstract Factoryは以下のように実装できます。

  1. 各classを抽象化したクラスを作成し、具体的なクラスは抽象クラスを継承するようにする。
  2. 抽象化したクラスを作成するAbstract Factoryを作成し、具体的なクラスを生成するFactory Classはこれを継承するようにします。

ソースコード (Abstract Factoryあり)

from abc import ABC, abstractmethod
from typing import Tuple


class AbstractCar(ABC):
    @abstractmethod
    def start(self):
        pass

    @abstractmethod
    def stop(self):
        pass


class CarA(AbstractCar):
    def start(self):
        # some process
        pass

    def stop(self):
        # some process
        pass


class CarB(AbstractCar):
    def start(self):
        # some process
        pass

    def stop(self):
        # some process
        pass


class AbstractBike(ABC):
    @abstractmethod
    def proceed(self):
        pass

    @abstractmethod
    def complete(self):
        pass


class BikeA(AbstractBike):
    def proceed(self):
        # some process
        pass

    def complete(self):
        # some process
        pass


class BikeB(AbstractBike):
    def proceed(self):
        # some process
        pass

    def complete(self):
        # some process
        pass


class AbstractFactory(ABC):
    @abstractmethod
    def build_car(self) -> AbstractCar:
        pass

    @abstractmethod
    def build_bike(self) -> AbstractBike:
        pass


class FactoryA(AbstractFactory):
    def build_car(self) -> CarA:
        return CarA()

    def build_bike(self) -> BikeA:
        return BikeA()


class FactoryB(AbstractFactory):
    def build_car(self) -> CarB:
        return CarB()

    def build_bike(self) -> BikeB:
        return BikeB()


def main(
    factory: AbstractFactory,
) -> Tuple[AbstractCar, AbstractBike]:
    car = factory.build_car()
    bike = factory.build_bike()
    return car, bike


if __name__ == "__main__":
    # 工場Aで作成する場合
    main(FactoryA())
    # 工場Bで作成する場合
    main(FactoryB())

このようにすることで新たにFactoryが増えた場合でもAbstract Factoryを継承したFactoryを増やして作成するCarとBikeを選べばいいだけですし、また新たなプロダクトを工場で作ることになった場合でも既存の工場classにメソッドを生やしてあげるだけで済みます。
このように複雑な組み合わせのクラスを作成する場合、Interfaceを定義した抽象クラスを用いることで共通処理をまとめられたり、変更の影響を最小限にできます。

なお abstract_methodとはその名の通り抽象クラスを定義するdecoratorで、継承したクラス内で対象の関数が定義することを担保することができます。

Builder

概要

同じ生成過程で別のものを作る設計パターンです。

どんな時に使える?

  • 同じ過程で作成される複数のクラスを作るとき。

作りたいもの例

  • 車(Car)とバイク(Bike)を以下の部品をそれぞれ専用の部品を取り付けて作りたいです。
    • エンジン(engine): 車にはガソリン(gasoline)、バイクにはディーゼル(diesel)
    • トランスミッション(transmission): 車にはオートマ(auto)、バイクにはマニュアル(manual)
    • タイヤ(wheel): 車には4つ(four wheels)、バイクには二つ(two wheels)

ソースコード(Builderなし)

from dataclasses import dataclass


@dataclass()
class Engine:
    type: str


@dataclass()
class Transmission:
    type: str


@dataclass()
class Wheel:
    type: str


@dataclass()
class Car:
    engine: Engine
    transmission: Transmission
    wheel: Wheel


@dataclass()
class Bike:
    engine: Engine
    transmission: Transmission
    wheel: Wheel


def main(target_product: str):
    if target_product == "Car":
        gasoline = Engine("gasoline")
        auto = Transmission("auto")
        four_wheels = Wheel("four")
        return Car(gasoline, auto, four_wheels)

    elif target_product == "Bike":
        diesel = Engine("diesel")
        manual = Transmission("manual")
        two_wheels = Wheel("two")
        return Car(diesel, manual, two_wheels)


if __name__ == "__main__":
    # 車を作る場合
    main("Car")
    # バイクを作る場合
    main("Bike")

一見問題ないコードのように見えますが部品が増えていくとどうなるでしょう。
作成するClass(Car, Bike)に渡す値がどんどん増えていき複雑性が増していきますね。このようにattributesが多いclassを設計するときにBuilderパターンは有効です。
Builderパターンではオブジェクトの生成を専用のclassを設けることでインスタンス生成処理がスッキリかけます。

ソースコード(Builderあり)

from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Optional


@dataclass()
class Engine:
    type: str


@dataclass()
class Transmission:
    type: str


@dataclass()
class Wheel:
    type: str


@dataclass()
class Car:
    engine: Optional[Engine] = None
    transmission: Optional[Transmission] = None
    wheel: Optional[Wheel] = None


@dataclass()
class Bike:
    engine: Optional[Engine] = None
    transmission: Optional[Transmission] = None
    wheel: Optional[Wheel] = None


class AbstractBuidler(ABC):
    @abstractmethod
    def build(self):
        pass

    @abstractmethod
    def build_engine(self):
        pass

    @abstractmethod
    def build_transmission(self):
        pass

    @abstractmethod
    def build_wheel(self):
        pass


@dataclass()
class CarBuilder(AbstractBuidler):
    car: Car = Car()

    @classmethod
    def build(cls) -> Car:
        if (
            cls.car.engine is None
            or cls.car.transmission is None
            or cls.car.wheel is None
        ):
            raise ValueError("some parts are missing")
        return cls.car

    @classmethod
    def build_engine(cls):
        cls.car.engine = Engine("auto")

    @classmethod
    def build_transmission(cls):
        cls.car.transmission = Transmission("auto")

    @classmethod
    def build_wheel(cls, wheel: Wheel):
        cls.car.wheel = Wheel("four")


@dataclass()
class BikeBuilder(AbstractBuidler):
    bike: Bike = Bike()

    @classmethod
    def build(cls) -> Bike:
        if (
            cls.bike.engine is None
            or cls.bike.transmission is None
            or cls.bike.wheel is None
        ):
            raise ValueError("some parts are missing")
        return cls.bike

    @classmethod
    def build_engine(cls):
        cls.bike.engine = Engine("auto")

    @classmethod
    def build_transmission(cls):
        cls.bike.transmission = Transmission("auto")

    @classmethod
    def build_wheel(cls, wheel: Wheel):
        cls.bike.wheel = Wheel("four")


class Director:
    @staticmethod
    def construct(builder: AbstractBuidler):
        builder.build_engine()
        builder.build_transmission()
        builder.build_wheel()

        return builder.build()


def main(target_product: str):
    if target_product == "Car":
        return Director.construct(CarBuilder())
    elif target_product == "Bike":
        return Director.construct(BikeBuilder())


if __name__ == "__main__":
    # 車を作る場合
    main("Car")
    # バイクを作る場合
    main("Bike")

CarBuilder及びBikeBuilderを設けることでそれぞれのインスタンスの作成がbuilder class内に閉じ込められコードがスッキリしました。またDirectorclassを設けCar BuilderまたはBike Builderを渡すことで作成したいインスタンスが作成されるようになりました。

Factory Method

概要

オブジェクト作成時に、作成するオブジェクトのクラスをサブクラスに選ばせるパターンです。

どんな時に使える?

クラス内で他のクラスを使用している時(サブクラス)。

作りたいもの例

  • 車とバイクを作る工場があるとします
  • 商品が完成したらチェックとしてタイヤの数を確認して4つあれば車、2つあればバイクができたとみなせるとします

ソースコード(Factory Methodなし)

from dataclasses import dataclass


def main():
    car = CarFactory().build()
    CarFactory.check_product(car)

    bike = BikeFactory().build()
    BikeFactory.check_product(bike)


@dataclass()
class Car:
    wheels_num = 4

    def count_wheels(self):
        return self.wheels_num


@dataclass()
class Bike:
    wheels_num = 2

    def count_wheels(self):
        return self.wheels_num


@dataclass()
class CarFactory:
    @staticmethod
    def build():
        return Car()

    @staticmethod
    def check_product(car: Car):
        if car.wheels_num != 4:
            raise ValueError("this car does not have 4 wheels!")


class BikeFactory:
    @staticmethod
    def build():
        return Bike()

    @staticmethod
    def check_product(bike: Bike):
        if bike.wheels_num != 2:
            raise ValueError("this bike does not have 2 wheels!")


if __name__ == "__main__":
    main()

一見問題ないコードに見えますね。ですがfactory methodをうまく使うことでよりスマートにかけます。

ソースコード(factory methodあり)

from abc import ABC, abstractmethod
from dataclasses import dataclass


def main():
    car = CarFactory()
    car.check_product(wheels_num=4)

    bike = BikeFactory()
    bike.check_product(wheels_num=2)


class Product(ABC):
    @abstractmethod
    def count_wheels() -> int:
        pass


@dataclass()
class Car(Product):
    wheels_num = 4

    def count_wheels(self) -> int:
        return self.wheels_num


@dataclass()
class Bike(Product):
    wheels_num = 2

    def count_wheels(self) -> int:
        return self.wheels_num


class Factory(ABC):
    def __init__(self) -> None:
        self.product = self.factory_method()

    def build(self) -> Product:
        if self.product is None:
            raise RuntimeError("product is None")
        return self.product

    def check_product(self, wheels_num: int):
        if self.product is None:
            raise RuntimeError("product is None")
        if self.product.count_wheels() != wheels_num:
            raise ValueError(f"this {self.product} does not have {wheels_num} wheels!")

    @abstractmethod
    def factory_method():
        pass


class CarFactory(Factory):
    def factory_method(self) -> Car:
        return Car()


class BikeFactory(Factory):
    def factory_method(self) -> Bike:
        return Bike()


if __name__ == "__main__":
    main()

factory methodの最大のポイントはクラス内で使用するサブクラスをConcrete Class(具体的なクラス)の中で指定できるという点です。
これを用いることによってクラス・サブクラスのセットを正しく作ることができます。

Prototype

概要

オブジェクトをコピーすることによりクラスを作成する方法です。

作りたいもの

  • "Lexus"という名前の車(Car)objectを複数作りたいです。

どんな時に使える?

同じオブジェクトから複数コピーを作りたい時

ソースコード(Prototypeなし)

def main():
    lexus_1 = Car("Lexus")
    lexus_2 = Car("Lexus")


class Car:
    def __init__(self, name):
        self.name = name


if __name__ == "__main__":
    main()

一見問題なさそうですが、"Lexus"という文字列をtypoする可能性もあり、lexus_1lexus_2が全く同じオブジェクトであるという担保はできておりません。
Prototypeを使うことでこの問題を解決できます。

ソースコード(Prototypeあり)

import copy


def main():
    lexus_1 = Car("Lexus")
    lexus_2 = copy.deepcopy(lexus_1)


class Car:
    def __init__(self, name):
        self.name = name


if __name__ == "__main__":
    main()

pythonの組み込みパッケージである copy.deepcopy()を使用してオブジェクトをコピーして別のオブジェクトを作成することができました!

Singleton

概要

オブジェクトが同一スレッド内で一つしか存在しないことを担保するための方法です。

どんな時に使える?

ConfigやLoggerなどのglobalで設定したいclassを作成する時

作りたいもの

  • 車を作成するための共通設定を管理するクラス(FactoryConfig)を作成したい。

ソースコード(Singletonなし)

class FactoryConfig:
    def __init__(self, log_level="INFO"):
        self.log_level = log_level


class Factory_A:
    def __init__(self, config):
        self.config = config

    def show_config(self):
        return self.config


class Factory_B:
    def __init__(self, config):
        self.config = config

    def show_config(self):
        return self.config


def main():
    config = FactoryConfig(log_level="DEBUG")
    factory_a = Factory_A(config)
    print(vars(factory_a.show_config()))

    bad_config = FactoryConfig(log_level="WARNING")
    factory_b = Factory_B(bad_config)
    print(vars(factory_b.show_config()))


if __name__ == "__main__":
    main()

実行結果

{'log_level': 'DEBUG'}
{'log_level': 'WARNING'}

プロセスの最初にConfigを設定したのに途中で再度configを読んで設定を変更してしまい間違ったconfigが適応されてしまいました。
ここでSingletonパターンを使うことでプロセス内で一度しかclassが生成されないことを担保できます。

ソースコード(Singletonあり)

class SingletonMeta(type):
    _instances = {}

    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            instance = super().__call__(*args, **kwargs)
            cls._instances[cls] = instance
        return cls._instances[cls]


class FactoryConfig(metaclass=SingletonMeta):
    def __init__(self, log_level="INFO"):
        self.log_level = log_level


class Factory_A:
    def __init__(self, config):
        self.config = config

    def show_config(self):
        return self.config


class Factory_B:
    def __init__(self, config):
        self.config = config

    def show_config(self):
        return self.config


def main():
    config = FactoryConfig(log_level="DEBUG")
    factory_a = Factory_A(config)
    print(vars(factory_a.show_config()))

    bad_config = FactoryConfig(log_level="WARNING")
    factory_b = Factory_B(bad_config)
    print(vars(factory_b.show_config()))


if __name__ == "__main__":
    main()

実行結果

{'log_level': 'INFO'}
{'log_level': 'INFO'}

このようにSingletonパターンを使うと既に呼ばれたclassの場合、同じインスタンスを返却するのでプロセス内でそのクラスのインスタンスが一つしかないことを保証できています。

所感

GOFのデザインパターンはJavaで書かれたものであるためPythonだと少し書き方や適応方法が異なってきますが、知っておいて損はない設計手法のように思いました。
ただデザインパターンを覚えるのではなく、「インターフェイスに対してプログラミングする」という考えを体感できたのでこれからの実装に取り入れていきたいです。

References

今回のデザインパターン理解するのにとても役に立ったリンク先です。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
22
Help us understand the problem. What are the problem?