#03 オブジェクト指向プログラミング入門

継承——コードを再利用して拡張する

継承とは何か

プログラムを書いていると、似たようなクラスをいくつも作ることがあります。

class Dog
{
    private string $name;

    public function __construct(string $name)
    {
        $this->name = $name;
    }

    public function getName(): string { return $this->name; }
    public function eat(): string { return "{$this->name}がご飯を食べた"; }
    public function sleep(): string { return "{$this->name}が眠った"; }
    public function bark(): string { return "ワンワン!"; }
}

class Cat
{
    private string $name;

    public function __construct(string $name)
    {
        $this->name = $name;
    }

    public function getName(): string { return $this->name; }
    public function eat(): string { return "{$this->name}がご飯を食べた"; }
    public function sleep(): string { return "{$this->name}が眠った"; }
    public function meow(): string { return "ニャーニャー!"; }
}

eat()sleep() が完全に重複しています。これは DRY(Don’t Repeat Yourself)原則に反します。継承を使えば、共通部分をまとめられます。


extendsキーワードで継承する

// 親クラス(スーパークラス)
class Animal
{
    protected string $name;

    public function __construct(string $name)
    {
        $this->name = $name;
    }

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

    public function eat(): string
    {
        return "{$this->name}がご飯を食べた";
    }

    public function sleep(): string
    {
        return "{$this->name}が眠った";
    }
}

// 子クラス(サブクラス)
class Dog extends Animal
{
    public function bark(): string
    {
        return "ワンワン!";
    }
}

class Cat extends Animal
{
    public function meow(): string
    {
        return "ニャーニャー!";
    }
}

extends キーワードで親クラスを継承します。子クラスは親クラスのプロパティとメソッドをすべて引き継ぎます。

$dog = new Dog('ポチ');
echo $dog->eat();   // ポチがご飯を食べた(親クラスのメソッド)
echo $dog->bark();  // ワンワン!(子クラス固有のメソッド)

$cat = new Cat('タマ');
echo $cat->sleep(); // タマが眠った(親クラスのメソッド)
echo $cat->meow();  // ニャーニャー!(子クラス固有のメソッド)

parent:: で親クラスを呼び出す

子クラスで独自のコンストラクタを持ちつつ、親のコンストラクタも実行したいことがあります。

class Animal
{
    protected string $name;
    protected int $age;

    public function __construct(string $name, int $age)
    {
        $this->name = $name;
        $this->age  = $age;
    }

    public function introduce(): string
    {
        return "{$this->name}({$this->age}歳)";
    }
}

class Dog extends Animal
{
    private string $breed; // 犬種

    public function __construct(string $name, int $age, string $breed)
    {
        parent::__construct($name, $age); // 親のコンストラクタを呼ぶ
        $this->breed = $breed;
    }

    public function introduce(): string
    {
        // 親メソッドの結果を流用して拡張する
        return parent::introduce() . "、{$this->breed}です。";
    }
}

$dog = new Dog('ポチ', 3, 'シバイヌ');
echo $dog->introduce(); // ポチ(3歳)、シバイヌです。

parent::メソッド名() で親クラスのメソッドを呼び出せます。コンストラクタを省略すると初期化漏れになるため、子クラスでコンストラクタを定義するときは必ず parent::__construct() を呼びましょう。


メソッドのオーバーライド

子クラスで親クラスと同名のメソッドを定義すると、子クラスのメソッドが優先されます。これをオーバーライドと呼びます。

class Animal
{
    public function sound(): string
    {
        return '...'; // 汎用的な鳴き声(あとで上書きする想定)
    }
}

class Dog extends Animal
{
    public function sound(): string // オーバーライド
    {
        return 'ワンワン!';
    }
}

class Cat extends Animal
{
    public function sound(): string // オーバーライド
    {
        return 'ニャーニャー!';
    }
}

$animals = [new Dog('ポチ'), new Cat('タマ'), new Animal()];
foreach ($animals as $animal) {
    echo $animal->sound() . "\n";
}
// ワンワン!
// ニャーニャー!
// ...

継承の「is-a」関係

継承は「AはBの一種だ(A is a B)」という関係が成り立つときに使います。

親クラス子クラスis-a関係
AnimalDogDog is an Animal ✓
VehicleCarCar is a Vehicle ✓
UserAdminUserAdminUser is a User ✓

逆に「has-a(〜を持つ)」関係は継承ではなくコンポジション(別オブジェクトをプロパティに持つ)で表現します。

// NG: Car has a Engine → 継承は不適切
class Car extends Engine { ... }

// OK: Car has a Engine → コンポジション
class Car
{
    public function __construct(
        private Engine $engine
    ) {}
}

final——継承・オーバーライドを禁止する

意図しない継承やオーバーライドを防ぎたいときは final を使います。

class Payment
{
    // このメソッドはオーバーライドさせたくない
    final public function processLog(): void
    {
        // ログ記録ロジック(変えられると困る)
    }
}

// final class にすれば継承そのものを禁止できる
final class Singleton
{
    // ...
}

継承の使いすぎに注意

継承は便利ですが、深い階層になるとコードが読みにくくなります。

// 深い継承階層はNG
Animal → Mammal → Pet → Dog → PoliceDog → TrainedPoliceDog

「3階層以上になりそうなら継承ではなくインターフェースやトレイトを検討する」が良い目安です(トレイトは第8回で解説します)。

また、継承はクラス間に強い結合を生みます。親クラスを変更すると子クラス全体に影響が出るため、変更の多い部分には慎重に使いましょう。


まとめ

  • extends で親クラスを継承し、共通コードを再利用できる
  • parent:: で親クラスのメソッドやコンストラクタを呼び出す
  • 同名メソッドを定義するとオーバーライドになる
  • 継承は「is-a」関係のときだけ使う
  • final で継承・オーバーライドを禁止できる
  • 深すぎる継承階層は避ける

次回はポリモーフィズム——同じメソッド名で違う動作をさせる仕組みを学びます。継承とセットで理解すると、OOPの力が一気に実感できます。