メインコンテンツへ移動
SEMentor
設計 約13分

デザインパターン入門

GoFの代表的なデザインパターンをPythonで学ぶ。Singleton・Factory・Observer・Strategy・Decorator

デザインパターンとは

デザインパターンは、ソフトウェア設計で繰り返し現れる問題に対する「定石の解決策」です。1994年にGoF(Gang of Four)が『Design Patterns』で23パターンを体系化しました。

パターンを知ることの価値は2つです。

  1. 実装の近道: 似た問題を解決済みの方法で素早く解ける
  2. 共通言語: 「ここはStrategyパターンにしよう」と一言で意図が伝わる

23パターンは生成・構造・振る舞いの3カテゴリに分類されます。ここでは実務頻出の5パターンを学びます。

1. Singleton(生成パターン)

インスタンスが1つしか存在しないことを保証する

class DatabaseConnection:
    _instance = None
    
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance._connect()
        return cls._instance
    
    def _connect(self):
        print("DB接続を確立")
        self.connected = True

# どこから呼んでも同じインスタンス
db1 = DatabaseConnection()
db2 = DatabaseConnection()
print(db1 is db2)  # True

使いどころ: 設定オブジェクト、ログマネージャ、DBコネクションプール

注意点: グローバル状態を持つため、テストが困難になりやすい。乱用禁止。

2. Factory Method(生成パターン)

オブジェクト生成をサブクラスに委譲する

from abc import ABC, abstractmethod

class Notification(ABC):
    @abstractmethod
    def send(self, message: str): ...

class EmailNotification(Notification):
    def send(self, message):
        print(f"メール送信: {message}")

class SlackNotification(Notification):
    def send(self, message):
        print(f"Slack通知: {message}")

class PushNotification(Notification):
    def send(self, message):
        print(f"プッシュ通知: {message}")

class NotificationFactory:
    @staticmethod
    def create(channel: str) -> Notification:
        if channel == "email":
            return EmailNotification()
        elif channel == "slack":
            return SlackNotification()
        elif channel == "push":
            return PushNotification()
        raise ValueError(f"Unknown channel: {channel}")

# 使う側はどのクラスかを意識しない
notif = NotificationFactory.create("slack")
notif.send("デプロイが完了しました")

使いどころ: 設定によって生成するクラスを切り替えたいとき

3. Observer(振る舞いパターン)

状態変化を複数のオブジェクトに自動通知する(イベント駆動)

from abc import ABC, abstractmethod

class Observer(ABC):
    @abstractmethod
    def update(self, event: str, data: dict): ...

class EventEmitter:
    def __init__(self):
        self._listeners: dict[str, list[Observer]] = {}
    
    def on(self, event: str, observer: Observer):
        self._listeners.setdefault(event, []).append(observer)
    
    def emit(self, event: str, data: dict = {}):
        for observer in self._listeners.get(event, []):
            observer.update(event, data)

# 具体的なオブザーバー
class OrderLogger(Observer):
    def update(self, event, data):
        print(f"[LOG] {event}: {data}")

class EmailNotifier(Observer):
    def update(self, event, data):
        if event == "order_placed":
            print(f"注文確認メール送信 to {data['email']}")

class InventoryUpdater(Observer):
    def update(self, event, data):
        if event == "order_placed":
            print(f"在庫を {data['product']} 分減らす")

# 使用例
emitter = EventEmitter()
emitter.on("order_placed", OrderLogger())
emitter.on("order_placed", EmailNotifier())
emitter.on("order_placed", InventoryUpdater())

emitter.emit("order_placed", {
    "email": "user@example.com",
    "product": "商品A",
})
# [LOG] order_placed: {...}
# 注文確認メール送信 to user@example.com
# 在庫を 商品A 分減らす

使いどころ: イベント処理、MVC のモデル変更通知、Webhook

4. Strategy(振る舞いパターン)

アルゴリズムをカプセル化して、実行時に切り替えられるようにする

from abc import ABC, abstractmethod

