LoginSignup
3
6

More than 5 years have passed since last update.

『独習デザインパターン』をPythonに読み替える Factory Method パターン

Last updated at Posted at 2019-01-23

Java から Python へ翻訳しよう

『独習〜』は Java で書いてあります。
Java は Java なのでそのまま Python にコードを書き換えるのは難しい部分が私にはあります。
書き換えるのが難しいと思ったところで、『あの言語でできることがこの言語ではできない』
ということは、難易度の差こそあれそんな場面はあんまりないと聞きます。
聞く機会がなかっただけかもしれんけど。

「デザインパターンってなんぞ?」というのを飲み込んでいくため、
どうも簡潔すぎる内容の『実践 Python3』を読む前の準備をしとうございます。
最近、『実践〜』の内容がいささか古い気がしてきているけど突き進む所存です。
今回は Factory Method パターンをPythonに書き直していきます。

月に1個パターンを書いてると2年経ってようやくGoFが終わるぞ。と友人から言われて恐怖しています。

要件:

  • 本番環境と開発環境で読み込むデータ元や形式を変えたい
  • できるだけ修正箇所を少なくして、本番環境へ以降する際の漏れをなくしたい

ファクトリメソッドパターンを適用しない場合

useless_factory_method.py
#! /usr/bin/env python3
from typing import List

# 本番ではデータベースに接続するが、開発中はcsvファイルを使う


class FileDataObject:
    def __init__(self) -> None:
        self.user_list: List[str] = list()

        with open('test.csv', 'r') as f:
            for line in f:
                self.user_list.append(line)

    def fetch_user(self, row_num: int) -> str:
        return self.user_list[row_num]


class DBDataObject:
    """未実装クラスだが、将来的に FileDataObject と差し替える予定"""

    def __init__(self) -> None:
        # DBへの接続手続きなど
        pass

    def fetch_user(self, id_num: int) -> str:
        pass


class Client:
    def __init__(self) -> None:
        # あとでFileDataObjectからDbDataObjectへ変更するときは
        # ここで呼び出すクラスを変更すればよいという目論見
        self.data_object = FileDataObject()


if __name__ == '__main__':
    client = Client().data_object

    i: int = 0
    while True:
        try:
            print(client.fetch_user(i), end="")
            i += 1
        except IndexError:
            break

    user: str = client.fetch_user(1)
    print("\n")
    print(user)

実行結果

id,name,bike
1,ClimbingSinger,BMC
2,Proper,SCOTT
3,AttentionSeeker,PINARELLO

1,ClimbingSinger,BMC


Process finished with exit code 0

Client クラスの __init__ でどのオブジェクトを利用するか決めようとしています。
FileDataObjectDBDataObject の2つあるが、同じ機能を持ちつつもこのコード上ではお互い無関係。
たまに片方で修正したものをもう片方で実装を忘れることあるよね。

パターンその1

狙い

  • 利用者側はオブジェクトの中身を直接知らなくてもよいと嬉しい
  • 具象クラスのオブジェクト生成を一箇所にまとめたい
factory_method_1.py
#! /usr/bin/env python3
from abc import ABCMeta, abstractmethod
from typing import List


class DataObject(metaclass=ABCMeta):

    def __init__(self) -> None:
        pass

    @staticmethod
    def create():
        # Client側は DataObject がどのオブジェクトを呼んでいるか意識する必要がなくなる
        # Client内で複数 DataObject を呼び出している場合はその全てを修正する必要があるが
        # このコードでは create の戻り値を変更するだけで全体の修正が可能になる
        return FileDataObject()

    @abstractmethod
    def fetch_user(self, id_num: int) -> str:
        pass


class FileDataObject(DataObject):

    def __init__(self) -> None:
        super().__init__()
        self.user_list: List[str] = list()

        with open('test.csv', 'r') as f:
            for line in f:
                self.user_list.append(line)

    def fetch_user(self, id_num: int) -> str:
        return self.user_list[id_num]


class DBDataObject(DataObject):
    """未実装クラスだが、将来的に FileDataObject と差し替える予定"""

    def __init__(self) -> None:
        # DBへの接続手続きなど
        super().__init__()

    def fetch_user(self, id_num: int) -> str:
        pass


