🤖

触って学べる図解:extendsとTraitの違いをロボット工場で例えてみた

に公開

はじめに

こんにちは!

オブジェクト指向を勉強していると、必ず出てくる extends(継承)Trait(トレイト)

「なんとなくわかるけど、どう使い分けるの?」って思いませんか?

今回は、ロボット工場に例えながら、クリックして遊べるインタラクティブな図解で、この2つの違いをざっくり理解してみましょう!

まずは触ってみよう!

百聞は一見にしかず。まずは下のカードをクリックしてみてください👇

extends(継承)とは?

extends は、親クラスの設計図を丸ごと引き継いで、新しいクラスを作る仕組みです。

class Robot {
    public function move(): void
    {
        echo "歩く";
    }
}

// BasicRobotはRobotの機能を全部引き継ぐ
class BasicRobot extends Robot {
    public function greet(): void
    {
        echo "こんにちは!";
    }
}

$robot = new BasicRobot();
$robot->move();  // 親から引き継いだ "歩く"
$robot->greet(); // 自分の "こんにちは!"

extendsのポイント

  • ✅ 親の機能を丸ごと引き継げる
  • ✅ コードの再利用がしやすい
  • 親は1つだけ(PHPは単一継承)
  • ❌ 「車」と「ロボット」を同時に親にできない

Trait(トレイト)とは?

Trait は、クラスに特定の機能だけをあとから追加できる仕組みです。
まるでロボットにオプションパーツを後付けするイメージです!

// 「飛ぶ」機能のTrait
trait CanFly {
    public function fly(): void
    {
        echo "飛んだ!";
    }
}

// 「炎を出す」機能のTrait
trait CanFlame {
    public function flame(): void
    {
        echo "炎!";
    }
}

// 複数のTraitを同時に使える!
class SuperRobot {
    use CanFly, CanFlame;
}

$robot = new SuperRobot();
$robot->fly();   // "飛んだ!"
$robot->flame(); // "炎!"

Traitのポイント

  • 複数のTraitを同時に使える
  • ✅ 欲しい機能だけをポン付けできる
  • ✅ 異なるクラスに同じ機能を共有できる
  • ❌ 単体でインスタンス化はできない(あくまで「パーツ」)

まとめ:使い分けの基準

extends Trait
イメージ 設計図の丸コピー オプションパーツの後付け
使える数 1つだけ いくつでも
向いている場面 「〇〇の一種」という関係 「〇〇できる」という機能追加
Dog extends Animal use CanSwim, CanRun

extendsとTraitの違い図解

Traitをもっと活かす使い方

さっきの例を振り返ってみましょう。

trait CanFly {
    public function fly(): void
    {
        echo "飛んだ!";
    }
}

「飛んだ!」と表示するだけで、どのロボットが飛んでも同じ結果——こういう処理は、staticクラスでも書けますよね。

staticメソッドとはnew でオブジェクトを組み立てなくても クラス名::メソッド名() の形で直接呼び出せるメソッドのことです(new でオブジェクトを作ることを「インスタンス化」と呼びます)。

// staticクラスで書けるケース
class FlyHelper {
    public static function fly(): void
    {
        echo "飛んだ!";
    }
}

FlyHelper::fly(); // newしなくても呼べる

staticクラスはシンプルでテストもしやすいので、うちのプロジェクトでは「$this(自分自身)を使わないTraitは作らない。staticメソッドを持ったクラスを利用する」という方針をとっています。

では、Traitが本当に活きるのはどんなときかというと、インターフェースとセットで使うときだと思っています。

こちらも図解で触って確認してみましょう👇

インターフェース+Traitの組み合わせ

インターフェースとは、「このクラスは必ずこのメソッドを持つ」という約束書のようなものです。サインしたクラスは必ずその約束を守らなければなりません。

ロボット工場では、こんな2種類の約束書があるとします。

約束書①「型番管理できる」(Modelable)
・getModel() で自分の型番を返せる

約束書②「飛べる」(Flyable)
・fly() で飛べる
// 約束書①:型番を持つ
interface Modelable {
    public function getModel(): string;
}

// 約束書②:飛べる
interface Flyable {
    public function fly(): void;
}

ガンダム型ロボットと偵察ドローンは、両方の約束書にサインします。

class FlyingRobot implements Modelable, Flyable {
    // getModel() と fly() を必ず実装しないといけない
}

class FlyingDrone implements Modelable, Flyable {
    // こちらも同様
}

ここで考えてみましょう。getModel() の中身ってどのクラスでもほぼ同じですよね。

// FlyingRobotのgetModel
public function getModel(): string
{
    return $this->modelId; // 自分のmodelIdを返すだけ
}

// FlyingDroneのgetModel
public function getModel(): string
{
    return $this->modelId; // ここも全く同じ!
}

毎回同じコードを書くのは無駄です。でも fly() はロボットとドローンで飛び方が異なるため、各クラスで個別に実装するのは自然なことです。

そこでTraitの出番です。「どのクラスでも実装が同じになる部分」だけをTraitで共通化します。

// 約束書①:型番を持つ
interface Modelable {
    public function getModel(): string;
}

// 約束書②:飛べる
interface Flyable {
    public function fly(): void;
}

// Trait:getModel()のデフォルト実装を提供
// どのクラスでも「return $this->modelId」と書くだけなので共通化できる
trait HasModel {
    public function getModel(): string
    {
        return $this->modelId; // $thisで自分自身のmodelIdにアクセス
    }
}

// ガンダム型ロボット
class FlyingRobot implements Modelable, Flyable {
    use HasModel; // getModel()はTraitにお任せ!

    public function __construct(private string $modelId) {}

    // fly()はロボット独自の実装(ブースターで飛ぶ)
    public function fly(): void
    {
        echo $this->getModel() . "がブースターで飛んだ!";
    }
}

// 偵察ドローン
class FlyingDrone implements Modelable, Flyable {
    use HasModel; // getModel()は同じTraitにお任せ!

    public function __construct(private string $modelId) {}

    // fly()はドローン独自の実装(プロペラで飛ぶ)
    public function fly(): void
    {
        echo $this->getModel() . "がプロペラで飛んだ!";
    }
}

$gundam = new FlyingRobot("ガンダム型");
$drone  = new FlyingDrone("偵察ドローン型");

$gundam->fly();      // "ガンダム型がブースターで飛んだ!"
$drone->fly();       // "偵察ドローン型がプロペラで飛んだ!"

$gundam->getModel(); // "ガンダム型"      ← Traitが提供
$drone->getModel();  // "偵察ドローン型" ← 同じTraitが提供

ポイントは fly()getModel() の役割の違いです。

  • fly():ロボットとドローンで飛び方が違う → 各クラスで個別に実装
  • getModel():どのクラスでもほぼ同じ実装になる → Traitで共通化

$this->modelId という形で自分自身のデータにアクセスできるので、同じTraitを使っても「ガンダム型」「偵察ドローン型」とそれぞれの型番を返せます。

おわりに

extendsTrait はどちらも「コードを再利用する仕組み」ですが、目的が違います。

  • extends親子関係を表す
  • Trait機能の追加に使う

この違いを意識するだけで、クラス設計がぐっとスッキリしますよ!

最初のインタラクティブ図解でも確認できるので、ぜひ何度でも触って感覚をつかんでみてください🤖

ソーシャルデータバンク テックブログ

Discussion