Python | OOP:コンポジション

Python Python
スポンサーリンク

概要(コンポジションは「小さな部品を組み合わせて振る舞いを作る」設計)

コンポジション(合成)は、継承に頼らず「部品(オブジェクト)を持つ」ことで機能を組み立てる設計です。車が“エンジンを持つ”、クライアントが“セッションを持つ”、サービスが“ルールを持つ”というように、役割を分割して組み合わせます。初心者がまず押さえるべき核心は、責務の分解、明確な所有(誰が部品の寿命を管理するか)、委譲(部品へ仕事を渡す)です。


基本の考え方(継承よりも「持つ」で柔軟に)

継承とコンポジションの違い

継承は“is-a(〜である)”の関係で、上位クラスの振る舞いをそのまま背負います。コンポジションは“has-a(〜を持つ)”で、必要な機能を部品として注入・交換できます。変更に強くテストしやすいのがコンポジションの利点です。

最小例(車はエンジンを“持つ”)

class Engine:
    def __init__(self, power: int):
        self.power = power
    def start(self) -> str:
        return f"engine {self.power}hp started"

class Car:
    def __init__(self, engine: Engine):
        self.engine = engine  # “持つ”(合成)

    def drive(self) -> str:
        return self.engine.start() + " → car moves"

car = Car(Engine(150))
print(car.drive())
Python

CarはEngineを“持つ”だけで、エンジンの実装(電気・ガソリンなど)を自由に差し替えられます。


実践パターン(サービス+戦略、クライアント+セッション、集約)

戦略の差し替え(ルールを“持つ”)

class PriceRule:
    def calc(self, amount: float) -> float:
        return amount  # デフォルト

class DiscountRule(PriceRule):
    def calc(self, amount: float) -> float:
        return amount * 0.9

class Checkout:
    def __init__(self, rule: PriceRule):
        self.rule = rule  # 戦略を“持つ”
    def total(self, amount: float) -> float:
        return self.rule.calc(amount)

print(Checkout(DiscountRule()).total(100.0))  # 90.0
Python

ルールを部品化して持つことで、挙動を安全に切り替えられます。

APIクライアントはセッションを“持つ”

import requests

class ApiClient:
    def __init__(self, session: requests.Session, base_url: str, timeout: float = 5.0):
        self.session = session          # 部品(外部I/O)を“持つ”
        self.base_url = base_url
        self.timeout = timeout

    def get_user(self, uid: str) -> dict:
        r = self.session.get(f"{self.base_url}/users/{uid}", timeout=self.timeout)
        r.raise_for_status()
        data = r.json()
        return {"id": str(data.get("id", "")), "name": data.get("name") or "unknown"}
Python

セッションを合成することで、認証・プロキシ・モック差し替えが容易になります(テストでフェイクセッションを渡せます)。

集約(複数部品をまとめて“持つ”)

class Cart:
    def __init__(self):
        self._items: list[tuple[str, int]] = []  # アイテムを集約

    def add(self, sku: str, qty: int):
        if qty <= 0: raise ValueError("数量は正の数")
        self._items.append((sku, qty))

    def total_qty(self) -> int:
        return sum(q for _, q in self._items)
Python

集約は「同種のオブジェクト群」をまとめて管理するコンポジションです。


重要ポイントの深掘り(責務分割・所有とライフタイム・委譲)

責務は“ひとつの軸”に絞る

  • 部品(Engine/PriceRule/Session): 明確な単機能に特化させる。
  • 組み立て(Car/Checkout/ApiClient): 使い方を決め、部品へ仕事を渡すだけに集中する。 役割が混ざると保守が難しくなるため、分割の境界をはっきりさせます。

所有とライフタイムを決める

長生きする部品(セッション、接続)は上位で一度作って共有し、短命の部品(戦略、フォーマッタ)は必要なときに差し替えます。誰が破棄・再作成を担うかをコード上で明確にすると、資源管理が安定します。

委譲(部品へ仕事を渡す)を丁寧に

主役クラスは「自分でやらない、部品に任せる」。メソッド内で部品メソッドを呼ぶ“委譲”に徹することで、差し替えが安全になり、テストで部品だけを検証できます。


継承との使い分け(いつ“持つ”、いつ“継承する”)

コンポジションを選ぶ場面

  • 「振る舞いを差し替えたい」「外部I/Oをモックしたい」「複数の機能を組み合わせたい」場合は“持つ”方が拡張に強いです。

継承を選ぶ場面

  • 「型として同一視したい」「抽象クラスで枠を強制したい」場合は継承が適切です。ただし具体実装の共有はMixinや合成に寄せると柔軟性が保てます。

よくある落とし穴と回避(漏れ・循環・多すぎる注入)

内部を漏らす(部品の詳細を外へ晒す)

主役クラスの公開メソッドだけを使わせ、部品を外へ直接返さない設計にします。外部が部品へ直接触れると、結合が強くなり壊れやすくなります。

循環依存(互いに持ち合って抜けない)

AがBを持ち、BがAを持つ設計は避けます。インターフェースを挟む、片方はコールバックやイベントで通知するなど、片方向の依存に整理します。

依存の数が多すぎて分かりづらい

コンストラクタ引数が増えすぎたら、設定オブジェクト(Config)やファクトリで“作り方”をまとめます。公開面は最小限に。


例題(前処理パイプ+ルール+クライアントの“組み合わせ”で現実的な設計)

前処理パイプを“持つ”サービス

class Normalizer:
    def user(self, data: dict) -> dict:
        return {"id": str(data.get("id", "")), "name": (data.get("name") or "unknown").strip()}

class UserService:
    def __init__(self, client, normalizer: Normalizer):
        self.client = client
        self.normalizer = normalizer

    def profile(self, uid: str) -> dict:
        raw = self.client.get_user(uid)
        return self.normalizer.user(raw)
Python

I/O(client)と変換(normalizer)を“持ち”、サービスは「流れ」を定義するだけ。

価格計算は“ルール”を差し替える

class TaxRule:
    def __init__(self, rate: float): self.rate = rate
    def apply(self, amount: float) -> float: return amount * (1 + self.rate)

class PriceService:
    def __init__(self, tax: TaxRule): self.tax = tax
    def total(self, base: float) -> float: return self.tax.apply(base)

print(PriceService(TaxRule(0.1)).total(100))  # 110.0
Python

税率や計算方法を部品化して持つことで、地域やキャンペーンに応じて切り替え可能です。


まとめ(コンポジションは“差し替え・テスト・拡張”を強くする王道)

コンポジションは、機能を小さな部品へ分割し、主役がそれを“持って委譲する”設計です。責務の分解、所有とライフタイムの明確化、委譲の徹底で、差し替えに強く、テストしやすく、変更が局所化されます。継承は枠の強制に限定し、具体実装は合成に寄せる。これを型として身につければ、初心者でも「壊れにくく、現実の変更に耐える」OOPを自然に書けるようになります。

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