Menu
Coddy logo textTech

コンポジション vs 継承

CoddyのJavaジャーニー「オブジェクト指向プログラミング」セクションの一部 — レッスン 47/87。

継承は「is-a」関係を作成します。つまり、DogAnimal であるということです。しかし、クラスが自身ではないものから機能を必要とする場合はどうすればよいでしょうか?そこで登場するのがコンポジションであり、代わりに「has-a」関係を構築します。

コンポジションを用いると、クラスは他のクラスを継承するのではなく、それらのインスタンスをフィールドとして保持します:

// 継承によるアプローチ: Car(車)はEngine(エンジン)である(意味が通りません)
class Car extends Engine { }

// コンポジションによるアプローチ: Car(車)はEngine(エンジン)を持っている(理にかなっています)
class Car {
    private Engine engine;
    
    public Car(Engine engine) {
        this.engine = engine;
    }
    
    public void start() {
        engine.ignite();
    }
}

コンポジションにはいくつかの利点があります。エンジンの入れ替えなど、実行時にコンポーズされたオブジェクトを変更することができます。

複数のクラスから振る舞いを組み合わせることもできます。これはJavaの継承では許可されていないことです。構成されたクラスは、自身が選択したメソッドのみを公開するため、より優れたカプセル化を提供します。

実践的なガイドライン:真の「is-a」関係があり、サブクラスが親クラスの特化したバージョンを真に表している場合に、継承を使用してください。

その型になることなく、別のクラスの機能を使用する必要がある場合は、コンポジションを使用してください。多くの経験豊富な開発者は、「継承よりもコンポジションを優先する」という原則に従っています。なぜなら、その方がより柔軟でメンテナンス性の高いコードにつながるからです。

challenge icon

チャレンジ

簡単

「has-a」関係を実証するために、コンポジションを使用してコンピュータシステムを構築しましょう。Computer がそのコンポーネントから継承する(これは意味をなしません)のではなく、コンピュータが機能するために必要なパーツを保持(contain)するようにします。

コードを3つのファイルに分けて整理します:

  • Processor.java: CPUを表すクラスを作成します。Processorは2つの private フィールド、brand (String) と speedGHz (double) を持ちます。両方のフィールドを初期化するコンストラクタと、それぞれのゲッターメソッドを含めてください。さらに、Stringを返す process() メソッドを追加します:[brand] processing at [speedGHz] GHz
  • Memory.java: RAMを表すクラスを作成します。Memoryは2つの private フィールド、type (String、"DDR4" や "DDR5" など) と sizeGB (int) を持ちます。コンストラクタ、ゲッター、および次を返す load() メソッドを含めてください:Loading [sizeGB]GB [type] memory
  • Computer.java: ここでコンポジションの本領が発揮されます!Computerクラスは、ProcessorとMemoryを private フィールドとして持つ必要があります。これらを継承(extend)するのではなく、包含(contain)します。両方のコンポーネントを受け取るコンストラクタを含めてください。コンポーネントを使用してコンピュータが起動する様子を示す、複数行の String を返す boot() メソッドを追加します:
    Booting computer...
    [processor.process() result]
    [memory.load() result]
    System ready!
    また、次を返す getSpecs() メソッドも追加してください:Specs: [processor brand] CPU, [memory sizeGB]GB [memory type]
  • Main.java: 構成したシステムを動かしてみましょう!プロセッサのブランド (String)、プロセッサの速度 (double)、メモリのタイプ (String)、メモリのサイズ (int) の4つの入力を受け取ります。

    これらの値を使用して Processor と Memory を作成し、それらを Computer に組み込みます。boot() を呼び出した結果を出力し、次に getSpecs() の結果を出力してください。

入力は、プロセッサのブランド、プロセッサの速度 (GHz)、メモリのタイプ、メモリのサイズ (GB) の順に4つ与えられます。

Computer がコンポーネントそのものになろうとするのではなく、コンポーネントに仕事を委譲していることに注目してください。これがコンポジションの力です!Computer は提供された任意の Processor や Memory を使用できるため、設計が柔軟で現実的になります。

チートシート

コンポジションは、クラスを継承するのではなく、他のクラスのインスタンスをフィールドとして保持する「has-a」関係を構築します。

継承 vs コンポジション:

// 継承: Car(車)は Engine(エンジン)である(意味が通りません)
class Car extends Engine { }

// コンポジション: Car(車)は Engine(エンジン)を持っている(意味が通ります)
class Car {
    private Engine engine;
    
    public Car(Engine engine) {
        this.engine = engine;
    }
    
    public void start() {
        engine.ignite();
    }
}

コンポジションの利点:

  • 実行時にオブジェクトを変更できる
  • 複数のクラスの振る舞いを組み合わせることができる
  • カプセル化の向上 - 選択したメソッドのみを公開できる
  • より柔軟でメンテナンス性の高いコード

使い分け:

  • 継承: 真の「is-a」関係があり、サブクラスが親クラスの特化したバージョンである場合に使用します
  • コンポジション: その型そのものではなく、別のクラスの機能が必要な場合に使用します

ベストプラクティス: 「継承よりもコンポジションを優先する」

自分で試してみよう

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        
        // 入力を読み込む
        String processorBrand = scanner.nextLine();
        double processorSpeed = Double.parseDouble(scanner.nextLine());
        String memoryType = scanner.nextLine();
        int memorySize = Integer.parseInt(scanner.nextLine());
        
        // TODO: ブランドと速度を指定して Processor を作成する
        
        // TODO: タイプとサイズを指定して Memory を作成する
        
        // TODO: Processor と Memory を組み合わせて Computer を作成する
        
        // TODO: boot() の結果を出力する
        
        // TODO: getSpecs() の結果を出力する
    }
}
quiz icon腕試し

このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。

オブジェクト指向プログラミングのすべてのレッスン