25.

PHP の継承|extends・parent・interface・trait の使い分け

編集
この記事の要点
  • 継承は class 子 extends 親親は 1 つだけ(多重継承はできない)
  • 親の実装を呼ぶのは parent::method()。コンストラクタも自分で呼ぶ
  • private は子から見えない。子にも見せたいなら protected にする
  • 「複数の型として扱いたい」なら implements、「実装を配りたい」なら use(トレイト)
  • 継承は「〜は〜の一種」が成り立つときだけ。再利用目的の継承は委譲にする

基本

class Animal
{
    public function __construct(protected string $name) {}

    public function speak(): string
    {
        return "{$this->name} が鳴いた";
    }

    public function name(): string
    {
        return $this->name;
    }
}

class Dog extends Animal
{
    public function speak(): string      // 上書き(オーバーライド)
    {
        return "{$this->name}: ワン";
    }

    public function fetch(): string      // 子だけの機能
    {
        return "取ってきた";
    }
}

$d = new Dog('ポチ');
echo $d->speak();     // ポチ: ワン
echo $d->name();      // ポチ      親のメソッドも使える
echo $d->fetch();     // 取ってきた

var_dump($d instanceof Dog);      // true
var_dump($d instanceof Animal);   // true   親としても扱える

親の実装を呼ぶ

class Cat extends Animal
{
    public function __construct(string $name, private bool $indoor = true)
    {
        parent::__construct($name);      // ← 忘れると $name が未初期化になる
    }

    public function speak(): string
    {
        return parent::speak() . '(ニャー)';   // 親の結果に足す
    }
}

echo (new Cat('タマ'))->speak();   // タマ が鳴いた(ニャー)

子クラスで __construct定義しなければ親のものがそのまま使われます。定義した時点で親の初期化は行われなくなるので、parent::__construct() の呼び忘れに注意してください。

可視性(private / protected / public)

class Base
{
    public    string $a = 'public';      // どこからでも
    protected string $b = 'protected';   // 自分と子クラスから
    private   string $c = 'private';     // 自分のクラスの中だけ

    public function show(): void
    {
        echo $this->a, $this->b, $this->c;   // 全部見える
    }
}

class Child extends Base
{
    public function test(): void
    {
        echo $this->a;   // OK
        echo $this->b;   // OK
        echo $this->c;   // Warning: Undefined property Child::$c
    }
}

$o = new Child();
echo $o->a;             // OK
// echo $o->b;          // Error: Cannot access protected property
クラス内子クラス外部
public
protected×
private××

継承される前提のクラスでは、子に触ってほしいものを protected、内部実装を private にします。迷ったら狭いほうから始めて、必要になったら広げてください。

final で継承を止める

final class Money { }              // このクラスは継承できない
// class Yen extends Money {}      // Fatal error: Class Yen cannot extend final class Money

class Service
{
    final public function run(): void  // このメソッドは上書きできない
    {
        $this->before();
        $this->handle();
        $this->after();
    }

    protected function handle(): void {}   // ここだけ子に差し替えさせる
    private function before(): void {}
    private function after(): void {}
}

処理の流れは固定して一部だけ差し替えさせたいときに final が役立ちます(テンプレートメソッドパターン)。

インターフェース

interface Speaker
{
    public function speak(): string;      // 実装は書かない
}

interface Walker
{
    public function walk(): string;
}

class Robot implements Speaker, Walker    // 複数実装できる
{
    public function speak(): string { return 'ピー'; }
    public function walk(): string  { return 'ガシャン'; }
}

function announce(Speaker $s): void       // 型として使える
{
    echo $s->speak();
}

announce(new Robot());
announce(new Dog('ポチ'));   // Dog も implements Speaker していれば渡せる

親クラスは 1 つしか持てませんが、インターフェースはいくつでも実装できます。「これができる」という役割を型で表したいときはインターフェースを使います。

トレイト(実装の使い回し)

trait Timestampable
{
    private ?string $createdAt = null;

    public function touch(): void
    {
        $this->createdAt = date('Y-m-d H:i:s');
    }
}

trait Loggable
{
    public function log(string $msg): void
    {
        error_log(static::class . ': ' . $msg);
    }
}

class Article
{
    use Timestampable, Loggable;      // 複数まとめて取り込める
}

$a = new Article();
$a->touch();
$a->log('作成');

トレイトは継承関係とは無関係にメソッドを配る仕組みです。同じ名前のメソッドが衝突したときは insteadofas で解決します。

trait A { public function hello(): string { return 'A'; } }
trait B { public function hello(): string { return 'B'; } }

class C
{
    use A, B {
        A::hello insteadof B;      // A のほうを採用
        B::hello as helloB;        // B のほうは別名で使う
    }
}

echo (new C())->hello();    // A
echo (new C())->helloB();   // B

3 つの使い分け

目的実装を持てるか
extends(継承)「〜は〜の一種」を表す1 つだけ持てる
implements(インターフェース)「〜ができる」を型で表すいくつでも持てない(定数と宣言のみ)
use(トレイト)実装を配るいくつでも持てる(型としては使えない)

継承より委譲を選ぶ場面

// 良くない継承: 「Stack は ArrayObject の一種」ではない
class Stack extends ArrayObject
{
    public function push($v): void { $this[] = $v; }
}
$s = new Stack();
$s->push(1);
$s->asort();        // 親の余計な機能まで公開されてしまう

// 委譲: 必要な操作だけを見せる
class Stack
{
    private array $items = [];

    public function push(mixed $v): void { $this->items[] = $v; }
    public function pop(): mixed         { return array_pop($this->items); }
    public function count(): int         { return count($this->items); }
}

継承すると親の public メソッドがすべて子でも使えるようになります。「便利だから継承する」と、意図しない使われ方を許すことになります。「子は親としても正しく振る舞えるか」を基準に判断してください(ポリモーフィズム(多様性)の具体例)。

関連

編集
Post Share
子ページ

子ページはありません

同階層のページ
  1. 基本事項
  2. HTMLへの埋め込み
  3. 変数
  4. 可変変数
  5. 定数
  6. データ型
  7. キャスト
  8. エスケープ文字
  9. 配列
  10. 演算子
  11. 代入の際の注意点
  12. 条件分岐
  13. 繰り返し処理
  14. クラスとインスタンス
  15. コンストラクタ
  16. 関数
  17. スーパーグローバル変数
  18. スコープ
  19. staticについて
  20. yieldについて
  21. ファイルのアップロード方法
  22. DB接続方法
  23. SQL実行方法
  24. カプセル化の具体例
  25. 継承の構文
  26. オーバーライド
  27. ポリモーフィズム(多様性)の具体例
  28. 抽象クラス・メソッドの構文と具体例
  29. GET通信
  30. try catchで全てのエラーを拾う方法

最近更新/作成されたページ