Menu
Coddy logo textTech

コンポジション vs 継承

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

クラス間の関係を設計する際、主に2つの選択肢があります:継承 ("is-a") と コンポジション ("has-a")。適切なアプローチを選択することは、コードの柔軟性と保守性に大きな影響を与えます。

継承はクラス間に強い結合を生み出します。Vehicleを継承するCarは、その関係に永続的に縛られます:

class Vehicle {
public:
    virtual void start() { std::cout << "Starting vehicle\n"; }
};

class Car : public Vehicle {
public:
    void start() override { std::cout << "Starting car\n"; }
};

コンポジションは、オブジェクトをメンバとして埋め込み、より柔軟な関係を構築します。Car はエンジンそのものであるのではなく、Engine を持っています:

class Engine {
public:
    void start() { std::cout << "Engine running\n"; }
};

class Car {
private:
    Engine engine;  // CarはEngineを持っています
public:
    void start() { engine.start(); }
};

コンポジションの主な利点は柔軟性です。硬直したクラス階層の制約を受けることなく、コンポーネントを簡単に交換したり、実行時に動作を変更したり、複数の機能を組み合わせたりすることができます。現代のC++設計では、ほとんどの場合、継承よりもコンポジションが好まれます。

継承は、真の「is-a」関係があり、ポリモーフィズムが必要な場合に使用します。コンポジションは、機能を再利用したい場合や、その関係が「has-a」または「uses-a」と表現するのが適切な場合に使用します。

challenge icon

チャレンジ

簡単

継承ではなくコンポジションをいつ使用すべきかを示す、コンピュータシステムシミュレータを構築しましょう。コンピュータがコンポーネントそのもの(継承)ではなく、コンポーネントを「持っている」(コンポジション)というモデルを作成し、コンポジションが実行時に部品を入れ替える柔軟性をどのように提供するかを確認します。

コードは以下の3つのファイルに分けて構成します:

  • Components.h: コンピュータが含めることができる個々のコンポーネントを定義します。

    プライベートな std::string modelint cores を持つ CPU クラスを作成します。両方の値を受け取るコンストラクタ、CPU [model] processing with [cores] cores と出力する process() メソッド、および getModel() メソッドを提供してください。

    プライベートな int sizeGB を持つ Memory クラスを作成します。コンストラクタ、Loading data into [sizeGB]GB RAM と出力する load() メソッド、および getSize() メソッドを提供してください。

    プライベートな std::string type ("SSD" や "HDD" など) と int capacityGB を持つ Storage クラスを作成します。コンストラクタ、Reading from [capacityGB]GB [type] と出力する read() メソッド、および両方のフィールドのゲッターメソッドを提供してください。

  • Computer.h: コンポジションを使用してコンポーネントを組み合わせる Computer クラスを定義します。

    Computer クラスは、プライベートメンバオブジェクトとして CPUMemoryStorage を持つ必要があります。コンストラクタの初期化リストを使用して、パラメータからこれらのコンポーネントを初期化してください。

    以下のメソッドを実装してください:

    • boot()Booting computer... と出力し、次にメモリの load()、ストレージの read()、CPU の process() をそれぞれ個別の行で呼び出します。
    • specs() — コンピュータの仕様を以下の形式で出力します:
      Computer Specs:
      - CPU: [model] ([cores] cores)
      - RAM: [size]GB
      - Storage: [capacity]GB [type]
  • main.cpp: 6つの入力(それぞれ個別の行)を読み取ります:
    1. CPUモデル名 (文字列)
    2. CPUコア数 (整数)
    3. メモリサイズ (GB、整数)
    4. ストレージタイプ (文字列、例: "SSD")
    5. ストレージ容量 (GB、整数)
    6. コマンド: "boot" または "specs" のいずれか

    コンポーネントオブジェクトを作成し、コンポジションを使用して Computer を作成します。コマンドに基づいて、boot() または specs() を呼び出します。

例えば、入力が Intel i7816SSD512、および boot の場合:

Booting computer...
Loading data into 16GB RAM
Reading from 512GB SSD
CPU Intel i7 processing with 8 cores

入力が AMD Ryzen632HDD1000、および specs の場合:

Computer Specs:
- CPU: AMD Ryzen (6 cores)
- RAM: 32GB
- Storage: 1000GB HDD

Computer クラスがいかなるコンポーネントからも継承しておらず、それらを「含んでいる」ことに注目してください。この「has-a(持っている)」関係は、異なるコンポーネントの組み合わせでコンピュータを簡単に作成したり、セッターメソッドを追加すれば後でコンポーネントを交換したりできることを意味します。この柔軟性が、継承に対するコンポジションの主な利点です。

チートシート

クラス間の関係を設計する際は、継承(「is-a」関係)とコンポジション(「has-a」関係)のどちらかを選択します。

継承はクラス間に密な結合を作成します:

class Vehicle {
public:
    virtual void start() { std::cout << "Starting vehicle\n"; }
};

class Car : public Vehicle {
public:
    void start() override { std::cout << "Starting car\n"; }
};

コンポジションはオブジェクトをメンバとして埋め込み、柔軟な関係を作成します:

class Engine {
public:
    void start() { std::cout << "Engine running\n"; }
};

class Car {
private:
    Engine engine;  // CarはEngineを持っています
public:
    void start() { engine.start(); }
};

それぞれの使い分け:

  • ポリモーフィズムが必要な、真の「is-a」関係には継承を使用します
  • 機能を再利用する場合や、「has-a」/「uses-a」関係にはコンポジションを使用します
  • 現代的なC++設計では、その柔軟性からコンポジションが好まれます。コンポーネントの入れ替えや実行時の動作変更が可能になり、厳格な階層構造なしに機能を組み合わせることができます

自分で試してみよう

#include <iostream>
#include <string>
#include "Computer.h"

using namespace std;

int main() {
    // 入力を読み込む
    string cpuModel;
    getline(cin, cpuModel);  // CPUモデル名(スペースを含む場合がある)
    
    int cpuCores;
    cin >> cpuCores;  // CPUコア数
    
    int memorySize;
    cin >> memorySize;  // メモリ容量 (GB)
    
    string storageType;
    cin >> storageType;  // ストレージの種類(例: "SSD")
    
    int storageCapacity;
    cin >> storageCapacity;  // ストレージ容量 (GB)
    
    string command;
    cin >> command;  // コマンド: "boot" または "specs"

    // TODO: コンポーネントオブジェクト(CPU, Memory, Storage)を作成する
    
    // TODO: コンポジションを使用して Computer オブジェクトを作成する
    
    // TODO: コマンドに基づいて、boot() または specs() のいずれかを呼び出す

    return 0;
}
quiz icon腕試し

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

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