🎨

【デザインパターン】Pythonにおけるファクトリーメソッドの整理

に公開

はじめに

ソフトウェア開発において、オブジェクトをどのように生成するかは、コードの柔軟性や拡張性に大きな影響を与える。機能追加のたびに、オブジェクト生成部分のコードをあちこち修正するのは避けたい。

ここでは、オブジェクト生成にまつわる問題を解決するためのデザインパターン、「ファクトリーメソッド (Factory Method)」パターンについて、自身の理解を整理し、備忘録としてまとめておく。

背景と課題

ファクトリーメソッドがない場合に、どのような問題が起こりうるかを整理する。

例えば、様々な形式でレポートを出力する機能を考える。最初はPDF形式のみだったとする。

# report.py
class PdfReport:
    def generate(self):
        print("PDFレポートを生成します。")

# main.py
from report import PdfReport

report_type = "pdf"
report = None

if report_type == "pdf":
    report = PdfReport()
else:
    raise ValueError("未対応のレポート形式です。")

report.generate()

ここへ「CSV形式でも出力したい」という要求が追加されると、次のようになる。

# report.py
class PdfReport:
    def generate(self):
        print("PDFレポートを生成します。")

class CsvReport:
    def generate(self):
        print("CSVレポートを生成します。")

# main.py
from report import PdfReport, CsvReport

report_type = "csv"
report = None

# レポート形式が増えるたびに、ここのif文がどんどん長くなる...
if report_type == "pdf":
    report = PdfReport()
elif report_type == "csv":
    report = CsvReport()
# elif report_type == "excel":
#   ...
else:
    raise ValueError("未対応のレポート形式です。")

report.generate()

このコードでは、main.py(利用者側)が PdfReportCsvReport といった具体的なクラス名に直接依存してしまっている。いわゆる密結合な状態。

この実装の課題点は以下の通り。

  • 拡張性の低下: 新しいレポート形式を追加するたびに、main.pyのif文を修正する必要がある。
  • 保守性の低下: オブジェクト生成のロジックが利用者側のコードに散らばってしまい、管理が煩雑になる。
  • 再利用性の低下: main.pyが特定のレポートクラスと強く結びついているため、他の箇所で再利用しにくい。

パターンの概要

これらの課題を解決するのがファクトリーメソッドである。

  • 目的 (Intent)
    • オブジェクトを生成するためのインターフェースを定義し、どのクラスのインスタンスを生成するかはサブクラスに決定させる。
  • 適用シーン (When to Use)
    • 生成するオブジェクトの種類が実行時に決まる場合。
    • クラスが、自身のサブクラスで生成されるべきオブジェクトを指定できるようにしたい場合。
    • オブジェクト生成のロジックを、それを利用するクライアントコードから分離したい場合。
  • メリット
    • 疎結合: 利用者コードは具体的なクラス名を知る必要がなくなり、抽象的なインターフェースにのみ依存する。
    • 拡張性: 新しいクラスを追加する際、既存の利用者コードを変更することなく、新しい「具体的な工場」を追加するだけで対応できる。
    • 保守性: オブジェクト生成のロジックが「具体的な工場」に集約されるため、管理が容易になる。
  • デメリット
    • パターンを適用するために、多くのクラス(Creator, ConcreteCreator, Product...)を作成する必要があり、コードの構造が少し複雑になる可能性がある。

クラス図

ファクトリーメソッドパターンは、主に4つの役割から構成される。

  • Product (製品)
    • 生成されるオブジェクトの共通インターフェース。(例: Report
  • ConcreteProduct (具体的な製品)
    • Productインターフェースを実装する具体的なクラス。(例: PdfReport, CsvReport
  • Creator (作成者)
    • Productを生成する「ファクトリーメソッド」を宣言する。戻り値はProduct型。
    • CreatorProductのインターフェースは知っているが、具体的な実装クラスは知らない。
  • ConcreteCreator (具体的な作成者)
    • Creatorのファクトリーメソッドをオーバーライドし、具体的なConcreteProductのインスタンスを生成して返す。
[Creator]----------->[Product]
    ^                    ^
    |                    |
[ConcreteCreator]---->[ConcreteProduct]

実装例

先のレポート出力の例をファクトリーメソッドで改善したコード。

1. ProductとConcreteProductの実装

「製品」のインターフェースと、それを実装する具体的なクラスを定義。

# report.py
from abc import ABC, abstractmethod

# Product
class Report(ABC):
    @abstractmethod
    def generate(self):
        pass

# ConcreteProduct
class PdfReport(Report):
    def generate(self):
        print("PDFレポートを生成します。")

# ConcreteProduct
class CsvReport(Report):
    def generate(self):
        print("CSVレポートを生成します。")

2. CreatorとConcreteCreatorの実装

「作成者」の抽象クラスと、それを継承して具体的な製品を作るクラスを定義。

# report_factory.py
from abc import ABC, abstractmethod
from report import Report, PdfReport, CsvReport

# Creator
class ReportCreator(ABC):
    @abstractmethod
    def factory_method(self) -> Report:
        """これがファクトリーメソッド"""
        pass

    def create_report(self):
        report = self.factory_method()
        report.generate()

# ConcreteCreator
class PdfReportCreator(ReportCreator):
    def factory_method(self) -> Report:
        return PdfReport()

# ConcreteCreator
class CsvReportCreator(ReportCreator):
    def factory_method(self) -> Report:
        return CsvReport()

3. 利用者側のコード

ファクトリーメソッドを使った利用者側のコード。

# main.py
from report_factory import ReportCreator, PdfReportCreator, CsvReportCreator

def main(creator: ReportCreator):
    # 利用者側は、渡されたCreatorのメソッドを呼び出すだけ。
    # 具体的なレポートが何かを知る必要はない。
    creator.create_report()

# PDFレポートが欲しい場合
pdf_creator = PdfReportCreator()
main(pdf_creator)
# > PDFレポートを生成します。

# CSVレポートが欲しい場合
csv_creator = CsvReportCreator()
main(csv_creator)
# > CSVレポートを生成します。

main関数は、PdfReportCsvReportといった具体的なクラスを知らなくてもレポートを生成できる。main関数はReportCreatorという抽象にのみ依存している状態になった。

もし「Excel形式のレポートを追加したい」場合でも、main.pyの修正は不要。

# report.py に追加
class ExcelReport(Report):
    def generate(self):
        print("Excelレポートを生成します。")

# report_factory.py に追加
from report import ExcelReport

class ExcelReportCreator(ReportCreator):
    def factory_method(self) -> Report:
        return ExcelReport()

# main.py は修正不要!
from report_factory import ExcelReportCreator

excel_creator = ExcelReportCreator()
main(excel_creator)
# > Excelレポートを生成します。

これで機能拡張に強い、柔軟な構造を実現できる。

まとめ

改めてファクトリーメソッドパターンについて整理すると以下のようになる。

  • 課題: 利用者コードが具体的なクラスに依存すると、拡張性や保守性が低下する(密結合)。
  • 解決策: ファクトリーメソッドを使い、オブジェクトの生成を「具体的な工場(ConcreteCreator)」に任せることで、利用者コードと具体的な製品クラスを分離する(疎結合)。
  • 効果: 機能追加の際に既存コードの修正が不要になり、変更に強いシステムを構築できる。

Discussion