デザインパターン
目次
- 概要
- デザインパターンとは
- なぜパターンとして学ぶのか
- GoFの3分類
- パターン全体の地図
- 設計圧力から選ぶ
- 生成に関するパターン
- 構造に関するパターン
- 振る舞いに関するパターン
- よく使うパターンの構造
- リファクタリング例
- Pythonで見るGoF 23パターン
- Factory Method
- Abstract Factory
- Builder
- Singleton
- Prototype
- Adapter
- Decorator
- Facade
- Composite
- Proxy
- Bridge
- Flyweight
- Iterator
- Observer
- Strategy
- Chain of Responsibility
- Command
- State
- Template Method
- Mediator
- Memento
- Visitor
- Interpreter
- 使いどころと注意点
- 現代的な見方
- パターンを選ぶための観点
- Pythonで軽く書くための道具
- Refactoring.guru デザインパターン実装
- SourceMaking パターン実装
- まとめ
- 参考文献
概要
デザインパターンは、ソフトウェア設計で繰り返し現れる問題に対する、名前付きの解決方針です。コードの書き方そのものというより、責務の分け方、依存の向き、変更しやすい境界の作り方を共有するための語彙として役立ちます。
デザインパターンは、暗記して当てはめるものではなく、設計上の力のかかり方を説明するための言葉です。パターン名よりも、「何を変化から守りたいのか」「どの依存を切りたいのか」を見ることが重要です。
Pythonはサンプルに向いています。クラス、関数、プロトコル、デコレータ、クロージャを短く書けるため、パターンの意図と「現代的にはもっと簡単に書ける場合がある」という両方を示しやすいからです。
デザインパターンとは
デザインパターンは、特定の実装コードではなく、よくある設計問題に対する再利用可能な構造です。
たとえば次のような問題に名前を与えます。
- オブジェクト生成の複雑さを隠したい
- 実装クラスを差し替えたい
- 既存クラスのインターフェースを変換したい
- 状態変化を複数の対象へ通知したい
- 処理の手順は固定し、一部だけ差し替えたい
- 操作をあとで実行・取り消し・記録できる形にしたい
名前があると、設計の会話が短くなります。「ここはObserverに近い」「Factoryで生成責務を分けたい」のように、意図を共有しやすくなります。
なぜパターンとして学ぶのか
パターンを学ぶ価値は、実装テンプレートを覚えることではありません。設計上の典型的なトレードオフを知ることにあります。
パターンを知ると、次を考えやすくなります。
- 変更されやすい部分はどこか
- 依存が集中している部分はどこか
- テストしにくい理由は何か
- 条件分岐が増え続けている理由は何か
- 継承より委譲の方がよい場面はどこか
- どの抽象が、将来の変更を受け止める境界になるか
デザインパターンは、コードを難しくするための装飾ではありません。むしろ、複雑さを置く場所を明確にするための整理法です。
GoFの3分類
GoFの古典的な23パターンは、意図によって3つに分類されます。
| 分類 | 扱う問題 | 代表例 |
|---|---|---|
| 生成 | オブジェクトをどう作るか | Factory Method, Abstract Factory, Builder, Prototype, Singleton |
| 構造 | オブジェクトやクラスをどう組み合わせるか | Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy |
| 振る舞い | オブジェクト同士がどう協調するか | Strategy, Observer, Command, State, Template Method, Iterator |
この分類は、パターン名を覚えるためではなく、「いま困っているのは生成なのか、構造なのか、振る舞いなのか」を切り分けるために使うと効果的です。
パターン全体の地図
GoFパターンは数が多いため、最初から23個を横並びに覚えようとするとつらくなります。まずは「変更されやすい場所をどこへ逃がすか」で見ると整理できます。
実務で特に出会いやすいのは、Adapter、Facade、Decorator、Strategy、Observer、Command、Stateです。これらは、外部API、middleware、イベント、認証、課金、画面状態、ジョブキューなど、日常的な設計問題に直結します。
この図は、この章のTierを分類ごとに合計したものです。振る舞いパターンの比重が大きいのは、アプリケーションコードでは「何をするか」より「いつ、誰が、どの順序で、どの条件で協調するか」が複雑になりやすいためです。
設計圧力から選ぶ
パターンは名前から選ぶより、コードにかかっている圧力から選びます。
| 設計圧力 | 兆候 | 候補 |
|---|---|---|
| 生成が散らばる | if type == ... があちこちにある |
Factory Method, Abstract Factory |
| 初期化が長い | constructorの引数が増え続ける | Builder, dataclass, 設定オブジェクト |
| 外部APIが合わない | 呼び出し側が変換処理だらけ | Adapter |
| サブシステムが複雑 | 利用側が内部手順を知りすぎる | Facade |
| 横断処理を重ねたい | ログ、認可、計測、キャッシュが混ざる | Decorator, Proxy, middleware |
| 条件分岐が増える | 同じ分岐が複数箇所に出る | Strategy, State |
| 変化を複数箇所へ伝えたい | 更新後に通知先が増え続ける | Observer, Pub/Sub |
| 操作を遅延・記録したい | undo、retry、queueが必要 | Command |
判断フローとしては次のように考えます。
最後の「まず単純な関数・クラスで保つ」は重要です。パターンは複雑さを消すものではなく、複雑さを置く場所を決めるものです。まだ複雑さが存在しないなら、導入しない判断も設計です。
生成に関するパターン
生成パターンは、オブジェクトをどう作るかを扱います。生成処理が複雑なときや、具体クラスへの依存を弱めたいときに使われます。
| パターン | Tier | 目的 | 注意点 |
|---|---|---|---|
| Factory Method | ★★★★☆ | 生成する具体クラスを呼び出し側から隠す | 単純な分岐だけなら関数で十分なこともある |
| Abstract Factory | ★★★☆☆ | 関連するオブジェクト群をまとめて生成する | ファミリー単位の差し替えがないと重い |
| Builder | ★★★☆☆ | 複雑な生成手順を段階的に組み立てる | Pythonではdataclassやkeyword引数で足りることも多い |
| Singleton | ★★★☆☆ | インスタンスを一つに制限する | グローバル状態になりやすくテストを難しくする |
| Prototype | ★★☆☆☆ | 既存オブジェクトを複製して生成する | 共有参照とdeep copyに注意する |
Singletonは便利に見えますが、グローバル状態を増やし、テストを難しくすることがあります。現代の設計では、DIコンテナや明示的な依存注入で代替できる場面も多いです。
構造に関するパターン
構造パターンは、クラスやオブジェクトをどう組み合わせるかを扱います。
| パターン | Tier | 目的 | 典型例 |
|---|---|---|---|
| Adapter | ★★★★★ | 既存のインターフェースを期待する形へ変換する | 外部API、レガシーコードの接続 |
| Decorator | ★★★★★ | 既存オブジェクトへ振る舞いを重ねる | ログ、キャッシュ、認可、計測 |
| Facade | ★★★★★ | 複雑なサブシステムに単純な入口を作る | SDK、アプリケーションサービス |
| Composite | ★★★★☆ | 木構造を単一オブジェクトのように扱う | ファイルツリー、UIコンポーネント |
| Proxy | ★★★★☆ | 代理オブジェクトでアクセス制御や遅延処理を行う | lazy load、リモート呼び出し、権限確認 |
| Bridge | ★★☆☆☆ | 抽象と実装を分け、それぞれ独立に変化させる | UIと描画エンジンの分離 |
| Flyweight | ★★☆☆☆ | 大量の細かいオブジェクトで共有可能な状態を分ける | 文字、タイル、ゲームオブジェクト |
AdapterとFacadeは混同されやすいですが、焦点が違います。Adapterは「形を合わせる」ための変換で、Facadeは「複雑さを隠す」ための入口です。
振る舞いに関するパターン
振る舞いパターンは、オブジェクト同士の協調や処理の流れを扱います。
| パターン | Tier | 目的 | 典型例 |
|---|---|---|---|
| Iterator | ★★★★★ | 集合の内部構造を隠して順に取り出す | コレクション、generator |
| Observer | ★★★★★ | 状態変化を購読者へ通知する | イベント、通知、Pub/Sub |
| Strategy | ★★★★★ | アルゴリズムを差し替え可能にする | ソート、課金、認証方式 |
| Chain of Responsibility | ★★★★☆ | 複数の処理候補へ順番に渡す | middleware、バリデーション |
| Command | ★★★★☆ | 操作をオブジェクトとして表す | undo、ジョブキュー、監査ログ |
| State | ★★★★☆ | 状態ごとの振る舞いを分離する | ワークフロー、注文状態 |
| Template Method | ★★★☆☆ | 処理の骨格を固定し、一部を差し替える | フレームワークのhook |
| Mediator | ★★☆☆☆ | 多数のオブジェクトの直接依存を集約する | UIイベント、チャットルーム |
| Memento | ★★☆☆☆ | 内部状態を隠したまま保存・復元する | undo、スナップショット |
| Visitor | ★★☆☆☆ | データ構造と処理を分離する | ASTの解析、変換 |
| Interpreter | ★☆☆☆☆ | 文法や式を評価する | 小さなDSL |
条件分岐が増え続けるコードでは、StrategyやStateが有効なことがあります。ただし、分岐が少なく安定しているなら、単純な if や match の方が読みやすい場合もあります。
よく使うパターンの構造
実務で頻出するパターンは、構造を図で覚えると使いどころを判断しやすくなります。
Adapterは、呼び出し側が期待する形と既存APIの形を合わせます。
Decoratorは、同じインターフェースを保ったまま機能を重ねます。
Observerは、状態変化を複数の購読者へ伝えます。
StrategyとStateはどちらも分岐を外へ出しますが、意図が違います。Strategyは「アルゴリズムを選ぶ」、Stateは「状態遷移に応じて振る舞いが変わる」です。
Commandは、操作を値として扱うためのパターンです。キュー、retry、undo、監査ログと相性がよいです。
このような図は、実装の細部を固定するものではありません。むしろ、「どの依存を直接つながないようにしているか」を見るための補助線です。
リファクタリング例
パターンは、最初から導入するより、コードに痛みが出てから小さく導入する方がうまくいくことが多いです。たとえば、課金方式の分岐が増えてきたケースを考えます。
def price(plan: str, base: int) -> int:
if plan == "regular":
return base
if plan == "student":
return int(base * 0.8)
if plan == "campaign":
return int(base * 0.7)
raise ValueError(plan)
この段階では、まだ if で十分です。しかし、同じ plan 分岐が請求、表示、メール文面、監査ログに広がると、変更のたびに複数箇所を直すことになります。
この圧力が見えてきたら、StrategyとFactoryを組み合わせて、分岐を一箇所に寄せます。
from collections.abc import Callable
PriceRule = Callable[[int], int]
def regular(base: int) -> int:
return base
def student(base: int) -> int:
return int(base * 0.8)
def campaign(base: int) -> int:
return int(base * 0.7)
def price_rule(plan: str) -> PriceRule:
rules = {
"regular": regular,
"student": student,
"campaign": campaign,
}
return rules[plan]
def price(plan: str, base: int) -> int:
return price_rule(plan)(base)
図にすると、呼び出し側は price_rule だけを知り、個々の計算方式には直接依存しません。
ここで重要なのは、「Strategyパターンを使った」ことではなく、分岐の増殖を止めたことです。もし分岐が1箇所にしかないなら、最初の if のままでよい場合もあります。パターンは、複雑さが実際に増えている場所へ局所的に使います。
Pythonで見るGoF 23パターン
以下のサンプルは、パターンの構造を理解するための最小例です。実務では、Pythonの関数、Protocol、dataclass、標準ライブラリを使うことで、古典的なクラス構造より短く表現できることがあります。
Factory Method
Factory Methodは、呼び出し側を具体クラスから切り離します。生成方法を一箇所に集めることで、差し替えやテストがしやすくなります。
from dataclasses import dataclass
from typing import Protocol
class Notifier(Protocol):
def send(self, message: str) -> None:
...
@dataclass
class EmailNotifier:
address: str
def send(self, message: str) -> None:
print(f"email to {self.address}: {message}")
@dataclass
class SlackNotifier:
channel: str
def send(self, message: str) -> None:
print(f"slack {self.channel}: {message}")
def create_notifier(kind: str) -> Notifier:
if kind == "email":
return EmailNotifier("ops@example.com")
if kind == "slack":
return SlackNotifier("#alerts")
raise ValueError(f"unknown notifier: {kind}")
notifier = create_notifier("slack")
notifier.send("deploy completed")
使われ方: 通知方式、ストレージ、認証プロバイダ、外部APIクライアントなどを設定値で切り替える場面でよく使います。呼び出し側は Notifier だけを見ればよく、具体クラスの生成条件を知らなくて済みます。
見極め: 分岐が1箇所だけで増えないなら単純な関数で十分です。生成後のオブジェクトを複数箇所で同じ規則で作るようになったら、Factoryとして切り出す価値が出ます。
Abstract Factory
Abstract Factoryは、関連するオブジェクト群をまとめて生成します。UIのテーマ、DBドライバ、クラウドプロバイダ差し替えのように、部品の組み合わせを一貫させたいときに使います。
from typing import Protocol
class Button(Protocol):
def render(self) -> str:
...
class Checkbox(Protocol):
def render(self) -> str:
...
class LightButton:
def render(self) -> str:
return "light button"
class LightCheckbox:
def render(self) -> str:
return "light checkbox"
class DarkButton:
def render(self) -> str:
return "dark button"
class DarkCheckbox:
def render(self) -> str:
return "dark checkbox"
class UIFactory(Protocol):
def create_button(self) -> Button:
...
def create_checkbox(self) -> Checkbox:
...
class LightFactory:
def create_button(self) -> Button:
return LightButton()
def create_checkbox(self) -> Checkbox:
return LightCheckbox()
class DarkFactory:
def create_button(self) -> Button:
return DarkButton()
def create_checkbox(self) -> Checkbox:
return DarkCheckbox()
def render_settings(factory: UIFactory) -> None:
print(factory.create_button().render())
print(factory.create_checkbox().render())
render_settings(DarkFactory())
使われ方: UIテーマ、クラウド環境、DB方言、テスト用/本番用の依存セットなど、関連する部品をまとめて差し替える場面で使います。DarkFactory はdark用のbuttonとcheckboxを同じファミリーとして作ります。
見極め: 単一オブジェクトの生成だけならFactory Methodで足ります。複数の部品の組み合わせ整合性を守りたいときにAbstract Factoryが効きます。
Builder
Builderは、複雑な生成手順を段階的に組み立てます。Pythonではkeyword引数や dataclass で十分なことも多いですが、手順や検証をまとめたいときに有効です。
from dataclasses import dataclass, field
@dataclass
class Query:
table: str
columns: list[str] = field(default_factory=list)
where: str | None = None
class QueryBuilder:
def __init__(self, table: str) -> None:
self.query = Query(table)
def select(self, *columns: str) -> "QueryBuilder":
self.query.columns.extend(columns)
return self
def filter(self, condition: str) -> "QueryBuilder":
self.query.where = condition
return self
def build(self) -> str:
columns = ", ".join(self.query.columns) or "*"
sql = f"SELECT {columns} FROM {self.query.table}"
if self.query.where:
sql += f" WHERE {self.query.where}"
return sql
sql = QueryBuilder("users").select("id", "name").filter("active = true").build()
print(sql)
使われ方: SQL、HTTP request、設定オブジェクト、複雑な検索条件など、段階的に組み立てる値に向きます。メソッドチェーンにすると、生成手順を読みやすくできます。
見極め: Pythonではkeyword引数や dataclass で十分な場合が多いです。手順の順序、検証、デフォルト、派生値が増えてきたときにBuilderを検討します。
Singleton
Singletonは、インスタンスを一つに制限します。ただしグローバル状態になりやすいため、Pythonでは明示的に依存として渡す方が扱いやすいことも多いです。
class Settings:
_instance: "Settings | None" = None
def __new__(cls) -> "Settings":
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance.debug = False
return cls._instance
first = Settings()
second = Settings()
second.debug = True
print(first is second)
print(first.debug)
使われ方: 設定、ロガー、メトリクス送信器など「プロセス内で1つだけ」としたい対象で見かけます。例では Settings() を何度呼んでも同じインスタンスになります。
見極め: テストしにくいグローバル状態になりやすいため、乱用は避けます。多くの場合、アプリケーション起動時に1回作って依存として渡す方が安全です。
Prototype
Prototypeは、既存オブジェクトを複製して新しいオブジェクトを作ります。設定テンプレートやゲームオブジェクトの初期値複製などに向きます。
from copy import deepcopy
from dataclasses import dataclass, field
@dataclass
class ServerConfig:
image: str
env: dict[str, str] = field(default_factory=dict)
base = ServerConfig("app:latest", {"LOG_LEVEL": "INFO"})
staging = deepcopy(base)
staging.env["LOG_LEVEL"] = "DEBUG"
print(base)
print(staging)
使われ方: 設定テンプレート、フォームの初期状態、ゲームオブジェクト、ワークフロー定義など、既存の雛形から少し変えたものを作る場面で使います。
見極め: shallow copyとdeep copyの違いに注意します。辞書やリストのようなmutableな値を共有してしまうと、片方の変更が別のオブジェクトへ漏れます。
Adapter
Adapterは、既存クラスのインターフェースを、利用側が期待する形へ変換します。
from typing import Protocol
class LegacyPaymentGateway:
def pay_in_yen(self, yen: int) -> bool:
print(f"legacy payment: {yen} JPY")
return True
class PaymentPort(Protocol):
def charge(self, amount: int) -> bool:
...
class PaymentAdapter:
def __init__(self, gateway: LegacyPaymentGateway) -> None:
self.gateway = gateway
def charge(self, amount: int) -> bool:
return self.gateway.pay_in_yen(amount)
def purchase(payment: PaymentPort) -> None:
if payment.charge(3000):
print("purchase completed")
purchase(PaymentAdapter(LegacyPaymentGateway()))
使われ方: 外部SDK、古いクラス、別チームのAPI、テストダブルなどを、自分たちのアプリケーションが期待する PaymentPort に合わせる場面で使います。
見極め: Adapterは境界に置くのが基本です。ドメインロジックの中に外部APIの都合を持ち込まず、入口で変換してから内部モデルへ渡すと保守しやすくなります。
Decorator
Decoratorは、既存の処理に追加の振る舞いを重ねます。Pythonでは言語機能としてのdecoratorがあるため、パターンの意図を短く表現できます。
from collections.abc import Callable
from functools import wraps
from time import perf_counter
def measure(func: Callable[..., str]) -> Callable[..., str]:
@wraps(func)
def wrapper(*args: object, **kwargs: object) -> str:
start = perf_counter()
result = func(*args, **kwargs)
elapsed = perf_counter() - start
print(f"{func.__name__}: {elapsed:.4f}s")
return result
return wrapper
@measure
def render_report() -> str:
return "report"
print(render_report())
使われ方: ログ、計測、キャッシュ、認可、リトライ、トランザクション境界など、主処理の前後に横断的な処理を重ねる場面で使います。Pythonのdecorator構文はこの用途に特に自然です。
見極め: 重ねすぎると実行順序が追いづらくなります。副作用のあるdecoratorは名前と責務を明確にし、テストで挙動を固定します。
Facade
Facadeは、複雑なサブシステムに単純な入口を作ります。呼び出し側が細かい手順を知らなくてよいようにします。
class AuthService:
def verify(self, token: str) -> bool:
return token == "valid"
class ProfileService:
def load_name(self) -> str:
return "Ada"
class UserFacade:
def __init__(self) -> None:
self.auth = AuthService()
self.profile = ProfileService()
def current_user_name(self, token: str) -> str | None:
if not self.auth.verify(token):
return None
return self.profile.load_name()
print(UserFacade().current_user_name("valid"))
使われ方: 複数サービス呼び出し、SDK、外部連携、アプリケーションサービス層など、利用側に細かい手順を見せたくない場面で使います。例では認証とプロフィール取得を UserFacade にまとめています。
見極め: Facadeは複雑さを消すのではなく隠します。内部が肥大化しすぎると巨大な神クラスになるため、Facade自体の責務も分割できるように保ちます。
Composite
Compositeは、木構造の葉と枝を同じインターフェースで扱います。ファイルツリーやUIコンポーネントでよく使われます。
from dataclasses import dataclass, field
from typing import Protocol
class Node(Protocol):
def size(self) -> int:
...
@dataclass
class File:
name: str
bytes: int
def size(self) -> int:
return self.bytes
@dataclass
class Directory:
name: str
children: list[Node] = field(default_factory=list)
def size(self) -> int:
return sum(child.size() for child in self.children)
root = Directory("root", [File("a.txt", 100), Directory("logs", [File("app.log", 900)])])
print(root.size())
使われ方: ファイルシステム、メニュー、UIコンポーネント、組織階層、カテゴリツリーなど、葉と枝を同じ操作で扱いたい場面で使います。File も Directory も size() を持つため再帰的に集計できます。
見極め: 木構造でないものに無理に適用すると分かりにくくなります。親子関係、再帰処理、同一インターフェースの3つが揃うと有効です。
Proxy
Proxyは、対象オブジェクトへのアクセスを代理します。遅延ロード、権限確認、キャッシュ、リモート呼び出しなどに使えます。
class Report:
def render(self) -> str:
return "expensive report"
class LazyReportProxy:
def __init__(self) -> None:
self._report: Report | None = None
def render(self) -> str:
if self._report is None:
print("loading report")
self._report = Report()
return self._report.render()
proxy = LazyReportProxy()
print(proxy.render())
print(proxy.render())
使われ方: lazy load、アクセス制御、キャッシュ、レート制限、リモート呼び出しの代理などで使います。例では重い Report を必要になるまで作りません。
見極め: Proxyは本体と同じように見えるため、裏で通信やI/Oが走ると利用者が驚くことがあります。遅延や失敗の可能性があるProxyはドキュメント化します。
Bridge
Bridgeは、抽象と実装を分け、それぞれを独立に変更できるようにします。UIと描画先、通知内容と送信経路などの分離に使えます。
from typing import Protocol
class Renderer(Protocol):
def draw_text(self, text: str) -> None:
...
class HtmlRenderer:
def draw_text(self, text: str) -> None:
print(f"<p>{text}</p>")
class ConsoleRenderer:
def draw_text(self, text: str) -> None:
print(text)
class Page:
def __init__(self, renderer: Renderer) -> None:
self.renderer = renderer
def show_title(self, title: str) -> None:
self.renderer.draw_text(title.upper())
Page(HtmlRenderer()).show_title("design patterns")
Page(ConsoleRenderer()).show_title("design patterns")
使われ方: 抽象側と実装側が別々に増える場合に使います。例では Page の表示ロジックと Renderer の出力先を分け、HTML/Consoleを独立に差し替えています。
見極め: 単なる依存注入と似ています。抽象の種類と実装の種類がどちらも増える見込みがあるとき、継承階層の爆発を避けるためにBridgeが役立ちます。
Flyweight
Flyweightは、大量のオブジェクトで共有できる状態を分離します。文字、タイル、アイコンなどを大量に扱う場面に向きます。
from dataclasses import dataclass
@dataclass(frozen=True)
class Glyph:
char: str
font: str
class GlyphFactory:
def __init__(self) -> None:
self._cache: dict[tuple[str, str], Glyph] = {}
def get(self, char: str, font: str) -> Glyph:
key = (char, font)
if key not in self._cache:
self._cache[key] = Glyph(char, font)
return self._cache[key]
factory = GlyphFactory()
a = factory.get("A", "Noto Sans")
b = factory.get("A", "Noto Sans")
print(a is b)
使われ方: 文字、アイコン、地図タイル、ゲームの粒子など、同じ内部状態を持つ小さなオブジェクトが大量に出る場面で使います。共有できる状態をcacheし、個別状態は外側で持ちます。
見極め: メモリ削減が主目的なので、対象数が少ない場合は不要です。共有する状態をimmutableに保つと、意図しない変更を避けやすくなります。
Iterator
Iteratorは、集合の内部構造を隠して順に値を取り出します。Pythonではiterator protocolとgeneratorが言語に組み込まれています。
from collections.abc import Iterator
class Countdown:
def __init__(self, start: int) -> None:
self.start = start
def __iter__(self) -> Iterator[int]:
current = self.start
while current > 0:
yield current
current -= 1
for value in Countdown(3):
print(value)
使われ方: コレクション、ファイル読み込み、ページングAPI、ストリーム処理などで使います。Pythonでは __iter__ と yield により、Iteratorは日常的な構文になっています。
見極め: 全件をリスト化せず、必要な分だけ順に処理したいときに有効です。I/OやDB cursorと組み合わせる場合は、リソース解放のタイミングに注意します。
Observer
Observerは、ある対象の状態変化を複数の購読者へ通知します。
from collections.abc import Callable
Subscriber = Callable[[str], None]
class EventBus:
def __init__(self) -> None:
self._subscribers: list[Subscriber] = []
def subscribe(self, subscriber: Subscriber) -> None:
self._subscribers.append(subscriber)
def publish(self, event: str) -> None:
for subscriber in self._subscribers:
subscriber(event)
def write_log(event: str) -> None:
print(f"log: {event}")
def send_metric(event: str) -> None:
print(f"metric: {event}")
bus = EventBus()
bus.subscribe(write_log)
bus.subscribe(send_metric)
bus.publish("user_registered")
使われ方: イベント通知、UI更新、ドメインイベント、監視、Pub/Subなどで使います。発行側は購読者を具体的に知らず、イベントだけを通知します。
見極め: 便利ですが、処理の流れが見えにくくなりがちです。イベント名、payload、購読解除、失敗時の扱いを設計しておくと事故が減ります。
Strategy
Strategyは、アルゴリズムを差し替え可能にするパターンです。Pythonでは、クラスではなく関数を渡すだけで自然に表現できます。
from collections.abc import Callable
Discount = Callable[[int], int]
def no_discount(price: int) -> int:
return price
def seasonal_discount(price: int) -> int:
return int(price * 0.9)
def vip_discount(price: int) -> int:
return int(price * 0.8)
def checkout(price: int, discount: Discount) -> int:
return discount(price)
print(checkout(10_000, seasonal_discount))
print(checkout(10_000, vip_discount))
使われ方: 課金計算、割引、検索、ソート、認証方式、圧縮方式など、同じ入口でアルゴリズムだけを差し替える場面で使います。Pythonでは関数を渡すだけで軽く表現できます。
見極め: 条件分岐が増え続けるときに有効です。一方、分岐が2個程度で増えないなら、単純な if の方が読みやすいこともあります。
Chain of Responsibility
Chain of Responsibilityは、処理を複数の候補へ順番に渡します。Web middlewareやバリデーションの連鎖でよく使われます。
from collections.abc import Callable
Handler = Callable[[dict[str, str]], str | None]
def require_user(request: dict[str, str]) -> str | None:
return None if request.get("user") else "401 Unauthorized"
def require_admin(request: dict[str, str]) -> str | None:
return None if request.get("role") == "admin" else "403 Forbidden"
def handle(request: dict[str, str], handlers: list[Handler]) -> str:
for handler in handlers:
result = handler(request)
if result is not None:
return result
return "200 OK"
print(handle({"user": "ada", "role": "admin"}, [require_user, require_admin]))
使われ方: Web middleware、入力バリデーション、権限チェック、ログ処理、問い合わせの段階的処理などで使います。各handlerは「処理する」か「次へ渡す」かを決めます。
見極め: 順序依存が強いので、handlerの並びを設定として明確にします。途中で止まる条件と、最後まで通った場合の挙動をテストすると安全です。
Command
Commandは、操作をオブジェクトとして表します。実行、取り消し、キューイング、ログ保存に向いています。
from dataclasses import dataclass
from typing import Protocol
class Command(Protocol):
def execute(self) -> None:
...
def undo(self) -> None:
...
@dataclass
class RenameFile:
old_name: str
new_name: str
def execute(self) -> None:
print(f"rename {self.old_name} -> {self.new_name}")
def undo(self) -> None:
print(f"rename {self.new_name} -> {self.old_name}")
history: list[Command] = []
command = RenameFile("draft.md", "article.md")
command.execute()
history.append(command)
last = history.pop()
last.undo()
使われ方: undo/redo、ジョブキュー、監査ログ、バッチ実行、GUI操作などで使います。操作をオブジェクトにすることで、あとから実行・保存・取り消しできます。
見極め: 単純な関数呼び出しで十分な場面では重いです。操作を履歴として扱う、再実行する、取り消す、キューに積む必要が出たらCommandが自然です。
State
Stateは、状態ごとの振る舞いをクラスや関数に分離します。状態遷移が増えて if が肥大化したときに有効です。
from typing import Protocol
class OrderState(Protocol):
def pay(self, order: "Order") -> None:
...
def ship(self, order: "Order") -> None:
...
class Pending:
def pay(self, order: "Order") -> None:
print("paid")
order.state = Paid()
def ship(self, order: "Order") -> None:
print("cannot ship before payment")
class Paid:
def pay(self, order: "Order") -> None:
print("already paid")
def ship(self, order: "Order") -> None:
print("shipped")
order.state = Shipped()
class Shipped:
def pay(self, order: "Order") -> None:
print("already shipped")
def ship(self, order: "Order") -> None:
print("already shipped")
class Order:
def __init__(self) -> None:
self.state: OrderState = Pending()
def pay(self) -> None:
self.state.pay(self)
def ship(self) -> None:
self.state.ship(self)
order = Order()
order.ship()
order.pay()
order.ship()
使われ方: 注文、チケット、ワークフロー、接続状態、ゲームキャラクターなど、状態によって許可される操作が変わる場面で使います。状態ごとの処理をクラスに分離できます。
見極め: 状態数が少ないうちは match やテーブル駆動で十分です。状態ごとの振る舞いが増え、遷移ルールが複雑になったらStateを検討します。
Template Method
Template Methodは、処理の骨格を基底側に置き、一部だけサブクラスで差し替えます。継承を使うため、委譲で十分かも確認します。
from abc import ABC, abstractmethod
class DataImporter(ABC):
def run(self, path: str) -> None:
raw = self.read(path)
rows = self.parse(raw)
self.save(rows)
def read(self, path: str) -> str:
return "name,score\nAda,100"
@abstractmethod
def parse(self, raw: str) -> list[dict[str, str]]:
...
def save(self, rows: list[dict[str, str]]) -> None:
print(rows)
class CsvImporter(DataImporter):
def parse(self, raw: str) -> list[dict[str, str]]:
header, line = raw.splitlines()
keys = header.split(",")
values = line.split(",")
return [dict(zip(keys, values))]
CsvImporter().run("scores.csv")
使われ方: フレームワーク、インポート処理、テストfixture、バッチ処理など、全体手順は固定で一部だけ差し替える場面で使います。例では読み込み、解析、保存の骨格を基底クラスに置いています。
見極め: 継承に依存するため、階層が深くなると追いづらくなります。Pythonでは高階関数や委譲で同じ意図を軽く表現できる場合もあります。
Mediator
Mediatorは、多数のオブジェクトが互いに直接参照し合うのを避け、やり取りを仲介役へ集めます。
class ChatRoom:
def __init__(self) -> None:
self.users: list[User] = []
def join(self, user: "User") -> None:
self.users.append(user)
user.room = self
def send(self, sender: "User", message: str) -> None:
for user in self.users:
if user is not sender:
user.receive(sender.name, message)
class User:
def __init__(self, name: str) -> None:
self.name = name
self.room: ChatRoom | None = None
def send(self, message: str) -> None:
if self.room:
self.room.send(self, message)
def receive(self, sender: str, message: str) -> None:
print(f"{self.name} <- {sender}: {message}")
room = ChatRoom()
ada = User("Ada")
linus = User("Linus")
room.join(ada)
room.join(linus)
ada.send("hello")
使われ方: UIコンポーネント間の連携、チャットルーム、複数サービスの調整役などで使います。オブジェクト同士が直接参照し合う網目状の依存を避けられます。
見極め: Mediatorにロジックが集まりすぎると新しい神オブジェクトになります。単なる連絡係か、ビジネスルールを持つ調整役かを分けて考えます。
Memento
Mementoは、オブジェクトの内部状態を隠したまま保存・復元します。undoやスナップショットで使われます。
from dataclasses import dataclass
@dataclass(frozen=True)
class EditorSnapshot:
text: str
class Editor:
def __init__(self) -> None:
self.text = ""
def type(self, value: str) -> None:
self.text += value
def save(self) -> EditorSnapshot:
return EditorSnapshot(self.text)
def restore(self, snapshot: EditorSnapshot) -> None:
self.text = snapshot.text
editor = Editor()
editor.type("hello")
snapshot = editor.save()
editor.type(" world")
editor.restore(snapshot)
print(editor.text)
使われ方: undo、エディタの履歴、フォームの一時保存、ゲームのセーブポイントなどで使います。内部状態を外から直接触らず、snapshotとして保存します。
見極め: snapshotが大きい場合はメモリコストに注意します。差分保存、永続化、履歴数制限などと組み合わせることが多いです。
Visitor
Visitorは、データ構造と処理を分離します。ASTの評価、整形、型検査のように、同じ構造に複数の処理を追加したい場合に使われます。
from dataclasses import dataclass
from typing import Protocol
class Visitor(Protocol):
def visit_number(self, node: "Number") -> int:
...
def visit_add(self, node: "Add") -> int:
...
class Expr(Protocol):
def accept(self, visitor: Visitor) -> int:
...
@dataclass
class Number:
value: int
def accept(self, visitor: Visitor) -> int:
return visitor.visit_number(self)
@dataclass
class Add:
left: Expr
right: Expr
def accept(self, visitor: Visitor) -> int:
return visitor.visit_add(self)
class Evaluator:
def visit_number(self, node: Number) -> int:
return node.value
def visit_add(self, node: Add) -> int:
return node.left.accept(self) + node.right.accept(self)
expr = Add(Number(2), Add(Number(3), Number(4)))
print(expr.accept(Evaluator()))
使われ方: ASTの評価、整形、型検査、コード生成など、同じデータ構造に複数の処理を追加したい場面で使います。データ構造と処理を分離できます。
見極め: 新しい処理を追加しやすい反面、新しいノード型を追加するとvisitor側の実装が増えます。データ構造が安定し、処理種類が増える場合に向きます。
Interpreter
Interpreterは、小さな文法や式を評価します。大きな言語処理系ではなく、設定式や検索条件のような小さなDSLに向きます。
from dataclasses import dataclass
from typing import Protocol
class Expr(Protocol):
def eval(self, env: dict[str, int]) -> int:
...
@dataclass
class Number:
value: int
def eval(self, env: dict[str, int]) -> int:
return self.value
@dataclass
class Var:
name: str
def eval(self, env: dict[str, int]) -> int:
return env[self.name]
@dataclass
class Add:
left: Expr
right: Expr
def eval(self, env: dict[str, int]) -> int:
return self.left.eval(env) + self.right.eval(env)
expr = Add(Var("x"), Number(3))
print(expr.eval({"x": 7}))
使われ方: 小さなDSL、フィルタ式、ルールエンジン、検索条件、計算式の評価などで使います。例では式木を作り、環境 env に基づいて評価しています。
見極め: 文法が大きくなるならparser generatorや既存ライブラリを使う方が安全です。小さく閉じた式言語に限定すると扱いやすいです。
使いどころと注意点
デザインパターンは、使えば設計がよくなる魔法ではありません。問題がない場所に先回りして導入すると、抽象が増えすぎて読みにくくなります。
導入を考える目安は次の通りです。
- 同じ種類の分岐や生成処理が何度も出ている
- 変更のたびに関係ない場所まで修正している
- テストで差し替えたい依存が直接埋め込まれている
- 責務が大きくなり、クラスや関数の説明が難しい
- 将来増えるバリエーションがかなり明確である
逆に、まだ要件が見えていない段階で過度に抽象化すると、将来の変更方向と合わない設計になりやすいです。
現代的な見方
GoFのデザインパターンはオブジェクト指向を前提にしたものが多いですが、現代の言語では別の機能で自然に表現できることがあります。
| 古典的なパターン | 現代的な代替・補助 |
|---|---|
| Strategy | 関数、クロージャ、インターフェース |
| Observer | イベント、Pub/Sub、リアクティブストリーム |
| Factory | DIコンテナ、モジュール境界 |
| Template Method | 高階関数、委譲、コンポジション |
| Iterator | 言語組み込みのiterator / generator |
| Decorator | 言語機能のdecorator、middleware |
| Command | job queue、event sourcing、undo stack |
パターン名を使うよりも、まず言語機能とチームの慣習で自然に表現できるかを考えると、過剰設計を避けやすくなります。
パターンを選ぶための観点
パターンを選ぶときは、名前から探すより、設計上の圧力から考える方が実践的です。
| 困りごと | 候補 |
|---|---|
| 生成処理が複雑 | Factory Method, Builder |
| 実装をまとめて差し替えたい | Abstract Factory, Strategy |
| 外部APIの形が合わない | Adapter |
| 複雑な内部構造を隠したい | Facade |
| 共通処理を重ねたい | Decorator, Proxy |
| 状態ごとの分岐が増えた | State |
| イベントを複数箇所へ伝えたい | Observer |
| 操作を記録・取り消ししたい | Command, Memento |
| 木構造を同じインターフェースで扱いたい | Composite |
導入前には、「この抽象によって削れる複雑さ」と「この抽象が新しく生む複雑さ」の両方を見る必要があります。
Pythonで軽く書くための道具
PythonでGoFパターンを使うときは、JavaやC++ のクラス構造をそのまま移植しない方が自然なことがあります。言語機能を使うと、パターンの意図だけを軽く表現できます。
| 道具 | 使いどころ |
|---|---|
Protocol |
継承ではなく「必要なメソッドを持つか」でStrategyやAdapterを表す |
dataclass |
BuilderやValue Objectの定義を簡潔にする |
| first-class function | Strategy、Command、Template Methodを関数として渡す |
| generator | Iteratorをクラスなしで表す |
| decorator syntax | Decoratorパターンや横断的処理を読みやすく重ねる |
| context manager | acquire / releaseを持つ処理を安全に囲む |
たとえばStrategyは、必ずしもクラスでなくても表現できます。
from collections.abc import Callable
PriceRule = Callable[[int], int]
def regular(price: int) -> int:
return price
def campaign(price: int) -> int:
return int(price * 0.9)
def checkout(price: int, rule: PriceRule) -> int:
return rule(price)
print(checkout(1000, campaign))
一方で、状態を持つ戦略、複数メソッドの契約、テスト用の差し替えが必要な場合は、クラスや Protocol の方が読みやすくなります。パターンは「形」ではなく「変更点をどこに閉じ込めるか」を考えるための語彙として使います。
Refactoring.guru デザインパターン実装
Refactoring.guru (refactoring.guru) は、デザインパターンをビジュアルと実装例で解説します。
Singleton パターン
class Database:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._initialized = False
return cls._instance
def __init__(self):
if self._initialized:
return
self.connection = self._connect()
self._initialized = True
db1 = Database()
db2 = Database()
assert db1 is db2
Factory Method パターン
from abc import ABC, abstractmethod
class PaymentProcessor(ABC):
@abstractmethod
def create_payment(self):
pass
class CreditCardProcessor(PaymentProcessor):
def create_payment(self):
return CreditCardPayment()
class Payment(ABC):
@abstractmethod
def process_payment(self, amount):
pass
class CreditCardPayment(Payment):
def process_payment(self, amount):
print(f"Credit card: ${amount}")
Builder パターン
class SQLQueryBuilder:
def __init__(self):
self._select = []
self._from = None
self._where = []
def select(self, *columns):
self._select = columns
return self
def from_table(self, table):
self._from = table
return self
def where(self, condition):
self._where.append(condition)
return self
def build(self):
query = f"SELECT {', '.join(self._select)} FROM {self._from}"
if self._where:
query += " WHERE " + " AND ".join(self._where)
return query
query = (SQLQueryBuilder()
.select('id', 'name')
.from_table('users')
.where('age > 18')
.build())
Adapter パターン
class LegacyPaymentSystem:
def do_payment(self, amount):
return f"Legacy: {amount}"
class ModernPaymentInterface:
def pay(self, amount):
raise NotImplementedError
class LegacyPaymentAdapter(ModernPaymentInterface):
def __init__(self, legacy_system):
self.legacy_system = legacy_system
def pay(self, amount):
return self.legacy_system.do_payment(amount)
Decorator パターン
from functools import wraps
def log_execution(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
def retry(max_attempts=3):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(max_attempts):
try:
return func(*args, **kwargs)
except:
if attempt == max_attempts - 1:
raise
return wrapper
return decorator
@retry(max_attempts=3)
@log_execution
def send_email(recipient, message):
print(f"To {recipient}: {message}")
Facade パターン
class UserAuthService:
def authenticate(self, username, password):
return username == "admin"
class RoleService:
def get_user_roles(self, username):
return ["admin"]
class AuditService:
def log_login(self, username):
print(f"Logged in: {username}")
class AuthFacade:
def __init__(self):
self.auth = UserAuthService()
self.roles = RoleService()
self.audit = AuditService()
def login(self, username, password):
if self.auth.authenticate(username, password):
roles = self.roles.get_user_roles(username)
self.audit.log_login(username)
return {"success": True, "roles": roles}
return {"success": False}
auth = AuthFacade()
auth.login("admin", "secret")
Observer パターン
from abc import ABC, abstractmethod
class Observer(ABC):
@abstractmethod
def update(self, subject):
pass
class DataStore:
def __init__(self):
self._data = None
self._observers = []
def attach(self, observer):
self._observers.append(observer)
def notify(self):
for observer in self._observers:
observer.update(self)
@property
def data(self):
return self._data
@data.setter
def data(self, value):
self._data = value
self.notify()
class LogObserver(Observer):
def update(self, subject):
print(f"Data: {subject.data}")
store = DataStore()
store.attach(LogObserver())
store.data = 50
Strategy パターン
from abc import ABC, abstractmethod
class SortingStrategy(ABC):
@abstractmethod
def sort(self, data):
pass
class BubbleSort(SortingStrategy):
def sort(self, data):
n = len(data)
for i in range(n):
for j in range(0, n-i-1):
if data[j] > data[j+1]:
data[j], data[j+1] = data[j+1], data[j]
return data
class QuickSort(SortingStrategy):
def sort(self, data):
if len(data) <= 1:
return data
pivot = data[len(data) // 2]
return (self.sort([x for x in data if x < pivot]) +
[x for x in data if x == pivot] +
self.sort([x for x in data if x > pivot]))
class DataProcessor:
def __init__(self, strategy):
self.strategy = strategy
def process(self, data):
return self.strategy.sort(data)
processor = DataProcessor(QuickSort())
processor.process([64, 34, 25, 12])
Command パターン
from abc import ABC, abstractmethod
class Command(ABC):
@abstractmethod
def execute(self):
pass
@abstractmethod
def undo(self):
pass
class OpenFileCommand(Command):
def __init__(self, filename):
self.filename = filename
def execute(self):
return f"Opening {self.filename}"
def undo(self):
return f"Closing {self.filename}"
class SaveFileCommand(Command):
def __init__(self, filename, content):
self.filename = filename
self.content = content
def execute(self):
return f"Saving {self.filename}"
def undo(self):
return f"Unsaving {self.filename}"
class TextEditor:
def __init__(self):
self.history = []
def execute_command(self, command):
command.execute()
self.history.append(command)
def undo_last(self):
if self.history:
command = self.history.pop()
command.undo()
editor = TextEditor()
editor.execute_command(OpenFileCommand("doc.txt"))
editor.undo_last()
State パターン
from abc import ABC, abstractmethod
class OrderState(ABC):
@abstractmethod
def handle(self, order):
pass
class PendingState(OrderState):
def handle(self, order):
print("Pending")
order.state = PaymentState()
class PaymentState(OrderState):
def handle(self, order):
print("Payment")
order.state = ShippingState()
class ShippingState(OrderState):
def handle(self, order):
print("Shipping")
class Order:
def __init__(self):
self.state = PendingState()
def process(self):
self.state.handle(self)
order = Order()
order.process()
order.process()
SourceMaking パターン実装
SourceMaking (sourcemaking.com) では、古典的デザインパターンの詳細な実装例を提供しています。
パターン選択の判断基準
| パターン | 目的 | 複雑度 |
|---|---|---|
| Singleton | グローバル管理 | 低 |
| Factory | オブジェクト生成 | 中 |
| Builder | 複雑な構築 | 高 |
| Adapter | インターフェース変換 | 中 |
| Decorator | 機能拡張 | 中 |
| Facade | 複雑さ隠蔽 | 中 |
| Observer | イベント駆動 | 中 |
| Strategy | アルゴリズム切り替え | 低 |
| Command | 命令キューイング | 中 |
| State | 状態遷移 | 高 |
パターン適用のアンチパターン
- シンプルな問題に複雑なパターンを当てはめない
- 問題解決が目的であり、パターン使用は手段
- フレームワーク依存のパターン実装を避ける
まとめ
デザインパターンは、設計の典型問題に名前を与えるための道具です。重要なのは、パターンを当てはめることではなく、変更される部分を見つけ、依存を適切な向きに整え、読みやすく保守しやすい境界を作ることです。
Pythonのような表現力の高い言語では、古典的なクラス図どおりに書く必要はありません。関数、Protocol、dataclass、decorator、標準ライブラリを使いながら、パターンの意図だけを軽く取り入れるのが現実的です。
参考文献
公式・標準
- Python Documentation: contextlib
- Python Documentation: dataclasses
- Python Documentation: typing.Protocol