コンポジション
CoddyのRustジャーニー「Object Oriented Programming」セクションの一部 — レッスン 55/61。
多くのオブジェクト指向言語は、型の間で機能を共有するために継承に大きく依存しています。Rustは異なるアプローチ、つまりコンポジション(合成)を採用しています。親から振る舞いを継承する代わりに、他の型をフィールドとして含めることで複雑な型を構築します。
車のモデリングを考えてみましょう。ベースとなる Vehicle クラスを作成してそれを継承するのではなく、より小さく、特定の目的に特化したコンポーネントから Car を構成します:
struct Engine {
horsepower: u32,
}
struct Wheels {
count: u8,
diameter: f32,
}
struct Car {
engine: Engine,
wheels: Wheels,
brand: String,
}
CarはEngineやWheelsを継承しているわけではありません。それらを持っているのです。この「has-a」関係がコンポジションです。フィールドを通じて内部コンポーネントにアクセスし、それらのメソッドを直接呼び出すことができます。
impl Engine {
fn start(&self) {
println!("Engine with {} HP started!", self.horsepower);
}
}
impl Car {
fn start(&self) {
self.engine.start(); // 内部コンポーネントに委譲する
}
}
このパターンは、各 struct が単一の責任に集中するようにします。エンジンの仕組みを変更する必要がありますか?Engine struct だけを修正してください。Wheels をオートバイに再利用したいですか?新しい struct にそれを含めるだけです。コンポジションは、継承によって頻繁に発生する密結合なしに柔軟性を提供します。
チャレンジ
簡単コンポジションを使ってコンピュータシステムを構築しましょう!1つの巨大な構造体を作成する代わりに、より小さく、役割が明確なコンポーネントである CPU と Memory から Computer を構成します。各コンポーネントは自分自身を説明する方法を知っており、Computer はその詳細を各パーツに委ねます。
コードは3つのファイルに分けて整理します:
cpu.rs: 2つの公開フィールドを持つ公開構造体CPUを定義します。コア数を示すcores(u32) と、クロック速度を示すspeed_ghz(f32) です。CPUを{cores}-core @ {speed_ghz}GHzという形式で説明する文字列を返すspecsメソッドを追加してください。memory.rs: メモリサイズを示す公開フィールドsize_gb(u32) を持つ公開構造体Memoryを定義します。{size_gb}GB RAMという形式の文字列を返すspecsメソッドを追加してください。main.rs: 両方のモジュールを取り込み、CPU型のcpuフィールドとMemory型のmemoryフィールドを「持つ」公開構造体Computerを作成します。Computer にsystem_infoメソッドを実装し、各コンポーネントに処理を委ねてシステム全体の仕様を表示するようにします。提供された入力を使用して Computer を構築し、その情報を表示してください。
system_info メソッドは次のように出力する必要があります:
System: {cpu_specs} | {memory_specs}例えば、入力が 8、3.5、16 の場合:
System: 8-core @ 3.5GHz | 16GB RAM入力が 4、2.8、32 の場合:
System: 4-core @ 2.8GHz | 32GB RAM3つの入力を受け取ります:CPUコア数(u32 としてパース)、CPU速度(f32 としてパース)、およびメモリサイズ(u32 としてパース)です。
チートシート
Rustは、型の間で機能を共有するために、継承ではなくコンポジション(合成)を使用します。他の型をフィールドとして含めることで複雑な型を構築し、「has-a(〜を持っている)」関係を作成します。
基本的なコンポジション
小さく、目的が明確な構造体を定義し、それらを組み合わせてより大きな構造体を作成します:
struct Engine {
horsepower: u32,
}
struct Wheels {
count: u8,
diameter: f32,
}
struct Car {
engine: Engine,
wheels: Wheels,
brand: String,
}
コンポーネントへの委譲
フィールドを介して内部コンポーネントにアクセスし、そのメソッドを呼び出します:
impl Engine {
fn start(&self) {
println!("Engine with {} HP started!", self.horsepower);
}
}
impl Car {
fn start(&self) {
self.engine.start(); // 内部コンポーネントに委譲
}
}
コンポジションにより、各構造体は単一の責任に集中し、密結合を避けながら柔軟性を提供します。
自分で試してみよう
mod cpu;
mod memory;
use cpu::CPU;
use memory::Memory;
// TODO: 2つのパブリックフィールドを持つパブリックな Computer 構造体を定義してください:
// - cpu: CPU
// - memory: Memory
// TODO: Computer に以下の処理を行う system_info メソッドを実装してください:
// - cpu.specs() と memory.specs() に委譲する
// - 次のように出力する: System: {cpu_specs} | {memory_specs}
fn main() {
let mut input = String::new();
std::io::stdin().read_line(&mut input).expect("Failed to read line");
let cores: u32 = input.trim().parse().expect("Invalid number");
input.clear();
std::io::stdin().read_line(&mut input).expect("Failed to read line");
let speed_ghz: f32 = input.trim().parse().expect("Invalid number");
input.clear();
std::io::stdin().read_line(&mut input).expect("Failed to read line");
let size_gb: u32 = input.trim().parse().expect("Invalid number");
// TODO: パースされた入力を使用して CPU と Memory のインスタンスを作成してください
// TODO: CPU と Memory を使用して Computer のインスタンスを作成してください
// TODO: computer の system_info() を呼び出して結果を出力してください
}
このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。