Java | オブジェクト指向:コンポジション(委譲)

Java Java
スポンサーリンク

コンポジション(委譲)とは

コンポジションは「クラスが“持つ(has-a)”関係で他のオブジェクトを部品として組み合わせ、仕事をその部品へ任せる(委譲する)」設計です。継承(is-a)と違い、親の内部契約や振る舞いに縛られません。必要な役割を小さく分けて持ち、呼び出しを委譲することで、依存が柔らかく保たれ、差し替え・テスト・拡張が容易になります。


なぜ継承よりコンポジションなのか(重要)

継承は「親の設計に強く結びつく」ため、親の変更が子へ波及し、拡張で破綻しやすい弱点があります。コンポジションは「役割を部品として持つ」だけなので、部品の差し替えで振る舞いを変えられ、壊れにくいです。単一責務の部品を明確化すれば、組み合わせで多様な振る舞いを安全に作れます。テストでは部品をフェイクに置き換えるだけで、外部依存なしの検証ができます。


基本パターン:役割を持って委譲する

コンポジションの最小形は「フィールドで役割(インターフェース)を持ち、メソッドでその役割へ処理を委譲する」ことです。

interface Formatter { String format(String raw); }

final class UpperFormatter implements Formatter {
    @Override public String format(String raw) {
        return raw == null ? "" : raw.toUpperCase();
    }
}

final class NameService {                // “持つ”ことで振る舞いを手に入れる
    private final Formatter formatter;   // 部品(役割)を保持
    NameService(Formatter formatter) {   // コンストラクタ注入で自由に差し替え
        this.formatter = formatter;
    }
    String render(String s) {            // 委譲(フォーマッタへ任せる)
        return formatter.format(s);
    }
}
Java

この形なら、UpperFormatter を SnakeFormatter に差し替えるだけで振る舞いが変わります。NameService 自体のコードは不変で、拡張時の波及がありません。


継承との対比:悪い継承をコンポジションで直す(重要な深掘り)

継承で「親のメソッドを上書きし、内部に踏み込む」設計は壊れやすいです。差し替えたい振る舞いを“持つ”へ変更しましょう。

// 悪い例:親の実装に縛られる継承
class Report {
    String build(String raw) { return "REPORT: " + raw; }
}
class FancyReport extends Report {
    @Override String build(String raw) {
        // 親実装の依存や順序に縛られる
        return super.build(raw.toUpperCase());
    }
}

// 良い例:コンポジションで役割を分離
interface Title { String make(String raw); }

final class SimpleTitle implements Title {
    @Override public String make(String raw) {
        return "REPORT: " + raw;
    }
}

final class ReportService {
    private final Title title;                 // 役割を“持つ”
    ReportService(Title title) { this.title = title; }
    String build(String raw) { return title.make(raw); } // 委譲
}
Java

役割を Title に切り出して持つだけなら、Fancy な作りへ自由に差し替え可能です。親子関係の制約から解放され、変更に強い構造になります。


デコレータで“足し引き可能”な拡張をする

コンポジションはデコレータパターンと相性が抜群です。元の役割を包み、前後に処理を追加できます。入れ子にすれば、機能を足し引き自在に組み替えられます。

interface Storage { void put(String key, String value); }

final class MemoryStorage implements Storage {
    private final java.util.Map<String,String> map = new java.util.HashMap<>();
    @Override public void put(String key, String value) { map.put(key, value); }
}

// デコレータ(前後に処理を追加)
final class LoggingStorage implements Storage {
    private final Storage inner;                 // 中身(元の役割)を“持つ”
    LoggingStorage(Storage inner) { this.inner = inner; }
    @Override public void put(String key, String value) {
        System.out.println("[LOG] " + key + "=" + value);
        inner.put(key, value);                   // 委譲
    }
}

final class ValidatingStorage implements Storage {
    private final Storage inner;
    ValidatingStorage(Storage inner) { this.inner = inner; }
    @Override public void put(String key, String value) {
        if (key == null || key.isBlank()) throw new IllegalArgumentException("key");
        inner.put(key, value);                   // 委譲
    }
}

// 組み合わせは自由
Storage st = new LoggingStorage(new ValidatingStorage(new MemoryStorage()));
st.put("a", "1");
Java

追加機能をクラス階層に積まず、包んで重ねることで、衝突せず拡張できます。テストでは各層を単独で検証可能です。


戦略の差し替えと委譲の相乗効果

ストラテジパターン(“やり方の違い”をインターフェースで表す)と委譲を組み合わせると、呼び出し側の分岐を消し、柔軟さが最大化します。

interface Discount { int apply(int price); }

final class NoDiscount implements Discount {
    @Override public int apply(int price) { return price; }
}
final class RateDiscount implements Discount {
    private final int percent;
    RateDiscount(int percent) { this.percent = percent; }
    @Override public int apply(int price) { return Math.max(0, price - price * percent / 100); }
}

final class Cart {
    private final Discount discount;           // “持って任せる”
    Cart(Discount discount) { this.discount = discount; }
    int checkout(int price) { return discount.apply(price); }  // 委譲
}
Java

割引の種類追加は Discount の実装を増やすだけ。Cart は不変で、切り替えは注入で完了します。


実務での指針と落とし穴(重要な深掘り)

コンポジションは「部品の責務を小さく明確に切る」ほど効きます。巨大なインターフェースに依存すると結局強い結合になります。役割を分解し、最小の契約に依存してください。委譲先の選定はコンストラクタ注入にし、クラス内部で new して固定しないこと。外部資源(ファイル・DB・ネット)はポート(インターフェース)として持ち、アダプタ(具体)は境界の外で作るとテスト容易性が上がります。

一方で、委譲の鎖を深くしすぎると読解が難しくなります。階層は浅く、名前で意図を明確にし、ログや監査などの“横断的関心事”はデコレータで一段ずつ足すのが安全です。また、状態共有が必要なら抽象クラスで骨格を固定し、差し替えは役割で委譲する“併用”が効きます。


まとめと設計の勘所

コンポジション(委譲)は「クラスが役割を持ち、その役割へ仕事を任せる」設計です。継承より結合が弱く、差し替え・テスト・拡張に圧倒的に強い。デコレータで前後処理を重ね、ストラテジで“やり方”を差し替える。依存は抽象へ、注入で固定、責務は小さく——この型を守ると、読みやすく壊れにくい設計が自然に育ちます。

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