コンポジション 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」と表現するのが適切な場合に使用します。
チャレンジ
簡単継承ではなくコンポジションをいつ使用すべきかを示す、コンピュータシステムシミュレータを構築しましょう。コンピュータがコンポーネントそのもの(継承)ではなく、コンポーネントを「持っている」(コンポジション)というモデルを作成し、コンポジションが実行時に部品を入れ替える柔軟性をどのように提供するかを確認します。
コードは以下の3つのファイルに分けて構成します:
Components.h: コンピュータが含めることができる個々のコンポーネントを定義します。プライベートな
std::string modelとint 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クラスは、プライベートメンバオブジェクトとしてCPU、Memory、Storageを持つ必要があります。コンストラクタの初期化リストを使用して、パラメータからこれらのコンポーネントを初期化してください。以下のメソッドを実装してください:
boot()—Booting computer...と出力し、次にメモリのload()、ストレージのread()、CPU のprocess()をそれぞれ個別の行で呼び出します。specs()— コンピュータの仕様を以下の形式で出力します:Computer Specs: - CPU: [model] ([cores] cores) - RAM: [size]GB - Storage: [capacity]GB [type]
main.cpp: 6つの入力(それぞれ個別の行)を読み取ります:- CPUモデル名 (文字列)
- CPUコア数 (整数)
- メモリサイズ (GB、整数)
- ストレージタイプ (文字列、例: "SSD")
- ストレージ容量 (GB、整数)
- コマンド: "boot" または "specs" のいずれか
コンポーネントオブジェクトを作成し、コンポジションを使用して
Computerを作成します。コマンドに基づいて、boot()またはspecs()を呼び出します。
例えば、入力が Intel i7、8、16、SSD、512、および boot の場合:
Booting computer...
Loading data into 16GB RAM
Reading from 512GB SSD
CPU Intel i7 processing with 8 cores入力が AMD Ryzen、6、32、HDD、1000、および specs の場合:
Computer Specs:
- CPU: AMD Ryzen (6 cores)
- RAM: 32GB
- Storage: 1000GB HDDComputer クラスがいかなるコンポーネントからも継承しておらず、それらを「含んでいる」ことに注目してください。この「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;
}
このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。
オブジェクト指向プログラミングのすべてのレッスン
3コンストラクタとデストラクタ
デフォルトコンストラクタ引数付きコンストラクタコピーコンストラクタムーブコンストラクタコンストラクタ初期化リスト委譲コンストラクタデストラクタ詳解Rule of Three / Five / Zeroまとめ - Stringクラス6演算子オーバーロード
演算子オーバーロード入門算術演算子のオーバーロード比較演算子のオーバーロードストリーム演算子代入演算子のオーバーロード[] と () 演算子のオーバーロード型変換演算子まとめ:Matrix クラス