Menu
Coddy logo textTech

コンポジション vs 継承

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

継承は「is-a」関係を作成する(DogAnimal である)ということを学びました。しかし、もう一つの強力なアプローチがあります。それは「has-a」関係を作成するコンポジション(合成)です。振る舞いを継承する代わりに、クラスが他のクラスのインスタンスを保持し、それらに処理を委譲します。

Carクラスを考えてみましょう。継承を使うと、Engineクラスを拡張しようとするかもしれませんが、車はエンジンではありません。車はエンジンを「持っている(has)」のです:

class Engine {
  void start() => print('Engine started');
}

class Car {
  final Engine _engine = Engine();  // コンポジション: CarはEngineを所有しています
  
  void start() {
    _engine.start();  // エンジンに処理を委譲します
    print('Car is ready to drive');
  }
}

void main() {
  var car = Car();
  car.start();
}

コンポジションの主な利点は柔軟性です。継承の場合、単一の親クラスに縛られてしまいます。コンポジションを使用すると、複数のコンポーネントを組み合わせることができ、実行時にそれらを入れ替えることさえ可能です:

class ElectricEngine {
  void start() => print('Electric motor humming');
}

class HybridCar {
  Engine? _gasEngine;
  ElectricEngine? _electricEngine;
  
  HybridCar({bool useElectric = false}) {
    if (useElectric) {
      _electricEngine = ElectricEngine();
    } else {
      _gasEngine = Engine();
    }
  }
}

一般的なガイドラインは 「継承よりも合成(コンポジション)を優先する」 です。真の「is-a」関係があり、ポリモーフィズムが必要な場合には継承を使用します。密結合を避けつつ振る舞いを再利用したい場合や、オブジェクトが複数のソースからの機能を必要とする場合には、合成を使用します。

challenge icon

チャレンジ

簡単

コンポジション(合成)を使ってコンピュータシステムを構築しましょう!コンピュータをその部品から継承させるのではなく、個別のコンポーネントクラスを作成してそれらを組み合わせます。なぜなら、コンピュータはCPUを「持っており(has)」、メモリを「持っている(has)」のであって、コンピュータ自体がCPUやメモリそのものではないからです。

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

  • components.dart: コンピュータの部品を表す2つのコンポーネントクラスを作成します:
    • String branddouble speedGHz を持つ CPU クラス。[brand] processing at [speedGHz]GHz という文字列を返す process() メソッドを含めてください。
    • int sizeGB を持つ Memory クラス。Loading [sizeGB]GB of data という文字列を返す load() メソッドを含めてください。
  • computer.dart: コンポジションを使用してコンポーネントを組み合わせる Computer クラスを作成します。このコンピュータは以下の条件を満たす必要があります:
    • String name フィールドを持つこと
    • CPUMemory のインスタンスを保持すること(コンポジション - コンピュータはこれらの部品を「持っている」状態です)
    • コンストラクタを通じて必要なすべての値を受け取り、内部で両方のコンポーネントを作成すること
    • 両方のコンポーネントに処理を委譲し、3行出力する boot() メソッドを持つこと:コンピュータの名前に続けて starting...、次にCPUのprocessメソッドの結果、最後にMemoryのloadメソッドの結果を表示します
    • システムを説明する文字列 [name]: [brand] [speedGHz]GHz, [sizeGB]GB RAM を返す specs() メソッドを持つこと
  • main.dart: ファイルをインポートし、コンポジションの動作を実演します:
    • 名前が Workstation、CPUが Intel3.5 GHz、メモリが 16 GBの Computer を作成します
    • boot() メソッドを呼び出します
    • specs() の結果をプリントします

Computer クラスが CPUMemory を継承(extend)していないことに注目してください。代わりにそれらを保持し、仕事を委譲しています。これがコンポジションによって作られる「has-a」関係です!

期待される出力:

Workstation starting...
Intel processing at 3.5GHz
Loading 16GB of data
Workstation: Intel 3.5GHz, 16GB RAM

チートシート

コンポジションは、あるクラスが他のクラスのインスタンスを保持し、それらに処理を委譲することで、「has-a(〜を持っている)」の関係を構築します。これは、他のクラスから継承するのではなく、それらを利用する手法です。

基本的なコンポジションの例:

class Engine {
  void start() => print('Engine started');
}

class Car {
  final Engine _engine = Engine();  // Car HAS an Engine (車はエンジンを持っている)
  
  void start() {
    _engine.start();  // Delegate to the engine (エンジンに委譲)
    print('Car is ready to drive');
  }
}

柔軟性のあるコンポジション - コンポーネントの入れ替え:

class ElectricEngine {
  void start() => print('Electric motor humming');
}

class HybridCar {
  Engine? _gasEngine;
  ElectricEngine? _electricEngine;
  
  HybridCar({bool useElectric = false}) {
    if (useElectric) {
      _electricEngine = ElectricEngine();
    } else {
      _gasEngine = Engine();
    }
  }
}

コンポジションと継承の使い分け:

  • ポリモーフィズムが必要な、真の「is-a(〜である)」関係には継承を使用します。
  • 密結合を避けつつ振る舞いを再利用したい場合や、オブジェクトが複数のソースからの機能を必要とする場合にはコンポジションを使用します。
  • 一般的なガイドライン:「継承よりもコンポジションを優先する」

自分で試してみよう

import 'computer.dart';

void main() {
  // TODO: 次の構成で "Workstation" という名前の Computer を作成してください:
  // - 3.5 GHz で動作する Intel CPU
  // - 16 GB のメモリ

  // TODO: boot() メソッドを呼び出してください

  // TODO: specs() の結果をプリントしてください
}
quiz icon腕試し

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

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