概要(コンポジションは「小さな部品を組み合わせて振る舞いを作る」設計)
コンポジション(合成)は、継承に頼らず「部品(オブジェクト)を持つ」ことで機能を組み立てる設計です。車が“エンジンを持つ”、クライアントが“セッションを持つ”、サービスが“ルールを持つ”というように、役割を分割して組み合わせます。初心者がまず押さえるべき核心は、責務の分解、明確な所有(誰が部品の寿命を管理するか)、委譲(部品へ仕事を渡す)です。
基本の考え方(継承よりも「持つ」で柔軟に)
継承とコンポジションの違い
継承は“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())
PythonCarは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)
PythonI/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を自然に書けるようになります。