class SortStrategy(ABC):
    @abstractmethod
    def sort(self, data: list) -> list: ...

class BubbleSortStrategy(SortStrategy):
    def sort(self, data):
        arr = data.copy()
        n = len(arr)
        for i in range(n):
            for j in range(n - i - 1):
                if arr[j] > arr[j + 1]:
                    arr[j], arr[j + 1] = arr[j + 1], arr[j]
        return arr

class QuickSortStrategy(SortStrategy):
    def sort(self, data):
        if len(data) <= 1:
            return data
        pivot = data[len(data) // 2]
        left = [x for x in data if x < pivot]
        mid = [x for x in data if x == pivot]
        right = [x for x in data if x > pivot]
        return self.sort(left) + mid + self.sort(right)

class Sorter:
    def __init__(self, strategy: SortStrategy):
        self._strategy = strategy
    
    def set_strategy(self, strategy: SortStrategy):
        self._strategy = strategy
    
    def sort(self, data: list) -> list:
        return self._strategy.sort(data)

# 実行時にアルゴリズムを切り替え
sorter = Sorter(BubbleSortStrategy())
print(sorter.sort([5, 2, 8, 1]))  # バブルソート

sorter.set_strategy(QuickSortStrategy())
print(sorter.sort([5, 2, 8, 1]))  # クイックソートに切り替え

使いどころ: 決済方法の切り替え、圧縮アルゴリズム、バリデーションロジック

5. Decorator(構造パターン)

既存クラスを修正せずに機能を追加する

from abc import ABC, abstractmethod
from functools import wraps

# Python の関数デコレータとしての実装
def log_calls(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"呼び出し: {func.__name__}({args}, {kwargs})")
        result = func(*args, **kwargs)
        print(f"結果: {result}")
        return result
    return wrapper

def measure_time(func):
    import time
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        elapsed = time.time() - start
        print(f"実行時間: {elapsed:.4f}秒")
        return result
    return wrapper

@log_calls
@measure_time
def fetch_data(url: str):
    import time
    time.sleep(0.1)  # API呼び出しシミュレーション
    return {"data": "..."}

fetch_data("https://api.example.com/users")
# 呼び出し: fetch_data(('https://...',), {})
# 実行時間: 0.1001秒
# 結果: {'data': '...'}

クラスベースの実装も可能です。

class TextProcessor(ABC):
    @abstractmethod
    def process(self, text: str) -> str: ...

class PlainText(TextProcessor):
    def process(self, text):
        return text

class UpperCaseDecorator(TextProcessor):
    def __init__(self, processor: TextProcessor):
        self._processor = processor
    def process(self, text):
        return self._processor.process(text).upper()

class TrimDecorator(TextProcessor):
    def __init__(self, processor: TextProcessor):
        self._processor = processor
    def process(self, text):
        return self._processor.process(text).strip()

# 自由に組み合わせ
processor = UpperCaseDecorator(TrimDecorator(PlainText()))
print(processor.process("  hello world  "))  # "HELLO WORLD"

使いどころ: ミドルウェア、ロギング・キャッシュの後付け、入力バリデーション

パターン選択の指針

オブジェクト生成を柔軟にしたい
→ Factory Method / Abstract Factory / Builder

インスタンスを1つに制限したい
→ Singleton(乱用注意)

アルゴリズムを切り替えたい
→ Strategy

イベント/変更を複数に通知したい
→ Observer

機能を動的に追加したい
→ Decorator

複雑なオブジェクトを段階的に構築したい
→ Builder

既存コードへのインターフェースを合わせたい
→ Adapter

オブジェクトのツリー構造を扱いたい
→ Composite

まとめ

デザインパターンは「こう実装せよ」という命令ではなく、問題と解決策の共通語彙です。

  • パターン名を会話に使うと意思疎通が速くなる
  • パターンを当てはめることが目的化しないよう注意
  • まず「何が問題か」を明確にしてから、パターンを手段として選ぶ

このレッスンは未完了です。