class Client:
    def __init__(self) -> None:
        self.data_object: DataObject = DataObject.create()


if __name__ == '__main__':

    client: DataObject = Client().data_object

    i = 0
    while True:
        try:
            print(client.fetch_user(i), end="")
            i += 1
        except IndexError:
            break

    user: str = client.fetch_user(1)
    print("\n")
    print(user)

  • 肝となるのは DataObject.create()
    • FileDataObjectを生成するのはこの中だけ。修正箇所があちこちに散らばらない。
    • 将来 DBDataObject に変更してもここで返すオブジェクトを変えるだけで、クライアント側のコードは修正しない。
  • @abstractmethodでデコレートした関数は子クラス側で実装し忘れると実行時に怒ってくれるので嬉しい。
    • DataObject.fetch_data_object() における実際の処理は FileDataObjectDBDataObject それぞれで実装したものが実行される。

パターンその2

狙い

  • 開発中から修正箇所が明確になっているプロジェクト・プロダクトばかりではない
  • 使用するクラスをクライアント側で決めたいこともある

ファクトリクラスを具象化しよう。

factory_method_2.py
#! /usr/bin/env python3
from abc import ABCMeta, abstractmethod
from enum import Enum, auto
from typing import List

# パターン1はファイルからDBへ切り替えが予定されていることが分かっている場合の設計で
# クライアント側のコードに影響を与えないことを大切にしている
# パターン2ではクライアント側からどのクラスを呼び出すか切り替えられる設計を目指す


class DBMode(Enum):
    STANDALONE: int = auto()  # ファイルからの読み書き
    NETWORKING: int = auto()  # DB からの読み書き


class DataObject(metaclass=ABCMeta):

    def __init__(self) -> None:
        pass

    @abstractmethod
    def fetch_user(self, id_num: int) -> str:
        pass


class FileDataObject(DataObject):

    def __init__(self) -> None:
        super().__init__()
        self.data_list: List[str] = list()

        with open('test.csv', 'r') as f:
            for line in f:
                self.data_list.append(line)

    def fetch_user(self, id_num) -> str:
        return self.data_list[id_num]


class DBDataObject(DataObject):
    """未実装クラスだが、将来的に FileDataObject と差し替える予定"""

    def __init__(self) -> None:
        # DBへの接続手続きなど
        super().__init__()

    def fetch_user(self, id_num: int) -> str:
        pass


class DataObjectFactory:
    # オブジェクト生成の責任を負う
    # create 時にどのクラスを呼び出すか1度決めてしまえば、以後は毎回クラスを指定する必要がなくなる

    def __init__(self, db_mode: Enum):
        self.db_mode: Enum = db_mode

    def create(self):
        if self.db_mode == DBMode.STANDALONE:
            return FileDataObject()
        if self.db_mode == DBMode.NETWORKING:
            return DBDataObject()


class Client:

    def __init__(self) -> None:
        # ここで DbMode を指定する
        self.data_object: DataObject = DataObjectFactory(DBMode.STANDALONE).create()


if __name__ == '__main__':
    client: DataObject = Client().data_object

    i: int = 0
    while True:
        try:
            print(client.fetch_user(i), end="")
            i += 1
        except IndexError:
            break

    row: str = client.fetch_user(2)
    print("\n")
    print(row)

  • 肝となるのは DataObjectFactory クラス
    • Client をインスタンス化する際に DBMode を決定するため、クライアント側の要求に応じたオブジェクトを返せる
  • クラスを切り替えためにはクライアントコード側を修正し、ファクトリーメソッド側は修正する必要がない

ただ、パターンその2はある事態に直面することになります。
それは『柔軟性と拡張性は高いが、その分だけ新たなクラスを作成してコード量が増える』というもの。
コードリーディングの大きな壁になりえるため、単純な解決策としてはパターン1を利用した方が無難です。

パターンその3

狙い

  • 既存の動作部分に影響を与えず、新クラスの定義で対応できるオブジェクトを増やせる設計にしたい

ファクトリクラスをインターフェースとしよう。

factory_method_3.py
#! /usr/bin/env python3
from abc import ABCMeta, abstractmethod
from typing import List


class DataObject(metaclass=ABCMeta):
    @abstractmethod
    def fetch_user(self, num: int) -> str:
        # read_data_objectは抽象メソッドで、
        # 実際の処理は子メソッドで実装したものが実行される
        pass


