Menu
Coddy logo textTech

コンポジション vs 継承

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

前章では、トレイトが「has-a」機能を提供する一方で、継承は「is-a」関係を確立することを学びました。コンポジションは、より単純なオブジェクトから複雑なオブジェクトを構築し、それらを継承するのではなくプロパティとして保持することで、「has-a」の概念をさらに推し進めます。

継承を使用すると、CarVehicle を拡張(extend)する場合があります。コンポジションを使用すると、CarEngine オブジェクトを含みます:

<?php
class Engine {
    public function start(): string {
        return "Engine started";
    }
}

class Car {
    private Engine $engine;
    
    public function __construct() {
        $this->engine = new Engine();
    }
    
    public function start(): string {
        return $this->engine->start();
    }
}

$car = new Car();
echo $car->start();

出力:

Engine started

CarクラスはEngineから継承しているのではなく、それを所有しています。このアプローチには大きな利点があります。エンジンを別の実装に交換でき、クラス間の結合は疎な状態に保たれ、深い継承が作り出す硬直した階層構造を避けることができます。

OOPにおける一般的なガイドラインは、「継承よりもコンポジションを優先する」ことです。継承は、真の型階層(Dogは真にAnimalである、といったもの)に対してはうまく機能しますが、独立したパーツから機能を組み立てる場合にはコンポジションが威力を発揮します。複数のオブジェクトを組み合わせたり、実行時にそれらを入れ替えたり、コンポーネントを個別にテストしたりすることができます。

次のレッスンでは、オブジェクトが依存関係を受け取る方法を制御することで、コンポジションを直接のベースとした依存性の注入(dependency injection)について探求します。

challenge icon

チャレンジ

簡単

コンポジションを使用してコンピュータシステムを構築しましょう。ここでは、Computer クラスが他のクラスを継承するのではなく、個別のコンポーネントオブジェクトを組み合わせて構成されます。これにより、コンポジションを使用して、より単純で独立したパーツから複雑なオブジェクトを構築する方法を学びます。

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

  • Processor.php — CPUを表す Processor クラスを作成します。コンストラクタのプロパティ昇格(constructor promotion)を使用して、privateな $model (string) と privateな $cores (integer) を定義します。"[model] ([cores] cores)" を返す getSpecs() メソッドを追加してください。
  • Memory.php — RAMを表す Memory クラスを作成します。コンストラクタのプロパティ昇格を使用して、privateな $size (GBを表すinteger) を定義します。"[size]GB RAM" を返す getCapacity() メソッドを追加してください。
  • main.php — 両方のコンポーネントファイルをインクルードし、コンポジションを使用する Computer クラスを作成します。Computer クラスは以下の要件を満たす必要があります:
    • Processor オブジェクトと Memory オブジェクトを private プロパティとして保持する
    • コンストラクタでプロセッサのモデル、コア数、メモリサイズを受け取る
    • コンストラクタ内で内部的にコンポーネントオブジェクトを生成する
    • "Computer with [processor specs] and [memory capacity]" を返す describe() メソッドを持つ

    3つの入力(プロセッサのモデル、コア数、GB単位のメモリサイズ)を受け取ります。これらの仕様で Computer を作成し、describe() を呼び出した結果を出力してください。

ComputerProcessorMemory を継承(extend)しているのではなく、それらを「保持(contains)」していることに注目してください。コンポジションによるこの「has-a」関係は、各コンポーネントの独立性と再利用性を保ちながら、Computer が必要に応じてそのパーツに処理を委譲することを可能にします。

チートシート

コンポジションは、単純なオブジェクトを継承するのではなく、プロパティとして保持することで、それらから複雑なオブジェクトを構築します。これにより、あるクラスが他のクラスのインスタンスを含む「has-a」関係が作成されます。

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

<?php
class Engine {
    public function start(): string {
        return "Engine started";
    }
}

class Car {
    private Engine $engine;
    
    public function __construct() {
        $this->engine = new Engine();
    }
    
    public function start(): string {
        return $this->engine->start();
    }
}

CarクラスはEngineから継承しているのではなく、それを所有しています。これにより、クラス間の結合が緩やかになり、硬直した継承階層を避けることができます。

継承よりもコンポジションを優先する: 継承は真の型階層(DogAnimalであるなど)には適していますが、独立したパーツから機能を組み立てる場合にはコンポジションが威力を発揮します。複数のオブジェクトを組み合わせたり、実行時にそれらを入れ替えたり、コンポーネントを個別にテストしたりすることが可能です。

自分で試してみよう

<?php
require_once 'Processor.php';
require_once 'Memory.php';

class Computer
{
    // TODO: ProcessorとMemoryオブジェクトを格納するためのプライベートプロパティを定義する

    // TODO: コンストラクタはプロセッサのモデル、コア数、メモリサイズを受け取り、
    // 内部でコンポーネントオブジェクトを作成する必要がある
    public function __construct()
    {
        // TODO: ProcessorとMemoryオブジェクトを作成して格納する
    }

    // TODO: "Computer with [processor specs] and [memory capacity]" を返す
    // describe() メソッドを実装する
    public function describe(): string
    {
        // TODO: コンポーネントオブジェクトに処理を委譲し、フォーマットされた文字列を返す
    }
}

// 入力を読み込む
$processorModel = trim(fgets(STDIN));
$cores = intval(fgets(STDIN));
$memorySize = intval(fgets(STDIN));

// TODO: 指定された仕様でComputerを作成し、describe()の結果を出力する

?>
quiz icon腕試し

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

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