Factory Method(ファクトリーメソッド)は、オブジェクトを作る処理をメソッドに切り出し、どのクラスを生成するかをサブクラスに決めさせるGoFの生成パターンです。似た言葉のファクトリ関数は、オブジェクトを作って返す関数全般を指します。Pythonはクラスそのものを変数・引数・属性に入れて渡せるので、Javaのように継承で書かなくて済む場面が多くあります。この記事では、abc.ABCで書くFactory Methodを動くコードで示したうえで、ファクトリ関数、標準ライブラリのdefault_factory、classmethodの代替コンストラクタ、クラス属性による差し替え、デコレータと__init_subclass__による登録、entry_pointsによるプラグイン読み込みを並べ、どれを選ぶかの基準を示します。掲載コードは、CPython 3.11.15、3.12.13、3.13.14、3.14.6、3.15.0で実行した結果です(3.10.20でも、typing.Selfの例を除いて同じ出力になります)。
まとめ:PythonでのFactory Methodとファクトリ関数の要点
- Factory Methodは、生成用のメソッドを基底クラスで宣言し、生成するクラスはサブクラスが決めるパターンです。ファクトリ関数は、生成を関数に任せる書き方の総称で、継承を使いません。
- Pythonではabc.ABCと@abstractmethodで生成メソッドを抽象化します。実装し忘れたサブクラスは、インスタンスを作った時点でTypeErrorになり、エラーの文面は3.12で変わりました。
- 種類を文字列で選ぶだけならdictで振り分けるファクトリ関数、入力形式ごとに作り方を変えるならclassmethodの代替コンストラクタを使います。戻り値の型はtyping.Self(3.11以降)で書くと、サブクラスから呼んだときも型チェッカーがサブクラスとして扱います。
- 生成するクラスを差し替えたいだけなら、生成メソッドを上書きするより、クラス属性に生成クラスを置くか、クラスを引数で渡すほうが短く書けます。標準ライブラリのhttp.clientもクラス属性の形です。
- 生成クラスを後から増やすならデコレータか__init_subclass__で登録し、別パッケージから差し込むならentry_pointsを使います。
Factory Methodパターンの定義と4つの役割
GoF(Gamma、Helm、Johnson、Vlissides)が1994年の『Design Patterns』で示した意図は、「オブジェクトを生成するためのインタフェースを定義し、どのクラスをインスタンス化するかはサブクラスに決めさせる」というものです。生成をサブクラスへ先送りするので、基底クラスのコードには具体クラスの名前が出てきません。
登場する役割は4つです。この記事のコードでは次のクラスが対応します。
| 役割 | 担当すること | 本記事のクラス |
|---|---|---|
| Product | 生成されるオブジェクトの共通の型 | Notifier |
| ConcreteProduct | 実際に生成されるクラス | EmailNotifier、SlackNotifier |
| Creator | 生成メソッドを宣言し、それを使う処理を持つ | AlertService |
| ConcreteCreator | 生成メソッドを実装して具体クラスを返す | EmailAlertService、SlackAlertService |
Creatorの本体は、通知を送るalert()のような業務処理です。生成メソッドはその途中で呼ばれる部品にすぎません。処理の流れを基底クラスに固定し、一部をサブクラスに任せる構造はテンプレートメソッドパターンのものです。この例では、Template Methodに相当するalert()の中からFactory Methodを呼び出しています。
abc.ABCで書くFactory Methodの実装例
障害通知をメールかSlackで送るサービスを例にします。AlertServiceのalert()は、通知手段が何かを知らないまま処理を進め、手段の生成だけをcreate_notifier()に任せます。
from abc import ABC, abstractmethod
class Notifier(ABC):
@abstractmethod
def send(self, to: str, body: str) -> str: ...
class EmailNotifier(Notifier):
def send(self, to: str, body: str) -> str:
return f"email to {to}: {body}"
class SlackNotifier(Notifier):
def send(self, to: str, body: str) -> str:
return f"slack to #{to}: {body}"
class AlertService(ABC):
def alert(self, to: str, message: str) -> str:
notifier = self.create_notifier() # ここがFactory Method
return notifier.send(to, f"[ALERT] {message}")
@abstractmethod
def create_notifier(self) -> Notifier: ...
class EmailAlertService(AlertService):
def create_notifier(self) -> Notifier:
return EmailNotifier()
class SlackAlertService(AlertService):
def create_notifier(self) -> Notifier:
return SlackNotifier()
for service in (EmailAlertService(), SlackAlertService()):
print(service.alert("ops", "disk 90%"))
出力は次のとおりです。
email to ops: [ALERT] disk 90%
slack to #ops: [ALERT] disk 90%
AlertServiceのコードにはEmailNotifierもSlackNotifierも登場しません。SMS通知を足すときは、SmsNotifierとSmsAlertServiceを追加するだけで、alert()は書き換えずに済みます。既存コードを変えずに機能を足せるこの性質は、オープンクローズドの原則がいう「拡張に開き、修正に閉じる」状態にあたります。
生成メソッドを実装し忘れたときのTypeError
@abstractmethodを付けたメソッドが残っているクラスは、インスタンスを作れません。create_notifier()を書き忘れたサブクラスで試します。
class BrokenAlertService(AlertService):
pass
BrokenAlertService()
エラーの文面はPython 3.12で変わりました。
# Python 3.10 / 3.11
TypeError: Can't instantiate abstract class BrokenAlertService with abstract method create_notifier
# Python 3.12 〜 3.15.0
TypeError: Can't instantiate abstract class BrokenAlertService without an implementation for abstract method 'create_notifier'
エラーはcreate_notifier()を呼んだときではなく、インスタンスを作った時点で出ます。起動時やテストの初期化で対象クラスをインスタンス化すれば、通知処理より前に実装漏れを検出できます。通知時に初めてインスタンス化する設計では、その時点まで検出されません。テストで例外を検証するときは、文面全体ではなく例外の型かクラス名だけを確かめてください。3.11から3.12以降へ上げたとき、メッセージ全文を照合するテストは落ちます。
ファクトリ関数:関数で生成を振り分ける書き方
ファクトリ関数は、オブジェクトの生成や取得を担い、そのオブジェクトを返す関数のことです。Python公式ドキュメントも、collections.namedtuple()を「factory function for creating tuple subclasses with named fields」と説明しています。namedtupleが生成するクラスの使い方はPython namedtupleの解説で扱っています。
dictで生成クラスを選ぶファクトリ関数
通知手段を文字列で選ぶなら、クラスをdictに入れて引くだけで済みます。Notifierと2つの具体クラスは前のコードと同じです。
NOTIFIERS = {
"email": EmailNotifier,
"slack": SlackNotifier,
}
def create_notifier(kind: str) -> Notifier:
try:
notifier_cls = NOTIFIERS[kind]
except KeyError:
raise ValueError(f"unknown notifier: {kind!r}") from None
return notifier_cls()
print(create_notifier("slack").send("ops", "deploy done"))
create_notifier("sms")
slack to #ops: deploy done
Traceback (most recent call last):
...
ValueError: unknown notifier: 'sms'
Pythonではクラスもオブジェクトなので、dictの値にクラスを入れ、取り出して呼べばインスタンスになります。種類を足すときはNOTIFIERSに1行加えるだけで、if/elifの分岐は増えません。from Noneは、内部で起きたKeyErrorを例外の連鎖表示から隠し、呼び出し側に「未知の種類」という意味のエラーだけを見せるための指定です。
この形は、サブクラスで生成を差し替える構造を持たないので、GoFのFactory Methodではありません。Simple Factoryと呼ばれる書き方で、GoFの23パターンにも含まれていません。それでも設定ファイルやリクエストの値で生成クラスを選ぶ用途なら、ほとんどの場合これで足ります。
ファクトリ関数は毎回新しいオブジェクトを作るとも限りません。logging.getLogger()は公式ドキュメントに「All calls to this function with a given name return the same logger instance.」とあるとおり、同じ名前には同じロガーを返します。生成をコンストラクタの直接呼び出しから関数に移すと、こうしたキャッシュや使い回しを呼び出し側に知らせずに入れられます。
default_factoryに渡す生成関数(defaultdict・dataclass)
Pythonで「factory」という語に最初に出会うのは、collections.defaultdictとdataclasses.field()のdefault_factory引数でしょう。どちらも、値が必要になったときに引数なしで呼ばれる生成関数を受け取ります。defaultdictの公式ドキュメントは「If default_factory is not None, it is called without arguments」と定め、dataclasses.field()のdefault_factoryも「it must be a zero-argument callable」としています。
from collections import defaultdict
from dataclasses import dataclass, field
alerts = defaultdict(list)
alerts["ops"].append("disk 90%")
print(dict(alerts), alerts.default_factory)
@dataclass
class Ticket:
title: str
tags: list[str] = field(default_factory=list)
a, b = Ticket("a"), Ticket("b")
a.tags.append("urgent")
print(a.tags, b.tags)
@dataclass
class BadTicket:
tags: list[str] = []
{'ops': ['disk 90%']} <class 'list'>
['urgent'] []
Traceback (most recent call last):
...
ValueError: mutable default <class 'list'> for field tags is not allowed: use default_factory
default_factory=listを指定すると、インスタンスごとにlist()が呼ばれ、aとbは別のリストを持ちます。既定値に[]を直接書くと、クラス定義の時点でValueErrorになります。全インスタンスが1つのリストを共有してしまう事故を、dataclassが定義時に止めているわけです。この判定は3.11で広がりました。3.10まではlist・dict・setの3型だけを拒否していましたが、3.11以降はハッシュできない値をすべて拒否します(公式ドキュメントの変更注記)。__eq__だけを定義して__hash__を持たない自作クラスのインスタンスを既定値にすると、3.10では定義でき、3.11と3.15.0では同じValueErrorになりました。
ここでいうfactoryは、生成をサブクラスに任せるGoFの意味ではなく、「呼ぶと新しい値を返すもの」という意味です。listやdictのようなクラス、lambda、普通の関数のどれでも渡せます。
JavaScriptのファクトリ関数との違い
「ファクトリ関数」で検索すると、JavaScriptの記事が多く出てきます。JavaScriptでいうファクトリ関数は、classとnewを使わずにオブジェクトを返す関数で、クロージャに状態を閉じ込めて外から触れなくする用途で使われることが多いものです。
function createCounter(start = 0) {
let count = start;
return {
increment() { count += 1; return count; },
get value() { return count; },
};
}
const c = createCounter(10);
c.increment();
console.log(c.value, c.count);
11 undefined
Node.js v26.5.0で実行すると、countは返したオブジェクトのプロパティではないのでc.countはundefinedになり、値はvalue経由でしか読めません。この記事のPython例では、ファクトリ関数で生成するクラスを呼び出し側から隠しています。Pythonでもクロージャで状態を保持できるため、これは言語仕様上の違いではなく、掲載例の用途の違いです。
classmethodで作る代替コンストラクタ
同じクラスを、CSVの1行やタイムスタンプなど別の形式の入力から作りたいときは、classmethodを生成メソッドにします。標準ライブラリのdict.fromkeys()、int.from_bytes()、datetime.fromtimestamp()はいずれもこの形で、型の__dict__から取り出すとclassmethod_descriptorになっています。次の例は引用符を含まない2列の入力に限った簡略版です。一般的なCSVの読み取りにはcsv.readerを使ってください。
from datetime import date
class Invoice:
def __init__(self, number: str, issued: date) -> None:
self.number = number
self.issued = issued
@classmethod
def from_csv_row(cls, row: str) -> "Invoice":
number, issued = row.split(",")
return cls(number, date.fromisoformat(issued))
@staticmethod
def parse(row: str) -> "Invoice":
number, issued = row.split(",")
return Invoice(number, date.fromisoformat(issued))
class TaxInvoice(Invoice):
pass
print(type(TaxInvoice.from_csv_row("T-001,2026-09-01")).__name__)
print(type(TaxInvoice.parse("T-002,2026-09-02")).__name__)
TaxInvoice
Invoice
classmethod版はclsを呼ぶので、サブクラスから呼べばサブクラスのインスタンスが返ります。staticmethod版はクラス名Invoiceを直書きしているため、TaxInvoiceから呼んでもInvoiceが返ります。継承される可能性があるクラスの生成メソッドは、staticmethodではなくclassmethodで書いてください。clsの受け取り方やstaticmethodとの違いはPythonの@classmethodの解説で詳しく扱っています。
typing.Selfによるサブクラスの戻り値型の保持
上のfrom_csv_row()は実行時にはTaxInvoiceを返しますが、戻り値の注釈を”Invoice”と書いているため、型チェッカーはInvoiceとして扱います。TaxInvoiceにだけある属性を読むと、実行は通るのに型エラーになります。Python 3.11で追加されたtyping.Self(PEP 673)を戻り値に書くと、呼び出したクラスの型になります。
from datetime import date
from typing import Self, reveal_type
class Invoice:
def __init__(self, number: str, issued: date) -> None:
self.number = number
self.issued = issued
@classmethod
def from_csv_row(cls, row: str) -> Self:
number, issued = row.split(",")
return cls(number, date.fromisoformat(issued))
@classmethod
def from_csv_row_str(cls, row: str) -> "Invoice":
number, issued = row.split(",")
return cls(number, date.fromisoformat(issued))
class TaxInvoice(Invoice):
rate = 0.1
a = TaxInvoice.from_csv_row("T-001,2026-11-01")
b = TaxInvoice.from_csv_row_str("T-002,2026-11-01")
reveal_type(a)
reveal_type(b)
print(a.rate, b.rate)
Python 3.15.0で実行した結果と、mypy 2.4.1にかけた結果です。
# python invoice.py
Runtime type is 'TaxInvoice'
Runtime type is 'TaxInvoice'
0.1 0.1
# mypy invoice.py
invoice.py:27: note: Revealed type is "invoice.TaxInvoice"
invoice.py:28: note: Revealed type is "invoice.Invoice"
invoice.py:29: error: "Invoice" has no attribute "rate" [attr-defined]
Found 1 error in 1 file (checked 1 source file)
実行時はどちらもTaxInvoiceなので、この差はテストを書いても見つかりません。型チェッカーを回しているプロジェクトでは、代替コンストラクタの戻り値はSelfにしてください。3.10ではfrom typing import SelfがImportErrorになりますが、3.10は2026年10月にサポートが終了しています(Python開発者ガイドの版一覧)。サポート中の3.11以降だけを対象にするなら、そのまま使えます。3.15で入ったほかの変更はPython 3.15の新機能の解説にまとめています。
クラス属性で生成クラスを差し替える書き方
Brandon RhodesはPythonのデザインパターンを解説するサイトで、「The “Factory Method” pattern is a poor fit for Python.」と評価しています。クラスを引数や属性として渡せない言語のために考えられた形だから、というのが理由です。代わりに挙げている書き方の1つが、生成するクラスをクラス属性に置く方法です。標準ライブラリのhttp.client.HTTPConnectionがこの形をとっています。
CPython 3.14のLib/http/client.pyでは、HTTPConnectionがクラス属性response_class = HTTPResponseを持ち、getresponse()の中でresponse = self.response_class(self.sock, method=self._method)と生成しています。サブクラスでこの属性を書き換えるだけで、getresponse()が返す型が変わります。
import threading
from http.client import HTTPConnection, HTTPResponse
from http.server import BaseHTTPRequestHandler, HTTPServer
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.end_headers()
self.wfile.write(b"ok")
def log_message(self, *args):
pass
class LoggedResponse(HTTPResponse):
def begin(self):
super().begin()
print("status:", self.status)
class LoggedConnection(HTTPConnection):
response_class = LoggedResponse
server = HTTPServer(("127.0.0.1", 0), Handler)
thread = threading.Thread(target=server.handle_request)
thread.start()
conn = LoggedConnection("127.0.0.1", server.server_port)
conn.request("GET", "/")
res = conn.getresponse()
print(type(res).__name__, res.read())
thread.join()
server.server_close()
status: 200
LoggedResponse b'ok'
ローカルのHTTPサーバーに1回だけリクエストする例です。getresponse()には手を触れずに、応答オブジェクトの型だけが変わりました。
前の通知の例をこの形で書き直すと、生成メソッドを持たずに済みます。
class AlertService:
notifier_class: type[Notifier] = EmailNotifier
def alert(self, to: str, message: str) -> str:
return self.notifier_class().send(to, f"[ALERT] {message}")
class SlackAlertService(AlertService):
notifier_class = SlackNotifier
print(AlertService().alert("ops", "disk 90%"))
print(SlackAlertService().alert("ops", "disk 90%"))
service = AlertService()
service.notifier_class = SlackNotifier
print(service.alert("ops", "disk 90%"))
print(AlertService().alert("ops", "disk 90%"))
email to ops: [ALERT] disk 90%
slack to #ops: [ALERT] disk 90%
slack to #ops: [ALERT] disk 90%
email to ops: [ALERT] disk 90%
サブクラスは属性を1行書き換えるだけになりました。インスタンスに同名の属性を代入すると、そのインスタンスだけが別のクラスを使い、ほかのインスタンスはクラス属性のEmailNotifierのままです。型注釈のtype[Notifier]は「Notifierかそのサブクラスのクラスそのもの」を表します。生成時に引数が要らないなら、Notifierのインスタンスではなくクラスを持たせるこの形が、継承版のFactory Methodより短く書けます。
生成クラスの自動登録:デコレータ・__init_subclass__・entry_points
ファクトリ関数のdictは、種類を足すたびに手で1行書き足す必要があります。ここでは、クラスの定義時に登録する2つの方法と、インストール済みパッケージのメタデータから候補を見つける方法を紹介します。
デコレータによる登録
登録用のデコレータを用意すると、生成クラスの直前に1行書くだけでdictに入ります。基底クラスを継承させなくてよいので、既存のクラスにも付けられます。
from collections.abc import Callable
EXPORTERS: dict[str, type] = {}
def register(fmt: str) -> Callable[[type], type]:
def deco(cls: type) -> type:
if fmt in EXPORTERS:
raise ValueError(f"duplicate exporter format: {fmt!r}")
EXPORTERS[fmt] = cls
return cls
return deco
@register("csv")
class CsvExporter:
def export(self, rows):
return "\n".join(",".join(map(str, r)) for r in rows)
@register("tsv")
class TsvExporter:
def export(self, rows):
return "\n".join("\t".join(map(str, r)) for r in rows)
print(sorted(EXPORTERS))
print(EXPORTERS["tsv"]().export([[1, "a"]]))
['csv', 'tsv']
1 a
デコレータはクラスをそのまま返すので、CsvExporterは登録後も普通のクラスとして使えます。CsvExporterとTsvExporterは、値に区切り文字・改行・引用符が含まれない場合に限った簡略例です。
__init_subclass__によるサブクラスの自動登録
__init_subclass__は、そのクラスを継承したサブクラスが定義された時点で呼ばれるフックです。クラス定義の括弧に書いたキーワード引数を受け取れるので、形式名とクラスの対応をここで登録できます。次のCsvExporterも、値にカンマ・改行・引用符が含まれない場合に限定した簡略例です。一般的なCSV出力にはcsv.writerを使ってください。
import json
class Exporter:
registry: dict[str, type["Exporter"]] = {}
def __init_subclass__(cls, *, fmt: str, **kwargs) -> None:
super().__init_subclass__(**kwargs)
if fmt in Exporter.registry:
raise ValueError(f"duplicate exporter format: {fmt!r}")
Exporter.registry[fmt] = cls
@classmethod
def for_format(cls, fmt: str) -> "Exporter":
return cls.registry[fmt]()
def export(self, rows: list) -> str:
raise NotImplementedError
class CsvExporter(Exporter, fmt="csv"):
def export(self, rows: list) -> str:
return "\n".join(",".join(map(str, row)) for row in rows)
class JsonExporter(Exporter, fmt="json"):
def export(self, rows: list) -> str:
return json.dumps(rows)
print(sorted(Exporter.registry))
print(Exporter.for_format("json").export([[1, "a"]]))
['csv', 'json']
[[1, "a"]]
fmtを付け忘れたサブクラスは、定義した時点でTypeError: Exporter.__init_subclass__() missing 1 required keyword-only argument: 'fmt'になります。デコレータ版は付け忘れても登録されないだけで何も起きないので、登録漏れを定義時に検出したいなら__init_subclass__が向いています。同じfmtを別のクラスが名乗ったときは、import順で選ばれるクラスが入れ替わらないよう、どちらの例でも登録時にValueErrorで止めています。
見落としやすいのは、デコレータでも__init_subclass__でも、登録はクラス定義を実行したとき、つまりそのモジュールがimportされたときにしか起きない点です。CsvExporterを別モジュールに置くと、基底クラスだけをimportした段階のregistryは空の{}のままで、CsvExporterのモジュールをimportした時点で'csv'が入ります。サブクラスを別ファイルに分けるなら、生成前に対象サブクラスの定義を実行しておく必要があります。importはパッケージの__init__.pyでも、アプリケーションの初期化処理でも構いません。
entry_pointsによる別パッケージの生成クラス読み込み
生成クラスを本体とは別のパッケージで配布したいときは、パッケージのメタデータにあるentry pointsを使います。追加側のパッケージmyapp-csvは、pyproject.tomlとモジュール1つの構成にします。
myapp_csv/pyproject.toml
myapp_csv/src/myapp_csv/__init__.py
pyproject.tomlでは、entry-pointsテーブルにグループ名myapp.exporters、エントリポイント名csv、読み込むクラスの場所を書きます。
[build-system]
requires = ["setuptools>=61"]
build-backend = "setuptools.build_meta"
[project]
name = "myapp-csv"
version = "0.1.0"
[project.entry-points."myapp.exporters"]
csv = "myapp_csv:CsvExporter"
src/myapp_csv/__init__.pyには生成されるクラスだけを置きます。
class CsvExporter:
def export(self, rows):
return "\n".join(",".join(map(str, r)) for r in rows)
本体側はimportlib.metadata.entry_points()でグループと名前を指定して取り出し、load()でクラスを得ます。同じグループと名前を複数のパッケージが登録したときの扱いは読み込む側が決めることになるので、この例では候補が2つ以上あればLookupErrorにしています。
from importlib.metadata import entry_points
def load_exporter(name: str):
matches = list(entry_points(group="myapp.exporters", name=name))
if len(matches) > 1:
raise LookupError(f"duplicate exporter: {name}")
if not matches:
raise LookupError(f"exporter not installed: {name}")
return matches[0].load()
if __name__ == "__main__":
print(load_exporter("csv")().export([[1, "a"], [2, "b"]]))
load_exporter("xml")
追加パッケージをインストールしてから、本体側のload.pyを実行します。
pip install ./myapp_csv
python load.py
1,a
2,b
Traceback (most recent call last):
...
LookupError: exporter not installed: xml
本体のコードはmyapp_csvを一度もimportしていません。追加パッケージをpip installするだけで生成の候補が増えます。entry_points()にgroupやnameを渡して絞り込む書き方はPython 3.10からで、3.9以前のentry_points()は引数を取らずdictを返していました。3.15.0のvenvでも同じ出力になることを確かめています。
書き方の選び方とFactory Methodを使わないほうがよい場面
ここまでの書き方を、解きたい問題から逆引きすると次のようになります。
| やりたいこと | 選ぶ書き方 | 本記事の例 |
|---|---|---|
| 文字列や設定値で生成クラスを選ぶ | ファクトリ関数 | create_notifier("slack") |
| 同じクラスを別の入力形式から作る | classmethod+Self | Invoice.from_csv_row() |
| 生成するクラスだけをサブクラスで変える | クラス属性 | AlertService.notifier_class |
| 生成と一緒に別の振る舞いも変える | Factory Method | AlertService.create_notifier() |
| 既存のクラスを登録する | デコレータ | @register(“csv”) |
| サブクラスの登録漏れを定義時に止める | __init_subclass__ | Exporter.for_format() |
| 別パッケージから候補を足す | entry_points | load_exporter() |
筆者の判断基準は、Creatorのサブクラスが生成メソッドしか上書きしていないなら、継承版のFactory Methodをやめてクラス属性か引数渡しに置き換える、というものです。継承版を残す価値があるのは、サブクラスが生成物と一緒にリトライ回数や宛先の整形といった別の振る舞いも変える場合です。そのときは1つのサブクラスに変更点がまとまっているほうが読みやすくなります。設計パターンの選定から既存システムの作り直しまで任せたい場合は、フルスクラッチ開発で相談を受けています。
クラスの引数渡しによるサブクラス削減
使う側ごとに生成するクラスを変えたいなら、クラス属性より、クラスそのものを引数で受け取るほうが向いています。
from collections.abc import Callable
class AlertService:
def __init__(self, notifier_factory: Callable[[], Notifier]) -> None:
self._notifier_factory = notifier_factory
def alert(self, to: str, message: str) -> str:
return self._notifier_factory().send(to, f"[ALERT] {message}")
print(AlertService(SlackNotifier).alert("ops", "disk 90%"))
print(AlertService(lambda: EmailNotifier()).alert("ops", "disk 90%"))
slack to #ops: [ALERT] disk 90%
email to ops: [ALERT] disk 90%
引数の型は「引数なしで呼ぶとNotifierを返すもの」なので、クラスでもlambdaでも渡せます。依存するオブジェクトを外から渡すこの形は依存性の注入で、渡す役をフレームワークに任せる仕組みがIoCコンテナです。
テストでは送信しない偽のNotifierを渡すだけで済みます。pytestには、fixtureからデータではなく生成関数を返す「Factories as fixtures」という書き方があり、1つのテストの中で条件を変えたサービスを何度も作れます。pytest 9.2.0で実行した例です。
import pytest
class FakeNotifier(Notifier):
def __init__(self) -> None:
self.sent: list[tuple[str, str]] = []
def send(self, to: str, body: str) -> str:
self.sent.append((to, body))
return "fake"
@pytest.fixture
def make_service():
def _make() -> tuple[AlertService, FakeNotifier]:
notifier = FakeNotifier()
return AlertService(lambda: notifier), notifier
return _make
def test_alert_sends_once(make_service):
service, notifier = make_service()
service.alert("ops", "disk 90%")
assert notifier.sent == [("ops", "[ALERT] disk 90%")]
. [100%]
1 passed in 0.02s
継承版のFactory Methodでは、テスト専用のAlertServiceサブクラスで生成メソッドを上書きする方法があります。既存の具象サービスの生成メソッドをunittest.mock.patch.objectで差し替える方法も使えます。
Abstract Factory・Builderとの違い
Abstract Factoryは、関連する複数の部品(ボタンと入力欄をテーマごとにそろえる、など)をまとめて作るオブジェクトを用意するパターンです。Factory Methodが継承によって生成処理を差し替えるのに対し、Abstract Factoryは生成メソッドを複数持つオブジェクトごと差し替えます。Builderは、生成物を段階的に組み立てる手順を分離するパターンで、どのクラスを作るかではなく、どう組み立てるかを扱います。言語をまたいだ全体像はファクトリパターンの解説、BuilderはBuilderパターンの解説を参照してください。
よくある質問
ファクトリ関数とは何ですか?
オブジェクトの生成や取得を担い、そのオブジェクトを返す関数です。Pythonでは、クラスをdictに入れて文字列で引き、呼び出して返す形がよく使われます。この記事のdictで生成クラスを選ぶ例は、サブクラスで生成を差し替えないSimple Factoryの形です。ファクトリ関数全般がこの形に限定されるわけではありません。JavaScriptでは、classとnewを使わずにオブジェクトを返す関数を指すことが多い言葉です。
ファクトリーメソッドとは何ですか?
オブジェクトを作る処理を担うメソッドのことで、デザインパターンとしては、生成メソッドを基底クラスで宣言し、どのクラスを作るかをサブクラスに決めさせるFactory Methodパターンを指します。Pythonではclassmethodで書いた代替コンストラクタもファクトリメソッドと呼ばれるので、文脈でどちらの意味かを確かめてください。
Factory MethodとAbstract Factoryの違いは何ですか?
Factory Methodは、サブクラスによって生成処理を差し替えるパターンです。Abstract Factoryは、関連する複数の生成物を作るメソッド群をオブジェクトにまとめ、そのオブジェクトごと差し替えます。
Pythonでstaticmethodのファクトリメソッドは使えますか?
使えます。staticmethodはclsを自動で受け取らないため、本記事のように基底クラス名を直書きした実装では、サブクラスから呼んでも基底クラスのインスタンスが返ります。継承されうるクラスでは、clsを受け取るclassmethodで書くとサブクラスから呼んだときにサブクラスのインスタンスが返ります。
PythonのFactory Methodに抽象基底クラスは必須ですか?
必須ではありません。abc.ABCを使わず、基底クラスのcreate_notifier()でNotImplementedErrorを送出する形でもFactory Methodは書けます。abc.ABCを使う利点は、生成メソッドの実装漏れをインスタンス生成時のTypeErrorで検出できることです。生成物のNotifierのように、継承させずに型だけを示したいなら、Python 3.8で追加されたtyping.Protocolで構造的な型として宣言し、mypyなどの型チェッカーで確かめる方法もあります。