Python | OOP:デザインパターン(Factory)

Python Python
スポンサーリンク

概要(Factoryは「作り方をひとまとめにして差し替える窓口」)

Factory(ファクトリ)パターンは、オブジェクトの“生成方法”を呼び手から切り離し、共通の窓口にまとめる設計です。呼び手は「何を作るか」を依頼するだけで、具体クラス名や生成手順を知らなくても使えます。効果は、密結合の解消、拡張時の修正範囲の局所化、テストでモックを差し込める柔軟性の確保です。


基本(シンプルファクトリで生成をひとまとめ)

最小例(種類を文字列で選ぶ)

class PdfReport:
    def generate(self) -> str: return "PDF生成"
class CsvReport:
    def generate(self) -> str: return "CSV生成"

def report_factory(kind: str):
    if kind == "pdf": return PdfReport()
    if kind == "csv": return CsvReport()
    raise ValueError("未知の種類")

r = report_factory("pdf")
print(r.generate())
Python

生成ロジックを関数(またはクラスメソッド)へ集約するだけで、呼び手から具体クラスの知識を取り除けます。種類追加時はファクトリの中身を足すだけで済み、呼び手を触らずに拡張できます。

マップで分岐を整理する(スケールしやすい)

REGISTRY = {
    "pdf": PdfReport,
    "csv": CsvReport,
}
def report_factory(kind: str):
    try:
        return REGISTRY[kind]()
    except KeyError:
        raise ValueError("未知の種類")
Python

分岐が増えるほど、テーブル駆動にすると見通しが良く、テストや差し替えが簡単になります。


ファクトリメソッド(“作り方”をサブクラスへ委ねる)

親が“枠”を提供し、子が“作り方”を決める

from abc import ABC, abstractmethod

class Report(ABC):
    @abstractmethod
    def generate(self) -> str: ...

class ReportCreator(ABC):
    @abstractmethod
    def create(self) -> Report: ...

class PdfCreator(ReportCreator):
    def create(self) -> Report: return PdfReport()

class CsvCreator(ReportCreator):
    def create(self) -> Report: return CsvReport()

def run(creator: ReportCreator) -> str:
    return creator.create().generate()

print(run(PdfCreator()))
Python

“ファクトリメソッド”は、生成処理をサブクラスへ委譲する構造です。呼び手はCreator(作り手)に依存し、具体クラスの追加・変更はCreator側で完結します。コンテキスト(run)は不変のまま差し替え可能になります。


抽象ファクトリ(関連オブジェクト一式をまとめて生成)

同じ“テーマ”の部品を一括で差し替える

from abc import ABC, abstractmethod

class Button(ABC): @abstractmethod
def render(self) -> str: ...
class TextBox(ABC): @abstractmethod
def render(self) -> str: ...

class DarkButton(Button): def render(self) -> str: return "<button class='dark'>"
class DarkTextBox(TextBox): def render(self) -> str: return "<input class='dark'>"

class LightButton(Button): def render(self) -> str: return "<button class='light'>"
class LightTextBox(TextBox): def render(self) -> str: return "<input class='light'>"

class WidgetFactory(ABC):
    @abstractmethod
    def create_button(self) -> Button: ...
    @abstractmethod
    def create_textbox(self) -> TextBox: ...

class DarkFactory(WidgetFactory):
    def create_button(self) -> Button: return DarkButton()
    def create_textbox(self) -> TextBox: return DarkTextBox()

class LightFactory(WidgetFactory):
    def create_button(self) -> Button: return LightButton()
    def create_textbox(self) -> TextBox: return LightTextBox()

def render_page(factory: WidgetFactory) -> str:
    b = factory.create_button().render()
    t = factory.create_textbox().render()
    return b + t

print(render_page(DarkFactory()))
Python

“抽象ファクトリ”は、関連する複数のオブジェクト群を“互換性のあるセット”として生成します。テーマ(ダーク/ライト、地域設定、DB種別)を切り替えるだけで、ページ全体やシステムの整合性を保ったまま差し替えが可能です。


重要ポイントの深掘り(依存性注入・設定駆動・テスト容易性)

依存性注入と組み合わせる(差し替えやすさの最大化)

ファクトリ(またはCreator)をコンストラクタで受け取り、上位から注入します。呼び手は“作り方”を知らず、渡されたファクトリに頼るだけなので、差し替え・テストが容易になります。

