コンポジション vs 継承
CoddyのDartジャーニー「オブジェクト指向プログラミング」セクションの一部 — レッスン 82/110。
継承は「is-a」関係を作成する(Dog は Animal である)ということを学びました。しかし、もう一つの強力なアプローチがあります。それは「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」関係があり、ポリモーフィズムが必要な場合には継承を使用します。密結合を避けつつ振る舞いを再利用したい場合や、オブジェクトが複数のソースからの機能を必要とする場合には、合成を使用します。
チャレンジ
簡単コンポジション(合成)を使ってコンピュータシステムを構築しましょう!コンピュータをその部品から継承させるのではなく、個別のコンポーネントクラスを作成してそれらを組み合わせます。なぜなら、コンピュータはCPUを「持っており(has)」、メモリを「持っている(has)」のであって、コンピュータ自体がCPUやメモリそのものではないからです。
コードを3つのファイルに整理します:
components.dart: コンピュータの部品を表す2つのコンポーネントクラスを作成します:String brandとdouble speedGHzを持つCPUクラス。[brand] processing at [speedGHz]GHzという文字列を返すprocess()メソッドを含めてください。int sizeGBを持つMemoryクラス。Loading [sizeGB]GB of dataという文字列を返すload()メソッドを含めてください。
computer.dart: コンポジションを使用してコンポーネントを組み合わせるComputerクラスを作成します。このコンピュータは以下の条件を満たす必要があります:String nameフィールドを持つことCPUとMemoryのインスタンスを保持すること(コンポジション - コンピュータはこれらの部品を「持っている」状態です)- コンストラクタを通じて必要なすべての値を受け取り、内部で両方のコンポーネントを作成すること
- 両方のコンポーネントに処理を委譲し、3行出力する
boot()メソッドを持つこと:コンピュータの名前に続けてstarting...、次にCPUのprocessメソッドの結果、最後にMemoryのloadメソッドの結果を表示します - システムを説明する文字列
[name]: [brand] [speedGHz]GHz, [sizeGB]GB RAMを返すspecs()メソッドを持つこと
main.dart: ファイルをインポートし、コンポジションの動作を実演します:- 名前が
Workstation、CPUがIntelで3.5GHz、メモリが16GBのComputerを作成します boot()メソッドを呼び出しますspecs()の結果をプリントします
- 名前が
Computer クラスが CPU や Memory を継承(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() の結果をプリントしてください
}
このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。
オブジェクト指向プログラミングのすべてのレッスン
4Null Safety
Null Safety 入門Nullable と Non-Nullable? 演算子と ! 演算子late キーワードと Null SafetyNull-Aware 演算子クラスにおける Null Safetyまとめ:ユーザープロフィールシステム10コレクションとジェネリクス
List, Set, Map の概要型安全なコレクションジェネリッククラスジェネリックメソッドジェネリクスの制約Iterable と Iteratorまとめ:ジェネリックなストレージ13高度な OOP の概念
コンポジション vs 継承拡張メソッドCallable クラスSealed クラス (Dart 3)レコード (Dart 3)パターンとマッチング (3.0)メソッドを持つ Enum