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は以下のように実装できます。
- 各classを抽象化したクラスを作成し、具体的なクラスは抽象クラスを継承するようにする。
- 抽象化したクラスを作成する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内に閉じ込められコードがスッキリしました。またDirector
classを設け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_1
と lexus_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
今回のデザインパターン理解するのにとても役に立ったリンク先です。