class ReportService:
    def __init__(self, factory):
        self.factory = factory
    def run(self) -> str:
        return self.factory().generate()
Python

設定駆動にする(実行時切替)

設定値(環境変数、設定ファイル)で種類を選び、レジストリからファクトリ(または具体クラス)を引く構造にすると、デプロイ環境ごとの挙動変更をコード改修無しで行えます。

テストダブルの注入で高速・安全なテスト

本番ファクトリの代わりに“テスト用ファクトリ(モック)”を注入して、外部I/Oを遮断したまま動作確認ができます。失敗パスの再現も簡単になり、信頼性が上がります。


よくある落とし穴と回避(分岐の再発・肥大化・漏れた依存)

ファクトリの中で分岐が増えすぎる

種類が増えても“テーブル駆動(マップ)”で管理し、if/elifを増やさないようにします。登録制(REGISTRY)にして、追加は“登録だけ”で済む形が読みやすいです。

ファクトリが“何でも屋”になる

生成以外の責務(検証、I/O、ログ)を抱えさせると肥大化します。ファクトリは“作るだけ”。前後処理は別レイヤーへ分離し、責務を細く保ちます。

具体クラスの知識が呼び手へ漏れる

ファクトリを使っているのに、呼び手が具体クラスへ直接アクセスすると結合が復活します。公開面は“インターフェース”に統一し、具体名はファクトリの内側に閉じ込めます。


例題(HTTPクライアントとストレージを“作り方”で差し替える)

HTTPクライアント(認証やタイムアウトを含む“作り方”)

import requests
class SessionFactory:
    def __init__(self, token: str, timeout: float = 5.0):
        self.token = token; self.timeout = timeout
    def create(self) -> requests.Session:
        s = requests.Session()
        s.headers.update({"Authorization": f"Bearer {self.token}"})
        # 他の設定もここで集約
        return s

class ApiClient:
    def __init__(self, session_factory: SessionFactory, base_url: str):
        self.session_factory = session_factory
        self.base_url = base_url
    def get_user(self, uid: str) -> dict:
        s = self.session_factory.create()
        r = s.get(f"{self.base_url}/users/{uid}", timeout=self.session_factory.timeout)
        r.raise_for_status()
        d = r.json()
        return {"id": str(d.get("id", "")), "name": d.get("name") or "unknown"}
Python

“作り方”を工場へまとめると、認証・共通設定の変更が工場だけで完結します。テストではフェイク工場を渡せば外部I/Oを遮断できます。

ストレージの切替(メモリ/ファイル/クラウド)

from abc import ABC, abstractmethod

class Store(ABC):
    @abstractmethod
    def put(self, k: str, v: str) -> None: ...
    @abstractmethod
    def get(self, k: str) -> str | None: ...

class MemoryStore(Store):
    def __init__(self): self._m = {}
    def put(self, k, v): self._m[k] = v
    def get(self, k): return self._m.get(k)

class FileStore(Store):
    def __init__(self, path: str): self.path = path
    def put(self, k, v): open(self.path + k, "w").write(v)
    def get(self, k):
        try: return open(self.path + k).read()
        except FileNotFoundError: return None

class StoreFactory(ABC):
    @abstractmethod
    def create(self) -> Store: ...

class MemoryStoreFactory(StoreFactory):
    def create(self) -> Store: return MemoryStore()

class FileStoreFactory(StoreFactory):
    def __init__(self, path: str): self.path = path
    def create(self) -> Store: return FileStore(self.path)

class Service:
    def __init__(self, factory: StoreFactory): self.factory = factory
    def save_user(self, uid: str, name: str):
        st = self.factory.create()
        st.put(uid, name)
Python

Storeの種類を増やしても、Serviceは工場から“作って使う”だけ。切替は工場の差し替えで完結します。


まとめ(Factoryで“作り方”を外へ出し、拡張とテストを軽くする)

Factoryは、具体クラスの生成をひとまとめにして呼び手から切り離す設計です。シンプルファクトリで集約し、ファクトリメソッドで委譲し、抽象ファクトリで関連オブジェクト一式を統一して差し替える。依存性注入・設定駆動と組み合わせれば、実行時切替とテスト容易性が劇的に向上します。責務を“作るだけ”に絞り、分岐のテーブル化で拡張をスムーズにする—これが初心者にもすぐ効く、実用的なFactoryの使い方です。

タイトルとURLをコピーしました