utamaro’s blog

誰かの役に立つ情報を発信するブログ

pythonを使ってファクトリメソッドパターンを実装する方法について

python を使ってファクトリメソッドパターンの実装をした際に試した内容です。

ファクトリメソッドパターンの作り方としては二通り知っていますが、そのうちの「カテゴリによって実行するインスタンスを決める」方法の実装を紹介します。

今回はデコレータを使ったパターンと、使わなかったときのパターンを比較して、どちらを選択したほうが良いか考えます。

結論としては、デコレータを使ったパターンの方がコード量が少なく、メンテ時の修正範囲も少なくなると思いました。

環境について

  • python3.9.0

題材について

以下の条件を満たすクラス設計をします。

  • 従業員が存在する
  • 従業員は2つのカテゴリで分類される
    • エンジニア
    • 営業
  • 各従業員には名前がある
  • 各従業員はカテゴリ別に通勤方法が決まっている
    • エンジニアは徒歩で通勤する
    • 営業は電車で通勤する
  • 従業員の名前と通勤方法を出力できる

デコレータを使ったパターン

デコレータを使うことで、どのクラスがファクトリーの対象となっているのかがわかりやすくなっています。

from enum import Enum


class Employee:
    def __init__(self, name):
        self._name = name

    def commute_method(self):
        # 本来は出社方法を集約かコンポジションで管理するのが良いです。
        # 簡易的な実装にするために関数を実装するようにします。
        # もしくは、出社するためのクラスを別に用意するのが良いと思います。
        raise NotImplementedError

    @property
    def name(self):
        return self._name


class CategoryTypes:
    engineer = 'engineer'
    sales = 'sales'


class EmployeeFactory:
    class_ = {}

    @classmethod
    def register(cls, category):
        def wrapper(cls_):
            if not issubclass(cls_, Employee):
                raise TypeError
            cls.class_[category] = cls_
            return cls_
        return wrapper

    @classmethod
    def create(cls, category):
        cls_ = cls.class_[category]
        return cls_


@EmployeeFactory.register(CategoryTypes.engineer)
class Engineer(Employee):

    def commute_method(self):
        return '歩いて出社'


@EmployeeFactory.register(CategoryTypes.sales)
class Sales(Employee):
    def commute_method(self):
        return '電車で出社'


class EmployeeInfo:
    def __init__(self, employee_):
        self._employee = employee_

    def print(self):
        print('\n'.join([
            f'名前: {self._employee.name}',
            f'通勤方法: {self._employee.commute_method()}',
        ]))


if __name__ == '__main__':
    employee = EmployeeFactory.create(CategoryTypes.engineer)
    sales = EmployeeFactory.create(CategoryTypes.sales)
    EmployeeInfo(employee('Aさん')).print()
    EmployeeInfo(sales('Bさん')).print()

実行結果

名前: Aさん
通勤方法: 歩いて出社
名前: Bさん
通勤方法: 電車で出社

デコレータを使わない例

デコレータを使った例を基に、デコレータを使わなかったときのコードを考えてみます。

from enum import Enum


class Employee:
    def __init__(self, name):
        self._name = name

    def commute_method(self):
        # 本来は出社方法を集約かコンポジションで管理するのが良いです。
        # 簡易的な実装にするために関数を実装するようにします。
        # もしくは、出社するためのクラスを別に用意するのが良いと思います。
        raise NotImplementedError

    @property
    def name(self):
        return self._name


class CategoryTypes(Enum):
    engineer = 'engineer'
    sales = 'sales'


class Engineer(Employee):

    def commute_method(self):
        return '歩いて出社'


class Sales(Employee):
    def commute_method(self):
        return '電車で出社'


class EmployeeFactory:
    FACTORY_MAP = {
        CategoryTypes.engineer: Engineer,
        CategoryTypes.sales: Sales,
    }

    @classmethod
    def create(cls, category_type):
        if category_type in cls.FACTORY_MAP:
            return cls.FACTORY_MAP[category_type]
        raise TypeError


class EmployeeInfo:
    def __init__(self, employee_):
        self._employee = employee_

    def print(self):
        print('\n'.join([
            f'名前: {self._employee.name}',
            f'通勤方法: {self._employee.commute_method()}',
        ]))


if __name__ == '__main__':
    employee = EmployeeFactory.create(CategoryTypes.engineer)
    sales = EmployeeFactory.create(CategoryTypes.sales)
    EmployeeInfo(employee('Aさん')).print()
    EmployeeInfo(sales('Bさん')).print()

デコレータを使った場合と異なるのは、ファクトリを作成するクラス(EmployeeFactory)です。

また、カテゴリを管理するクラスがEnumになっています。

なぜEnumになっているのかというと、デコレータを使った場合の実装だと、Enum.field.valueのようにvalueを書く必要があったからです。 valueを書くのを忘れてエラーになってしまうことが多いため、あえてEnumでの実装を避けています。

class CategoryTypes(Enum):
    engineer = 'engineer'
    sales = 'sales'


class EmployeeFactory:
    FACTORY_MAP = {
        CategoryTypes.engineer: Engineer,
        CategoryTypes.sales: Sales,
    }

    @classmethod
    def create(cls, category_type):
        if category_type in cls.FACTORY_MAP:
            return cls.FACTORY_MAP[category_type]
        raise TypeError

比較する

デコレータを使った場合と、デコレータを使わなかった場合を比較すると、デコレータを使った場合の方がカテゴリの追加時(更新時)に修正範囲が小さいです。

例えば、新しく「カスタマーサポート」を追加する際を考えてみます。

デコレータを使った実装の場合は以下の修正が必要になります。

class CategoryTypes:
    engineer = 'engineer'
    sales = 'sales'
    customer_support = 'customer_support'


@EmployeeFactory.register(CategoryTypes.customer_support)
class CustomerSupport(Employee):
    def commute_method(self):
        return '自転車で出社'


customer_support = EmployeeFactory.create(CategoryTypes.customer_support)
EmployeeInfo(customer_support('Cさん')).print()

このように、カテゴリを追加して、そのクラスを用意するだけで修正が完了します。

デコレータを使わない場合についても同様に考えてみます。

class CategoryTypes(Enum):
    engineer = 'engineer'
    sales = 'sales'
    customer_support = 'customer_support'


class CustomerSupport(Employee):
    def commute_method(self):
        return '自転車で出社'


class EmployeeFactory:
    FACTORY_MAP = {
        CategoryTypes.engineer: Engineer,
        CategoryTypes.sales: Sales,
        CategoryTypes.customer_support: CustomerSupport,
    }
    # 略


customer_support = EmployeeFactory.create(CategoryTypes.customer_support)
EmployeeInfo(customer_support('Cさん')).print()

このように、FACTORY_MAPを修正するひと手間が必要になってしまいます。

今回はとてもシンプルな例で比較したため、1箇所のみの差しかありませんでしたが複雑になれば大きな差になるため気をつけたほうが良いです。

結論

ファクトリメソッドパターンを python で実装する場合は、デコレータを積極的に使うことで、修正箇所が少なくなります。