コンポジション vs 継承
「継承よりコンポジションを優先せよ」はGoF以来の鉄則。しかし盲目的にコンポジションを使うのではなく、両者のトレードオフを理解して使い分けることが重要。
81 分で読めます40,349 文字
コンポジション vs 継承
「継承よりコンポジションを優先せよ」はGoF以来の鉄則。しかし盲目的にコンポジションを使うのではなく、両者のトレードオフを理解して使い分けることが重要。
この章で学ぶこと
- コンポジションと継承の本質的な違いを理解する
- 「継承よりコンポジション」の理由を把握する
- 実践的な判断基準を学ぶ
- デザインパターンにおけるコンポジションの活用を習得する
- 継承が適切な場面とその設計指針を理解する
前提知識
このガイドを読む前に、以下の知識があると理解が深まります:
- 基本的なプログラミングの知識
- 関連する基礎概念の理解
1. 本質的な違い
継承(Inheritance): is-a 関係
→ Dog is-a Animal(犬は動物である)
→ 親のすべてを引き継ぐ(強い結合)
→ コンパイル時に関係が固定
コンポジション(Composition): has-a 関係
→ Car has-a Engine(車はエンジンを持つ)
→ 必要な部品を組み合わせる(弱い結合)
→ 実行時に部品を差し替え可能
継承:| Animal |
|---|
│ is-a| Dog |
|---|
コンポジション: ┌─────────┐ ┌────────┐
│ Car │────→│ Engine │ has-a
│ │────→│ Wheels │ has-a
│ │────→│ GPS │ has-a
└─────────┘ └────────┘委譲(Delegation):
→ コンポジションの一形態
→ 内部のオブジェクトにメソッド呼び出しを転送
→ 「自分で処理する」のではなく「持っているものに頼む」
集約(Aggregation):
→ コンポジションの弱い形態
→ 「部品」が独立して存在できる
→ Car has-a Driver(ドライバーは車がなくても存在する)
コンポジション: 部品はオーナーと共に生死する
集約: 部品はオーナーとは独立して存在する
1.1 継承の構造的問題
継承のメカニズム:
class Dog extends Animal {
// Dog は Animal の全てを自動的に引き継ぐ
// 1. public メソッド → そのまま公開
// 2. protected メソッド → アクセス可能
// 3. private メソッド → アクセス不可だが存在する
// 4. フィールド → すべて引き継ぐ
}
問題点:
1. カプセル化の破壊:
→ protected フィールドにアクセスできてしまう
→ 親クラスの内部実装に依存してしまう
→ 親クラスのリファクタリングが困難に
2. 脆い基底クラス問題(Fragile Base Class Problem):
→ 親クラスの変更が子クラスを予期せず壊す
→ 子クラスが親の実装詳細に依存しているため
3. 密結合:
→ 親クラスのインターフェースすべてを強制的に継承
→ 不要なメソッドもすべて公開される
4. 単一継承の制約(Java, C#, TypeScript):
→ 1つの親クラスしか持てない
→ 複数の振る舞いを組み合わせられない
1.2 脆い基底クラス問題の具体例
// ❌ 脆い基底クラス問題の実例
// Java の HashSet を継承した「カウント付きセット」
public class CountingSet<E> extends HashSet<E> {
private int addCount = 0;
@Override
public boolean add(E e) {
addCount++;
return super.add(e);
}
@Override
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}
public int getAddCount() {
return addCount;
}
}
// 使ってみる
CountingSet<String> s = new CountingSet<>();
s.addAll(Arrays.asList("A", "B", "C"));
System.out.println(s.getAddCount()); // 期待: 3, 実際: 6 !!!
// なぜ 6 になるのか?
// HashSet.addAll() の内部で add() を呼んでいる!
// addAll() で +3、add() x 3 で +3 = 合計 6
// → 親クラスの実装詳細に依存してしまった
// ✅ コンポジションで解決
public class CountingSet<E> implements Set<E> {
private final Set<E> delegate = new HashSet<>();
private int addCount = 0;
@Override
public boolean add(E e) {
addCount++;
return delegate.add(e); // 委譲
}
@Override
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return delegate.addAll(c); // 委譲(内部で add() を呼んでも影響なし)
}
@Override
public int size() { return delegate.size(); }
@Override
public boolean contains(Object o) { return delegate.contains(o); }
// ... 他の Set メソッドもすべて delegate に委譲
public int getAddCount() {
return addCount;
}
}
CountingSet<String> s = new CountingSet<>();
s.addAll(Arrays.asList("A", "B", "C"));
System.out.println(s.getAddCount()); // ✅ 3(正しい!)2. なぜ「継承よりコンポジション」か
継承の問題:
1. 強い結合: 親の変更が全子クラスに波及
2. カプセル化の破壊: 子が親の実装詳細に依存
3. 柔軟性の欠如: 実行時に振る舞いを変更できない
4. 爆発的な組み合わせ:
例: ゲームキャラクター
継承で設計すると:
Character
├── Warrior
│ ├── FireWarrior
│ ├── IceWarrior
│ └── FlyingWarrior
├── Mage
│ ├── FireMage
│ ├── IceMage
│ └── FlyingMage
└── Archer
├── FireArcher
├── IceArcher
└── FlyingArcher
→ 3属性 × 3職業 = 9クラス
→ 新属性追加で +3クラス、新職業追加で +3クラス
コンポジションで設計すると:
Character
├── has-a: AttackStyle(Warrior, Mage, Archer)
├── has-a: Element(Fire, Ice, Lightning)
└── has-a: Movement(Walk, Fly, Teleport)
→ 3 + 3 + 3 = 9コンポーネント
→ 新属性追加で +1コンポーネント
2.1 コンポジションによるリファクタリング
// ❌ 継承: クラスの爆発
class Animal {
eat(): void { console.log("食べる"); }
}
class FlyingAnimal extends Animal {
fly(): void { console.log("飛ぶ"); }
}
class SwimmingAnimal extends Animal {
swim(): void { console.log("泳ぐ"); }
}
class FlyingSwimmingAnimal extends ??? {
// 多重継承できない!
}
// ✅ コンポジション: 柔軟な組み合わせ
interface MovementAbility {
move(): string;
}
class Flying implements MovementAbility {
move(): string { return "空を飛ぶ"; }
}
class Swimming implements MovementAbility {
move(): string { return "水中を泳ぐ"; }
}
class Walking implements MovementAbility {
move(): string { return "地上を歩く"; }
}
class Animal {
private abilities: MovementAbility[] = [];
addAbility(ability: MovementAbility): void {
this.abilities.push(ability);
}
moveAll(): string[] {
return this.abilities.map(a => a.move());
}
}
// カモ: 飛べる + 泳げる + 歩ける
const duck = new Animal();
duck.addAbility(new Flying());
duck.addAbility(new Swimming());
duck.addAbility(new Walking());
console.log(duck.moveAll()); // ["空を飛ぶ", "水中を泳ぐ", "地上を歩く"]2.2 ゲームキャラクターのコンポジション設計
// ECS(Entity-Component-System)的なコンポジション設計
// === コンポーネント(振る舞いの部品)===
interface AttackBehavior {
attack(target: string): string;
getRange(): number;
}
interface DefenseBehavior {
defend(): string;
getArmor(): number;
}
interface MovementBehavior {
move(direction: string): string;
getSpeed(): number;
}
interface ElementalPower {
element(): string;
specialAttack(target: string): string;
}
// === 具体的なコンポーネント ===
class SwordAttack implements AttackBehavior {
attack(target: string): string {
return `剣で${target}を斬りつけた!`;
}
getRange(): number { return 1; }
}
class BowAttack implements AttackBehavior {
attack(target: string): string {
return `弓矢で${target}を射た!`;
}
getRange(): number { return 10; }
}
class MagicAttack implements AttackBehavior {
attack(target: string): string {
return `魔法で${target}を攻撃した!`;
}
getRange(): number { return 5; }
}
class ShieldDefense implements DefenseBehavior {
defend(): string { return "盾で防御!"; }
getArmor(): number { return 50; }
}
class DodgeDefense implements DefenseBehavior {
defend(): string { return "素早く回避!"; }
getArmor(): number { return 10; }
}
class MagicBarrier implements DefenseBehavior {
defend(): string { return "魔法障壁を展開!"; }
getArmor(): number { return 30; }
}
class WalkMovement implements MovementBehavior {
move(direction: string): string { return `${direction}に歩いた`; }
getSpeed(): number { return 3; }
}
class FlyMovement implements MovementBehavior {
move(direction: string): string { return `${direction}に飛んだ`; }
getSpeed(): number { return 8; }
}
class TeleportMovement implements MovementBehavior {
move(direction: string): string { return `${direction}にテレポートした`; }
getSpeed(): number { return 100; }
}
class FirePower implements ElementalPower {
element(): string { return "炎"; }
specialAttack(target: string): string {
return `${target}を炎で焼き尽くした!`;
}
}
class IcePower implements ElementalPower {
element(): string { return "氷"; }
specialAttack(target: string): string {
return `${target}を氷で凍らせた!`;
}
}
class LightningPower implements ElementalPower {
element(): string { return "雷"; }
specialAttack(target: string): string {
return `${target}に雷を落とした!`;
}
}
// === キャラクター(コンポジション)===
class Character {
constructor(
public name: string,
private attackBehavior: AttackBehavior,
private defenseBehavior: DefenseBehavior,
private movementBehavior: MovementBehavior,
private elementalPower?: ElementalPower,
) {}
performAttack(target: string): string {
return this.attackBehavior.attack(target);
}
performDefense(): string {
return this.defenseBehavior.defend();
}
performMove(direction: string): string {
return this.movementBehavior.move(direction);
}
performSpecial(target: string): string {
if (!this.elementalPower) {
return "特殊能力を持っていない";
}
return this.elementalPower.specialAttack(target);
}
// 実行時に振る舞いを変更可能!
setAttackBehavior(attack: AttackBehavior): void {
this.attackBehavior = attack;
}
setElementalPower(power: ElementalPower): void {
this.elementalPower = power;
}
describe(): string {
const parts = [
`[${this.name}]`,
`攻撃: ${this.attackBehavior.constructor.name}`,
`防御: ${this.defenseBehavior.constructor.name}`,
`移動: ${this.movementBehavior.constructor.name}`,
];
if (this.elementalPower) {
parts.push(`属性: ${this.elementalPower.element()}`);
}
return parts.join(" / ");
}
}
// === 使用例 ===
// 炎の剣士: 剣 + 盾 + 歩行 + 炎
const fireWarrior = new Character(
"炎の剣士",
new SwordAttack(),
new ShieldDefense(),
new WalkMovement(),
new FirePower(),
);
// 氷の魔法使い: 魔法 + 魔法障壁 + テレポート + 氷
const iceMage = new Character(
"氷の魔法使い",
new MagicAttack(),
new MagicBarrier(),
new TeleportMovement(),
new IcePower(),
);
// 雷の弓使い: 弓 + 回避 + 飛行 + 雷
const lightningArcher = new Character(
"雷の弓使い",
new BowAttack(),
new DodgeDefense(),
new FlyMovement(),
new LightningPower(),
);
// ゲーム中に装備を変更!
fireWarrior.setAttackBehavior(new BowAttack()); // 弓に持ち替え
fireWarrior.setElementalPower(new IcePower()); // 属性変更
console.log(fireWarrior.performAttack("ドラゴン")); // "弓矢でドラゴンを射た!"
console.log(fireWarrior.performSpecial("ドラゴン")); // "ドラゴンを氷で凍らせた!"2.3 Python でのコンポジション
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import Protocol, Optional
# === コンポーネントの定義 ===
class Renderer(Protocol):
"""レンダリング戦略"""
def render(self, data: dict) -> str: ...
class Validator(Protocol):
"""バリデーション戦略"""
def validate(self, data: dict) -> list[str]: ...
class Serializer(Protocol):
"""シリアライズ戦略"""
def serialize(self, data: dict) -> str: ...
def deserialize(self, raw: str) -> dict: ...
class Logger(Protocol):
"""ログ出力戦略"""
def info(self, message: str) -> None: ...
def error(self, message: str) -> None: ...
# === コンポーネントの実装 ===
class HtmlRenderer:
def render(self, data: dict) -> str:
rows = "".join(
f"<tr><td>{k}</td><td>{v}</td></tr>" for k, v in data.items()
)
return f"<table>{rows}</table>"
class MarkdownRenderer:
def render(self, data: dict) -> str:
header = "| Key | Value |\n|-----|-------|\n"
rows = "\n".join(f"| {k} | {v} |" for k, v in data.items())
return header + rows
class JsonRenderer:
def render(self, data: dict) -> str:
import json
return json.dumps(data, indent=2, ensure_ascii=False)
class StrictValidator:
"""厳密なバリデーション"""
def __init__(self, required_fields: list[str]):
self.required_fields = required_fields
def validate(self, data: dict) -> list[str]:
errors = []
for field_name in self.required_fields:
if field_name not in data or not data[field_name]:
errors.append(f"'{field_name}' は必須です")
return errors
class LenientValidator:
"""緩いバリデーション(警告のみ)"""
def validate(self, data: dict) -> list[str]:
return [] # 常に OK
class JsonSerializer:
def serialize(self, data: dict) -> str:
import json
return json.dumps(data, ensure_ascii=False)
def deserialize(self, raw: str) -> dict:
import json
return json.loads(raw)
class ConsoleLogger:
def info(self, message: str) -> None:
print(f"[INFO] {message}")
def error(self, message: str) -> None:
print(f"[ERROR] {message}")
class NullLogger:
"""何も出力しないロガー(テスト用)"""
def info(self, message: str) -> None:
pass
def error(self, message: str) -> None:
pass
# === コンポジションで組み立てる ===
@dataclass
class ReportGenerator:
"""レポート生成器: コンポーネントを組み合わせて構築"""
renderer: Renderer
validator: Validator
serializer: Serializer
logger: Logger
def generate(self, data: dict) -> str:
self.logger.info(f"レポート生成開始: {len(data)} 項目")
# バリデーション
errors = self.validator.validate(data)
if errors:
for error in errors:
self.logger.error(f"バリデーションエラー: {error}")
raise ValueError(f"バリデーション失敗: {errors}")
# レンダリング
rendered = self.renderer.render(data)
self.logger.info(f"レンダリング完了: {len(rendered)} 文字")
return rendered
def save(self, data: dict, filepath: str) -> None:
serialized = self.serializer.serialize(data)
with open(filepath, "w") as f:
f.write(serialized)
self.logger.info(f"保存完了: {filepath}")
def load(self, filepath: str) -> dict:
with open(filepath) as f:
raw = f.read()
return self.serializer.deserialize(raw)
# === 使用例: 異なる用途で異なるコンポーネントを組み合わせ ===
# 本番環境: HTML + 厳密バリデーション + JSON + コンソールログ
production_report = ReportGenerator(
renderer=HtmlRenderer(),
validator=StrictValidator(["title", "author", "date"]),
serializer=JsonSerializer(),
logger=ConsoleLogger(),
)
# 開発環境: Markdown + 緩いバリデーション + JSON + 無出力ログ
dev_report = ReportGenerator(
renderer=MarkdownRenderer(),
validator=LenientValidator(),
serializer=JsonSerializer(),
logger=NullLogger(),
)
# テスト環境: JSON + 緩いバリデーション + JSON + 無出力ログ
test_report = ReportGenerator(
renderer=JsonRenderer(),
validator=LenientValidator(),
serializer=JsonSerializer(),
logger=NullLogger(),
)
# 同じ ReportGenerator クラスだが、振る舞いが全く異なる
data = {"title": "月次報告", "author": "田中", "date": "2026-01-01"}
print(production_report.generate(data)) # HTML形式
print(dev_report.generate(data)) # Markdown形式
print(test_report.generate(data)) # JSON形式3. Strategy パターンとの関係
// コンポジション + Strategy = 実行時に振る舞いを変更
interface SortStrategy {
sort<T>(data: T[], compareFn: (a: T, b: T) => number): T[];
}
class QuickSort implements SortStrategy {
sort<T>(data: T[], compareFn: (a: T, b: T) => number): T[] {
// クイックソートの実装
return [...data].sort(compareFn);
}
}
class MergeSort implements SortStrategy {
sort<T>(data: T[], compareFn: (a: T, b: T) => number): T[] {
// マージソートの実装
return [...data].sort(compareFn);
}
}
class DataProcessor {
// コンポジション: 戦略を外部から注入
constructor(private sortStrategy: SortStrategy) {}
// 実行時に戦略を変更可能
setSortStrategy(strategy: SortStrategy): void {
this.sortStrategy = strategy;
}
process(data: number[]): number[] {
return this.sortStrategy.sort(data, (a, b) => a - b);
}
}
const processor = new DataProcessor(new QuickSort());
processor.process([3, 1, 4, 1, 5]);
// データ量が増えたら戦略を変更
processor.setSortStrategy(new MergeSort());3.1 デザインパターンとコンポジション
コンポジションを活用するデザインパターン:
Strategy パターン:
→ 振る舞いを交換可能にする
→ 例: ソートアルゴリズム、認証戦略
Decorator パターン:
→ 既存のオブジェクトに機能を追加
→ 例: ストリーム処理、ミドルウェア
Observer パターン:
→ イベントの通知
→ 例: UIイベント、Pub/Sub
Composite パターン:
→ ツリー構造の表現
→ 例: UIコンポーネント、ファイルシステム
Bridge パターン:
→ 抽象と実装を分離
→ 例: プラットフォーム別の描画
State パターン:
→ 状態に応じて振る舞いを変更
→ 例: ワークフロー、TCP接続
Chain of Responsibility パターン:
→ 処理の連鎖
→ 例: ミドルウェアチェーン、バリデーション
3.2 Decorator パターン(コンポジションの応用)
// Decorator パターン: 継承ではなくコンポジションで機能拡張
interface Logger {
log(message: string): void;
}
class ConsoleLogger implements Logger {
log(message: string): void {
console.log(message);
}
}
// デコレーター: 基本のロガーを包んで機能追加
class TimestampLogger implements Logger {
constructor(private inner: Logger) {}
log(message: string): void {
const timestamp = new Date().toISOString();
this.inner.log(`[${timestamp}] ${message}`);
}
}
class PrefixLogger implements Logger {
constructor(private inner: Logger, private prefix: string) {}
log(message: string): void {
this.inner.log(`${this.prefix} ${message}`);
}
}
class JsonLogger implements Logger {
constructor(private inner: Logger) {}
log(message: string): void {
this.inner.log(JSON.stringify({
message,
timestamp: new Date().toISOString(),
level: "info",
}));
}
}
class FilterLogger implements Logger {
constructor(private inner: Logger, private minLevel: string) {}
log(message: string): void {
// フィルタリングロジック
if (this.shouldLog(message)) {
this.inner.log(message);
}
}
private shouldLog(message: string): boolean {
// 簡易的なフィルタリング
return !message.startsWith("[DEBUG]");
}
}
// デコレーターを組み合わせて使う
const logger = new TimestampLogger(
new PrefixLogger(
new FilterLogger(
new ConsoleLogger(),
"info"
),
"[MyApp]"
)
);
logger.log("Hello, World!");
// → [2026-01-15T10:30:00.000Z] [MyApp] Hello, World!
// 継承で同じことをやろうとすると:
// TimestampConsoleLogger, TimestampFileLogger,
// PrefixConsoleLogger, PrefixFileLogger,
// TimestampPrefixConsoleLogger, TimestampPrefixFileLogger...
// → クラスの爆発!# Python: デコレーター(関数)を使ったコンポジション
from functools import wraps
from typing import Callable, Any
import time
import logging
def with_logging(func: Callable) -> Callable:
"""ログ出力を追加するデコレーター"""
@wraps(func)
def wrapper(*args, **kwargs):
logging.info(f"Calling {func.__name__} with args={args}, kwargs={kwargs}")
result = func(*args, **kwargs)
logging.info(f"{func.__name__} returned {result}")
return result
return wrapper
def with_timing(func: Callable) -> Callable:
"""実行時間計測を追加するデコレーター"""
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
elapsed = time.time() - start
print(f"{func.__name__} took {elapsed:.4f}s")
return result
return wrapper
def with_retry(max_retries: int = 3, delay: float = 1.0):
"""リトライを追加するデコレーター"""
def decorator(func: Callable) -> Callable:
@wraps(func)
def wrapper(*args, **kwargs):
last_exception = None
for attempt in range(max_retries):
try:
return func(*args, **kwargs)
except Exception as e:
last_exception = e
print(f"Attempt {attempt + 1} failed: {e}")
if attempt < max_retries - 1:
time.sleep(delay)
raise last_exception
return wrapper
return decorator
def with_cache(func: Callable) -> Callable:
"""結果をキャッシュするデコレーター"""
cache = {}
@wraps(func)
def wrapper(*args, **kwargs):
key = (args, tuple(sorted(kwargs.items())))
if key not in cache:
cache[key] = func(*args, **kwargs)
return cache[key]
return wrapper
# デコレーターを組み合わせ(コンポジション)
@with_logging
@with_timing
@with_retry(max_retries=3)
@with_cache
def fetch_data(url: str) -> dict:
"""外部APIからデータを取得"""
import requests
response = requests.get(url)
return response.json()
# 実行すると:
# 1. ログ出力(with_logging)
# 2. 時間計測開始(with_timing)
# 3. リトライ処理(with_retry)
# 4. キャッシュ確認(with_cache)
# 5. 実際の関数実行3.3 State パターン(コンポジションで状態管理)
// State パターン: 状態オブジェクトをコンポジションで持つ
interface OrderState {
name: string;
canConfirm(): boolean;
canShip(): boolean;
canCancel(): boolean;
canDeliver(): boolean;
confirm(order: Order): void;
ship(order: Order): void;
cancel(order: Order): void;
deliver(order: Order): void;
}
class PendingState implements OrderState {
name = "pending";
canConfirm() { return true; }
canShip() { return false; }
canCancel() { return true; }
canDeliver() { return false; }
confirm(order: Order): void {
console.log("注文を確認しました");
order.setState(new ConfirmedState());
}
ship(order: Order): void {
throw new Error("未確認の注文は出荷できません");
}
cancel(order: Order): void {
console.log("注文をキャンセルしました");
order.setState(new CancelledState());
}
deliver(order: Order): void {
throw new Error("未確認の注文は配達できません");
}
}
class ConfirmedState implements OrderState {
name = "confirmed";
canConfirm() { return false; }
canShip() { return true; }
canCancel() { return true; }
canDeliver() { return false; }
confirm(order: Order): void {
throw new Error("既に確認済みです");
}
ship(order: Order): void {
console.log("注文を出荷しました");
order.setState(new ShippedState());
}
cancel(order: Order): void {
console.log("確認済み注文をキャンセルしました(返金処理開始)");
order.setState(new CancelledState());
}
deliver(order: Order): void {
throw new Error("出荷前に配達はできません");
}
}
class ShippedState implements OrderState {
name = "shipped";
canConfirm() { return false; }
canShip() { return false; }
canCancel() { return false; }
canDeliver() { return true; }
confirm(order: Order): void { throw new Error("出荷済み"); }
ship(order: Order): void { throw new Error("既に出荷済み"); }
cancel(order: Order): void { throw new Error("出荷済みの注文はキャンセルできません"); }
deliver(order: Order): void {
console.log("注文を配達しました");
order.setState(new DeliveredState());
}
}
class DeliveredState implements OrderState {
name = "delivered";
canConfirm() { return false; }
canShip() { return false; }
canCancel() { return false; }
canDeliver() { return false; }
confirm() { throw new Error("配達済み"); }
ship() { throw new Error("配達済み"); }
cancel() { throw new Error("配達済みの注文はキャンセルできません"); }
deliver() { throw new Error("既に配達済み"); }
}
class CancelledState implements OrderState {
name = "cancelled";
canConfirm() { return false; }
canShip() { return false; }
canCancel() { return false; }
canDeliver() { return false; }
confirm() { throw new Error("キャンセル済み"); }
ship() { throw new Error("キャンセル済み"); }
cancel() { throw new Error("既にキャンセル済み"); }
deliver() { throw new Error("キャンセル済み"); }
}
class Order {
private state: OrderState = new PendingState(); // コンポジション
setState(state: OrderState): void {
console.log(`状態変更: ${this.state.name} → ${state.name}`);
this.state = state;
}
confirm(): void { this.state.confirm(this); }
ship(): void { this.state.ship(this); }
cancel(): void { this.state.cancel(this); }
deliver(): void { this.state.deliver(this); }
getStatus(): string { return this.state.name; }
}
// 使用例
const order = new Order();
console.log(order.getStatus()); // "pending"
order.confirm(); // 状態変更: pending → confirmed
order.ship(); // 状態変更: confirmed → shipped
order.deliver(); // 状態変更: shipped → delivered
// order.cancel(); // Error: 配達済みの注文はキャンセルできません4. 継承が適切な場面
継承を使うべき場面:
✓ 明確な is-a 関係(ListはCollectionである)
✓ フレームワークの拡張ポイント(AbstractController)
✓ テンプレートメソッドパターン
✓ 子クラスが親の全メソッドを意味的に満たす
✓ 型階層が安定している(頻繁に変わらない)
コンポジションを使うべき場面:
✓ has-a 関係(CarはEngineを持つ)
✓ 振る舞いの組み合わせが必要
✓ 実行時に振る舞いを変更したい
✓ 複数の「機能」を組み合わせたい
✓ テスト時にモックに差し替えたい
迷ったとき:
→ コンポジションを選ぶ(より安全)
→ 「このクラスは本当に親の "一種" か?」を自問
→ 「コード再利用のためだけの継承」は避ける
4.1 テンプレートメソッドパターン(継承の適切な使用例)
from abc import ABC, abstractmethod
from typing import Any
class ETLPipeline(ABC):
"""ETL(Extract-Transform-Load)パイプラインの基底クラス
テンプレートメソッドパターン: アルゴリズムの骨格を定義し、
具体的なステップをサブクラスに委ねる。
"""
def run(self) -> dict:
"""テンプレートメソッド: ETLの全体フロー(final)"""
self._log("パイプライン開始")
# 1. データ抽出
raw_data = self.extract()
self._log(f"抽出完了: {len(raw_data)} レコード")
# 2. データ変換
transformed = self.transform(raw_data)
self._log(f"変換完了: {len(transformed)} レコード")
# 3. バリデーション(オプショナルフック)
valid_data = self.validate(transformed)
self._log(f"バリデーション完了: {len(valid_data)} レコード")
# 4. データロード
result = self.load(valid_data)
self._log(f"ロード完了")
# 5. 後処理(オプショナルフック)
self.after_load(result)
return result
@abstractmethod
def extract(self) -> list[dict]:
"""データを抽出する(サブクラスで実装)"""
...
@abstractmethod
def transform(self, data: list[dict]) -> list[dict]:
"""データを変換する(サブクラスで実装)"""
...
@abstractmethod
def load(self, data: list[dict]) -> dict:
"""データをロードする(サブクラスで実装)"""
...
def validate(self, data: list[dict]) -> list[dict]:
"""バリデーション(デフォルト: すべて通過)"""
return data
def after_load(self, result: dict) -> None:
"""後処理(デフォルト: 何もしない)"""
pass
def _log(self, message: str) -> None:
print(f"[{self.__class__.__name__}] {message}")
# サブクラス: CSV → PostgreSQL のETL
class CsvToPostgresETL(ETLPipeline):
def __init__(self, csv_path: str, db_connection):
self.csv_path = csv_path
self.db = db_connection
def extract(self) -> list[dict]:
import csv
with open(self.csv_path) as f:
reader = csv.DictReader(f)
return list(reader)
def transform(self, data: list[dict]) -> list[dict]:
# 型変換やクレンジング
for row in data:
row["price"] = float(row.get("price", 0))
row["name"] = row.get("name", "").strip()
return data
def validate(self, data: list[dict]) -> list[dict]:
# 価格が正の値のデータのみ
return [row for row in data if row["price"] > 0]
def load(self, data: list[dict]) -> dict:
# PostgreSQLにINSERT
count = 0
for row in data:
self.db.execute(
"INSERT INTO products (name, price) VALUES (%s, %s)",
(row["name"], row["price"]),
)
count += 1
return {"inserted": count}
# サブクラス: API → Elasticsearch のETL
class ApiToElasticsearchETL(ETLPipeline):
def __init__(self, api_url: str, es_client):
self.api_url = api_url
self.es = es_client
def extract(self) -> list[dict]:
import requests
response = requests.get(self.api_url)
return response.json()["results"]
def transform(self, data: list[dict]) -> list[dict]:
# Elasticsearch用にドキュメント変換
return [
{
"_index": "products",
"_id": item["id"],
"_source": {
"name": item["name"],
"price": item["price"],
"category": item.get("category", "uncategorized"),
},
}
for item in data
]
def load(self, data: list[dict]) -> dict:
# Elasticsearchにバルクインサート
from elasticsearch.helpers import bulk
success, errors = bulk(self.es, data)
return {"success": success, "errors": len(errors)}
def after_load(self, result: dict) -> None:
# インデックスのリフレッシュ
self.es.indices.refresh(index="products")4.2 フレームワーク拡張(継承の適切な使用例)
// フレームワークが提供する基底クラスの拡張
// → これは継承が適切な場面
// React の クラスコンポーネント(歴史的な例)
abstract class Component<P, S> {
constructor(public props: P) {}
abstract render(): VNode;
setState(newState: Partial<S>): void {
// フレームワーク内部の処理
}
componentDidMount(): void {}
componentWillUnmount(): void {}
shouldComponentUpdate(nextProps: P, nextState: S): boolean {
return true;
}
}
// フレームワークの拡張ポイントとして継承
class UserProfile extends Component<UserProps, UserState> {
componentDidMount(): void {
this.fetchUser(this.props.userId);
}
render(): VNode {
// UIの描画
}
}
// Express のミドルウェア基底クラス(仮想例)
abstract class Middleware {
abstract handle(req: Request, res: Response, next: NextFunction): void;
protected sendError(res: Response, status: number, message: string): void {
res.status(status).json({ error: message });
}
}
class AuthMiddleware extends Middleware {
handle(req: Request, res: Response, next: NextFunction): void {
const token = req.headers.authorization;
if (!token) {
this.sendError(res, 401, "認証が必要です");
return;
}
// トークン検証...
next();
}
}
class RateLimitMiddleware extends Middleware {
private requests = new Map<string, number[]>();
handle(req: Request, res: Response, next: NextFunction): void {
const ip = req.ip;
const now = Date.now();
const windowMs = 60000; // 1分
const reqs = this.requests.get(ip) ?? [];
const recent = reqs.filter(t => now - t < windowMs);
if (recent.length >= 100) {
this.sendError(res, 429, "リクエスト制限を超えました");
return;
}
recent.push(now);
this.requests.set(ip, recent);
next();
}
}5. コンポジション vs 継承の判断フローチャート
判断フローチャート:
Q1: 「B は A の一種か?」(is-a 関係か?)
│
├── No → コンポジション
│
└── Yes
│
Q2: 「B は A の全メソッドを正しく実装できるか?」
│
├── No → コンポジション(+ ISPでインターフェース分離)
│
└── Yes
│
Q3: 「A の実装詳細に B が依存する必要があるか?」
│
├── Yes → 継承(ただし protected の使用を最小限に)
│
└── No
│
Q4: 「B の振る舞いは実行時に変更する必要があるか?」
│
├── Yes → コンポジション(Strategy パターン)
│
└── No
│
Q5: 「型階層は安定しているか?」
│
├── Yes → 継承で OK
│
└── No → コンポジション(将来の変更に備える)
具体的な判断例:
ArrayList extends AbstractList → ✅ 継承(is-a + 安定した型階層)
Stack extends Vector → ❌ 継承(Stack is-a Vector ではない)
CountingSet extends HashSet → ❌ 継承(脆い基底クラス問題)
Button extends Component → ✅ 継承(フレームワーク拡張)
Car has-a Engine → ✅ コンポジション(has-a 関係)
Logger has-a Formatter → ✅ コンポジション(実行時変更)
5.1 実務でよくあるケースの判断
// ケース1: ログ出力のカスタマイズ
// ❌ 継承
class FileLogger extends ConsoleLogger { ... }
class JsonLogger extends FileLogger { ... }
// → ロガーは is-a 関係ではなく、出力先の違い
// ✅ コンポジション
class Logger {
constructor(
private transport: LogTransport, // 出力先
private formatter: LogFormatter, // フォーマット
private filter: LogFilter, // フィルタ
) {}
}
// ケース2: HTTPクライアントの認証
// ❌ 継承
class AuthenticatedHttpClient extends HttpClient { ... }
class OAuthHttpClient extends AuthenticatedHttpClient { ... }
// ✅ コンポジション
class HttpClient {
constructor(private auth: AuthStrategy) {}
// BasicAuth, BearerToken, OAuth, NoAuth を差し替え可能
}
// ケース3: バリデーションロジック
// ❌ 継承
class EmailValidator extends StringValidator { ... }
class StrongPasswordValidator extends PasswordValidator { ... }
// ✅ コンポジション
class CompositeValidator implements Validator {
constructor(private validators: Validator[]) {}
validate(value: string): ValidationResult {
const errors = this.validators
.map(v => v.validate(value))
.filter(r => !r.isValid);
return errors.length === 0
? { isValid: true }
: { isValid: false, errors: errors.flatMap(r => r.errors) };
}
}
// バリデーションルールを組み合わせ
const passwordValidator = new CompositeValidator([
new MinLengthValidator(8),
new MaxLengthValidator(100),
new ContainsUppercaseValidator(),
new ContainsLowercaseValidator(),
new ContainsDigitValidator(),
new ContainsSpecialCharValidator(),
]);
// ケース4: データリポジトリ
// ❌ 継承
class CachedUserRepository extends PostgresUserRepository { ... }
// → キャッシュは永続化戦略ではない
// ✅ コンポジション(Decorator パターン)
class CachedRepository<T> implements Repository<T> {
constructor(
private inner: Repository<T>,
private cache: CacheStore,
) {}
async findById(id: string): Promise<T | null> {
const cached = await this.cache.get(id);
if (cached) return cached;
const result = await this.inner.findById(id);
if (result) await this.cache.set(id, result);
return result;
}
}
const userRepo = new CachedRepository(
new PostgresUserRepository(db),
new RedisCache(redis),
);6. 言語ごとのコンポジション支援機能
各言語のコンポジション支援:
Rust:
→ トレイト + impl → 明示的なコンポジション
→ 継承なし(設計上の意思決定)
→ derive マクロ → 自動実装
Go:
→ 埋め込み(embedding)→ 委譲の糖衣構文
→ インターフェースは暗黙的
→ 継承なし(設計上の意思決定)
Kotlin:
→ by キーワード → 委譲の糖衣構文
→ data class → 値オブジェクトの自動生成
Swift:
→ protocol extension → プロトコルにデフォルト実装
→ protocol composition → プロトコルの合成
TypeScript:
→ ミックスイン → クラス式による合成
→ インターセクション型 → 型レベルの合成
// Go: 埋め込み(Embedding)によるコンポジション
type Logger struct{}
func (l *Logger) Log(msg string) {
fmt.Printf("[LOG] %s\n", msg)
}
type Metrics struct{}
func (m *Metrics) RecordLatency(duration time.Duration) {
fmt.Printf("[METRICS] latency: %v\n", duration)
}
// 埋め込みによるコンポジション(委譲の糖衣構文)
type Service struct {
Logger // 埋め込み: Service.Log() が使える
Metrics // 埋め込み: Service.RecordLatency() が使える
db *sql.DB
}
func (s *Service) GetUser(id string) (*User, error) {
start := time.Now()
s.Log(fmt.Sprintf("Getting user: %s", id)) // Logger のメソッド
var user User
err := s.db.QueryRow("SELECT * FROM users WHERE id = $1", id).
Scan(&user.ID, &user.Name)
s.RecordLatency(time.Since(start)) // Metrics のメソッド
return &user, err
}// Kotlin: by キーワードによる委譲
interface Printer {
fun print(message: String)
}
class ConsolePrinter : Printer {
override fun print(message: String) {
println(message)
}
}
// by キーワードで委譲: printer に処理を委譲
class TimestampPrinter(private val printer: Printer) : Printer by printer {
// print() は自動的に printer に委譲される
// 必要に応じてオーバーライド
override fun print(message: String) {
val timestamp = java.time.LocalDateTime.now()
printer.print("[$timestamp] $message")
}
}
// 複数のインターフェースの委譲
interface Logger {
fun log(message: String)
}
interface Cache {
fun get(key: String): String?
fun set(key: String, value: String)
}
class MyService(
logger: Logger,
cache: Cache,
) : Logger by logger, Cache by cache {
// Logger と Cache の全メソッドが自動委譲
// このクラスでは追加のビジネスロジックのみ定義
fun processRequest(key: String): String {
log("Processing request for key: $key")
val cached = get(key)
if (cached != null) {
log("Cache hit for key: $key")
return cached
}
val result = "computed_result"
set(key, result)
return result
}
}// Rust: トレイトによるコンポジション(継承なし)
trait Drawable {
fn draw(&self);
}
trait Clickable {
fn on_click(&mut self);
}
trait Resizable {
fn resize(&mut self, width: u32, height: u32);
}
// 複数のトレイトを実装(コンポジション的)
struct Button {
label: String,
x: u32,
y: u32,
width: u32,
height: u32,
click_count: u32,
}
impl Drawable for Button {
fn draw(&self) {
println!("Drawing button '{}' at ({}, {})", self.label, self.x, self.y);
}
}
impl Clickable for Button {
fn on_click(&mut self) {
self.click_count += 1;
println!("Button '{}' clicked! (count: {})", self.label, self.click_count);
}
}
impl Resizable for Button {
fn resize(&mut self, width: u32, height: u32) {
self.width = width;
self.height = height;
}
}
// トレイトオブジェクトで動的ディスパッチ
fn draw_all(items: &[&dyn Drawable]) {
for item in items {
item.draw();
}
}
// トレイト境界でジェネリクスの制約
fn interactive<T: Drawable + Clickable + Resizable>(widget: &mut T) {
widget.draw();
widget.on_click();
widget.resize(200, 100);
widget.draw();
}実践演習
演習1: 基本的な実装
以下の要件を満たすコードを実装してください。
要件:
- 入力データの検証を行うこと
- エラーハンドリングを適切に実装すること
- テストコードも作成すること
# 演習1: 基本実装のテンプレート
class Exercise1:
"""基本的な実装パターンの演習"""
def __init__(self):
self.data = []
def validate_input(self, value):
"""入力値の検証"""
if value is None:
raise ValueError("入力値がNoneです")
return True
def process(self, value):
"""データ処理のメインロジック"""
self.validate_input(value)
self.data.append(value)
return self.data
def get_results(self):
"""処理結果の取得"""
return {
'count': len(self.data),
'data': self.data
}
# テスト
def test_exercise1():
ex = Exercise1()
assert ex.process(1) == [1]
assert ex.process(2) == [1, 2]
assert ex.get_results()['count'] == 2
try:
ex.process(None)
assert False, "例外が発生するべき"
except ValueError:
pass
print("全テスト合格!")
test_exercise1()演習2: 応用パターン
基本実装を拡張して、以下の機能を追加してください。
# 演習2: 応用パターン
from typing import List, Dict, Optional
from datetime import datetime
class AdvancedExercise:
"""応用パターンの演習"""
def __init__(self, max_size: int = 100):
self._items: List[Dict] = []
self._max_size = max_size
self._created_at = datetime.now()
def add(self, key: str, value: any) -> bool:
"""アイテムの追加(サイズ制限付き)"""
if len(self._items) >= self._max_size:
return False
self._items.append({
'key': key,
'value': value,
'timestamp': datetime.now().isoformat()
})
return True
def find(self, key: str) -> Optional[Dict]:
"""キーによる検索"""
for item in reversed(self._items):
if item['key'] == key:
return item
return None
def remove(self, key: str) -> bool:
"""キーによる削除"""
for i, item in enumerate(self._items):
if item['key'] == key:
self._items.pop(i)
return True
return False
def stats(self) -> Dict:
"""統計情報"""
return {
'total_items': len(self._items),
'max_size': self._max_size,
'usage_percent': len(self._items) / self._max_size * 100,
'uptime': str(datetime.now() - self._created_at)
}
# テスト
def test_advanced():
ex = AdvancedExercise(max_size=3)
assert ex.add("a", 1) == True
assert ex.add("b", 2) == True
assert ex.add("c", 3) == True
assert ex.add("d", 4) == False # サイズ制限
assert ex.find("b")['value'] == 2
assert ex.remove("b") == True
assert ex.find("b") is None
stats = ex.stats()
assert stats['total_items'] == 2
print("応用テスト全合格!")
test_advanced()演習3: パフォーマンス最適化
以下のコードのパフォーマンスを改善してください。
# 演習3: パフォーマンス最適化
import time
from functools import lru_cache
# 最適化前(O(n^2))
def slow_search(data: list, target: int) -> int:
"""非効率な検索"""
for i in range(len(data)):
for j in range(i + 1, len(data)):
if data[i] + data[j] == target:
return (i, j)
return (-1, -1)
# 最適化後(O(n))
def fast_search(data: list, target: int) -> tuple:
"""ハッシュマップを使った効率的な検索"""
seen = {}
for i, num in enumerate(data):
complement = target - num
if complement in seen:
return (seen[complement], i)
seen[num] = i
return (-1, -1)
# ベンチマーク
def benchmark():
import random
data = list(range(5000))
random.shuffle(data)
target = data[100] + data[4000]
start = time.time()
result1 = slow_search(data, target)
slow_time = time.time() - start
start = time.time()
result2 = fast_search(data, target)
fast_time = time.time() - start
print(f"非効率版: {slow_time:.4f}秒")
print(f"効率版: {fast_time:.6f}秒")
print(f"高速化率: {slow_time/fast_time:.0f}倍")
benchmark()ポイント:
- アルゴリズムの計算量を意識する
- 適切なデータ構造を選択する
- ベンチマークで効果を測定する
FAQ
Q1: このトピックを学ぶ上で最も重要なポイントは何ですか?
実践的な経験を積むことが最も重要です。理論だけでなく、実際にコードを書いて動作を確認することで理解が深まります。
Q2: 初心者がよく陥る間違いは何ですか?
基礎を飛ばして応用に進むことです。このガイドで説明している基本概念をしっかり理解してから、次のステップに進むことをお勧めします。
Q3: 実務ではどのように活用されていますか?
このトピックの知識は、日常的な開発業務で頻繁に活用されます。特にコードレビューやアーキテクチャ設計の際に重要になります。
まとめ
| 観点 | 継承 | コンポジション |
|---|---|---|
| 関係 | is-a | has-a |
| 結合度 | 強い | 弱い |
| 柔軟性 | 低い | 高い |
| 実行時変更 | 不可 | 可能 |
| 推奨度 | 限定的 | 優先 |
| テスト容易性 | 低い | 高い |
| 再利用性 | 型階層に依存 | 独立して再利用可能 |
実践的な指針:
1. デフォルトはコンポジション
→ 迷ったらコンポジションを選ぶ
→ 後から継承に変更するより、後からコンポジションに変更する方が困難
2. 継承を使う条件:
→ 明確な is-a 関係がある
→ 親クラスの全メソッドがサブクラスで意味を持つ(LSP準拠)
→ 型階層が安定している
→ フレームワークが要求している
3. 「コード再利用のための継承」は避ける
→ 共通コードが欲しいだけならユーティリティクラスやヘルパー関数
→ 振る舞いの再利用ならトレイト/ミックスイン
4. 継承の深さは2〜3レベルまで
→ 深い継承ツリーは理解が困難
→ 「A → B → C → D → E」は危険信号
5. 継承よりインターフェース
→ 型の互換性が必要ならインターフェースで十分
→ 実装の共有はコンポジション + 委譲で
次に読むべきガイド
参考文献
- Gamma, E. et al. "Design Patterns." Addison-Wesley, 1994. (Favor composition over inheritance)
- Bloch, J. "Effective Java." Item 18: Favor composition over inheritance. 3rd Edition, 2018.
- Martin, R. "Clean Architecture." Prentice Hall, 2017.
- Sandi Metz. "Practical Object-Oriented Design in Ruby." 2nd Edition, 2018.
- The Go Programming Language Specification. "Embedding." golang.org.
- The Rust Programming Language. "Traits." doc.rust-lang.org.