class FileDataObject(DataObject):

    def __init__(self) -> None:
        self.data_list: List[str] = list()

        with open('test.csv', 'r') as f:
            for line in f:
                self.data_list.append(line)

    def fetch_user(self, id_num: int) -> str:
        return self.data_list[id_num]


class DBDataObject(DataObject):
    """未実装クラスだが、将来的に FileDataObject と差し替える予定"""

    def __init__(self) -> None:
        # DBへの接続手続きなど
        pass

    def fetch_user(self, id_num: int) -> str:
        pass


class DefaultDataObject(DataObject):
    """デバッグ用クラスを実装する"""

    def __init__(self) -> None:
        pass

    def fetch_user(self, id_num: int) -> str:
        pass


class DataObjectFactory(object, metaclass=ABCMeta):

    def __init__(self) -> None:
        pass

    @abstractmethod
    def create(self):
        pass
    #     if self._db_type == DbMode.STANDALONE:
    #         return FileDataObject()
    #     if self._db_type == DbMode.NETWORKING:
    #         return DbDataObject()
    #
    #     if self._db_type == DbMode.DEBUGGING: <- このように追加するとコード修正時に他の部分へ影響が出るかもしれない
    #         return DefaultDataObject()
    #
    # DataObjectFactory クラスは、パターン2ではオブジェクト生成の責任を負うクラスだったが
    # パターン3ではこれを ABC を用いてインターフェースにする
    #
    # Python で Java のインターフェースのように振る舞うのは Mix-In であり
    # ABC を用いると手っ取り早いみたい?
    #
    # > ABC は直接的にサブクラス化することができ、ミックスイン(mix-in)クラスのように振る舞います。
    # 出典: https://docs.python.org/ja/3.7/library/abc.html
    #
    # .register の使い方がよくわからん。


class FileDataObjectFactory(DataObjectFactory):
    def __init__(self) -> None:
        super().__init__()

    # インターフェースを使用して DataObject を生成する
    def create(self) -> FileDataObject:
        return FileDataObject()


class DBDataObjectFactory(DataObjectFactory):

    def __init__(self) -> None:
        super().__init__()

    def create(self) -> DBDataObject:
        return DBDataObject()


class DefaultObjectFactory(DataObjectFactory):
    def __init__(self) -> None:
        super().__init__()

    def create(self) -> DefaultDataObject:
        return DefaultDataObject()


class Client:
    """DataObject側で行を取得できるようにしているので、このクラスで列指定とかしたい"""

    def __init__(self) -> None:
        pass


if __name__ == '__main__':

    client1: DataObject = FileDataObjectFactory().create()
    client2: DataObject = DBDataObjectFactory().create()
    client3: DataObject = DefaultObjectFactory().create()

    i = 0
    while True:
        try:
            print(client1.fetch_user(i), end="")
            i += 1
        except IndexError:
            break

    user = client1.fetch_user(3)
    print("\n")
    print(user)

  • パターンその2同様、クライアント側から利用するオブジェクトを指定していく
  • DataObject を利用したいときは DataObjectFactoryをインターフェースとした各 ObjectFactory たちを利用して追加していく
  • パターンその2のような条件文が並ぶことはない

生成するクラスが増える度に、それに対応するファクトリクラスを書かないといけないのが弱点。
他2つに比べてうまく飲み下せていない。

継承周り

ABC(抽象基底クラス)さんのおかげで実装忘れがなくなりホンマありがたいです。
今回はインターフェース(Mix-In?)をどうやって使うのかで悩んでいました。

PyCharm の Warning が消えなくて気持ち悪かったところ、メッセージには super() を使えと書いてある。
どうやって使うの? と検索かけたら[Python]クラス継承(super)という記事でわかりやすく解説されていてありがたかったです。
Singletonパターンでもsuper()を使ったものの「公式ドキュメントに書いてあるから」以外に使った理由を思い出せない……。
ドキュメントを読もう。

『おまじない』を撲滅しとうございます。

『おまじない』がほとんど見えないからPythonを好んでいるものの、見えないだけなんですよね。
Shebangで遊んでいます。仮想環境破壊したりとか。

3
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
6