PHP入門 オブジェクト指向 継承

継承(Inheritance)とは、基になるクラスの機能(メソッド)を引き継ぎながら、新たな機能を追加したり、元の機能一部だけを修正したりする仕組みです。このとき、継承元となるクラスのことをスーパークラス(親クラス、基底クラス)、継承の結果できたクラスのことをサブクラス(子クラス、派生クラス)と呼びます。

継承の基本

継承するには、クラス定義の際にextendsキーワードでスーパークラスを指定します。

構文:継承(extendsキーワード)

class サブクラス名 extends スーパークラス名{
  ・・・プロパティ/メソッドの定義
}

例:Personクラスを継承してBusinessPersonクラスを定義

Person.php

<?php
class Person{
    public string $firstName;
    public string $lastName;

    public function __construct(string $firstName, string $lastName){
        $this->firstName = $firstName;
        $this->lastName = $lastName;
    }

    public function show():void{
        print "<p>私の名前は{$this->lastName}{$this->firstName}です</p>";
    }
}

BusinessPerson.php

<?php
//継承元のクラスファイルをインポート
require_once 'Person.php';

class BusinessPerson extends Person{
    //サブクラス独自のworkメソッドを定義
    public function work():void{
        print "<p>{$this->lastName}{$this->firstName}は働いています。</p>";
    }
}

呼び出し方

<?php
require_once 'BusinessPerson.php';
$p1 = new BusinessPerson('太郎','山田');
$p1->work();//➊
$p1->show();//➋
実行結果
実行結果

BusinessPersonクラスで定義されたworkメソッドを呼び出せていることはもちろん(❶)、もともとのPersonクラスで定義されたコンストラクターやshowメソッドが、あたかもBusinessPersonクラスのメンバーであるかのように呼びだれていることがわかります(❷)。このように、継承の世界では、まず現在のクラスで要求されたメソッドを探し、存在しなかった場合には、親クラスで定義されたメソッドを探しにいきます。

多重継承

PHPでは、次のような多重継承は認めていません。つまり、さるサブクラスのスーパークラスは常に1つだけです。ただし、あるサブクラスをさらに継承して、異なるサブクラスを定義するのはかまいません。

class BusinessPerson extends Person, Animal{

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

継承を利用することで、スーパークラスで定義されたメソッドをサブクラスで上書きすることもできます。これをメソッドのオーバーライドと言います。オーバーライドとは、スーパークラスで定義された機能をサブクラスで再定義することです。

例:BusinessPersonクラスを継承して、EliteBusinessPersonクラスを定義しworkメソッドをオーバーライドします。

EliteBusinessPerson.php

<?php
require_once 'BusinessPerson.php';

class EliteBusinessPerson extends BusinessPerson{
    public function work(): void
    {
        print "<p>{$this->lastName}{$this->firstName}は売上貢献主義です。";
    }
}

呼び出し方

<?php
require_once 'EliteBusinessPerson.php';
$p1 = new EliteBusinessPerson('太郎','山田');
$p1->work();
$p1->show();
実行結果
実行結果

workメソッドはEliteBusinessPersonクラスで上書きされているので、結果もEliteBusinessPersonクラスのworkメソッドが実行されます。

オーバーライドの条件

PHP7.4では、メソッドをオーバーライドする際の引数/戻り値型の条件がより緩和されました。

<?php

//親子関係にあるMyParent/MyChildクラスを定義
class MyParent {}
class MyChild extends MyParent

//親子関係にあるMyMain/MySubクラスを定義
class MyMain{
    public function work(MyChild $p):MyParent{・・・}

}

class MySub  extends MyMain{
    public function work(MyParent $p):MyChild{・・・}
    
}

継承関係にあるMyParent/MyChild、MyMain/MySubと、4個のクラスが定義されており、MyMain::workメソッドがMySubクラスでオーバーライドされているコードです。

1、サブクラスの戻り値はより狭い型を許容する

戻り値では、MyMain::workメソッドがMyParent型を、MySub::workがMyChild型を返しています。is-a関係のルールからすれば、MyChildはMyParentなので、戻り値の型には互換性があることになります。その逆は不可です。スーパークラスがMyChild型を返すのに、サブクラスがMyParent型を返すようなオーバーライドです。MyParentは必ずしもMyChildであるとは限りません。

2、サブクラスの引数はより広い型を許容する

一方、引数の性質は逆です。MyMain::workメソッドがMyChild型を受け取るのに対して、MySub::workがMyParent型を受け取っています。MyChildはMyParentなので、MyChildを受け取るべき引数にMyParentを渡しても、常に動作するはずです。当然、その逆スーパークラスがMyParent型を受け取るのに、サブクラスがMyChild型を受け取るようなオーバーライドは不可です。

なお、このような引数の性質を反変性(contravariant)、戻り値の性質のことを共通性(convariant)と呼びます。

オーバーライドの条件

項目概要
メソッド名一致していること
仮引数型は一致しているか、より広い型であること。個数は一致していること(名前、既定値はことなっていてもかまわない)
戻り値型一致しているか、より狭い型であること
アクセス修飾子一致しているか、より緩いこと(親がprotectedであれば、子のpublicは可能)

ただし、privateメソッドをサブクラスで再定義した場合、引数/戻り値などの型も一切チェックされません(privateメソッドは継承もされないので、オーバーライドの判定そのものに意味がないからです)

スーパークラスのメソッドを呼び出す(parentキーワード)

オーバーライドは、スーパークラスの機能を完全に書き換えるものばかりではありません。スーパークラスの機能を流用しつつ、サブクラス側で独自の機能を追加する場合もあります。このような場合には、サブクラスのメソッドからスーパークラスのメソッドを明示的に呼び出す必要があります。

<?php
require_once 'BusinessPerson.php';
class HeetareBusinessPerson extends BusinessPerson{
    //BusinessPersonクラスのworkメソッドをオーバーライド
    public function work():void{
        //スーパークラスのworkメソッド
        parent::work();
        //独自の処理
        print '社内CEO';
    }
}

呼び出し方

<?php
require_once 'HetareBusinessPerson.php';
$p1 = new HetareBusinessPerson('太郎','山田');
$p1->work();
$p1->show();

HetareBusinessPersonクラス独自の機能が追加されていることが確認できます。

実行結果
実行結果

スーパークラスを呼び出すのは、parentキーワードの役割です。

構文:parentキーワード

parent::メソッド名(引数,・・・)

parentキーワードによって、現在クラスの親クラスのメソッドを呼び出すことができます。parentキーワードとメソッド名とをつなぐのは->演算子ではなく::演算子です。

メソッド内では、次のように親クラスや現在クラスをインスタンス化することができます。

$t = new parent('太郎','山田'); //親クラス
$t = new self('太郎','山田');   //現在のクラス

スーパークラスのコンストラクターを呼び出す(parentキーワード)

スーパークラスのコンストラクターを流用しながら、サブクラスのコンストラクターを定義することもできます。

<?php
require_once 'Person.php';

class Foreigner extends Person{

    //新たに追加したmiddleNmaeプロパティ
    public string $middleName;

    public function __construct(string $firstName, string $middleName, string $lastName){
        //スーパークラスのコンストラクターを呼び出し
        parent::__construct($firstName, $lastName);
        //独自のmiddleNameプロパティを初期化
        $this->middleName = $middleName;
    }
    //middleNameプロパティ対応にshowメソッドもオーバーライド
    public function show():void{
        print "<p>私の名前は{$this->firstName}.{$this->middleName}.{$this->lastName}です。</p>";
    }
}

呼び出し方

<?php
require_once 'Foreigner.php';
$p1 = new Foreigner('太郎','ノーダ','山田');
$p1->show();
実行結果
実行結果

オーバーライドの禁止(final修飾子)

継承/オーバーライドを禁止するのは、final修飾子の役割です。

public final function work():void{

final修飾子はクラスレベルで定義することもできます。

final class BusinessPerson extends Person{

委譲

継承は、PHPにおけるコード再利用の代表的なアプローチですが、唯一のアプローチではありませんし、常に最良の手段というわけでもありません。むしろ継承は利用すべき状況は相応に限られています。継承とは、スーパークラスとサブクラスとが密に結びついた関係です。サブクラスはスーパークラスの実装に依存しますし、である以上、内部的な構造を意識しなければなりません。スーパークラスでの実装修正によって、サブクラスうが動作しなくなることもあるでしょう。影響の範囲は、スーパークラスが上位になればなるほど、継承構造が複雑になるほど広がり、修正コストも高まります。継承を利用するのは、スーパークラスとサブクラスとがis-a関係を満たしている場合、かつ、ライブラリをまたがって継承するならば、そのクラスが「拡張を前提としており、その旨が文書化されている」場合に限定すべきでしょう。

継承が不適切な例

is-a関係を確認するための代表的なアプローチとして、リスコフの置換原則があげられます。リスコフの置換原則とは、サブクラスのインスタンスは、常にスーパークラスのそれと置き換え可能であることです。この原則に照らすと、たとえば、次の例のようなFileLoggerクラスは不当です。FileLoggerクラスは、標準のSplFileObjectクラスをもとにロギング機能を定義しています。

<php
class FileLogger extends SplFileObject{
  public DateTime $current;

  //指定された名前ログファイルをオープン
  public function __construct(string $logname){
    $this->current = new DateTime();
    parent::__construct("{$logname}-{$this->current->format('Ymd')}.log",'a');
  }
  //SplFileObject::fwriteメソッドをオーバーライド
  public function fwrite(string $str, int $length = 0): int{
    return parent::fwrite("[{$this->current->format('Y/m/d')}]{$str}\n");
  }
  //その他の不要メソッドは無効化
  public function fread(int $length): string|false {
    throw new Exception('Method is not supported.');
  }

  ・・・中略・・・

}
$logger = new FileLogger('log');
$logger->fwrite('Test Text');

FileLoggerクラスで利用しているのは、ログファイルを開くためのコンストラクター、ログ記録のためのfwriteメソッドです。そして、その他のメソッドはFileLoggerとしては不要なので、例外をスローして、疑似的に無効化しています。これがリスコフの置換原則に反します。

//SplFileObjectとしてFileLoggerを操作してみる
$splFile = new FIleLogger('log');
$splFile -> fwrite('Test Text');
$splFile -> fread(10);

FileLoggerクラスがSplFileObjectとしては動作しないのです。このような継承関係は、一般的に妥当ではありません。

委譲による解説

このような状況を解決するのが委譲です。委譲とは再利用したい機能を持つオブジェクトを、現在のクラスのプロパティとして取り込みます。

委譲
委譲

このような関係をhas-a関係と呼びます。fileプロパティにSplFileObjectオブジェクトを保持(has)し、必要に応じて、そこから既成のメソッドを利用させてもらうわけです。他のインスタンスに処理を委ねる、委譲と呼ばれる理由です。

<?php
class FileLogger{
    private DateTime $current;
    //委譲先のオブジェクトをプロパティに保持
    private SqlFileObject $file;

    public function __construct(string $logname){
        $this->current = new DateTime();
        $this->file = new SqlFileObject("{$logname}-{$this->current->format('Ymd')}.log",'a');
    }

    //必要に応じて処理を委譲
    public function fwrite(string $str){
        $this->file-fwrite(" [{$this->current->format('Y/m/d')}] {$str}\n ");
    }

}
$logger = new FileLogger('log');
$logger->fwrite('Test Text');

委譲の良い点は、クラス同士の関係が緩まる点です。再利用しているのがプロパティなので、委譲先の内部的な実装に左右される心配はありません。また、クラス同士の関係が固定されません。委譲先を変更するもの自由ですし、複数のクラスに処理を委ねることも、インスタンス単位に委譲先を切り替えることすら可能です。継承がクラス同士の静的な関係とするなら、委譲とはインスタンス同士の動的な関係となります。

遅延静的束縛

遅延静的束縛(Late Static Bindings)は、静的継承のコンテキストで呼び出し元のクラスを参照するための仕組みです。

<?php
class MyParent{
    //現在のクラス名を表示
    public static function show(){    //➊
        print __CLASS__;
    }
    //showメソッド経由で現在のクラス名を表示
    public static function staticTest(){  //➌
        self::show();
    }
}

class MyChild extends MyParent{    //➋
    //現在のクラス名を表示
    public static function show(){
        print __CLASS__;
    }
}

MyChild::staticTest();   //➍

静的メソッドshow(❶❷)は、いずれも現在のクラス名を表示するためのメソッド。staticTestメソッド(➌)は、そのshowメソッドを利用して、やはり現在のクラス名を表示するためのメソッドです。staticTestメソッドは、MyParentクラス(親)でだけ定義されていますが、MyChildクラスはMyParentを継承しているので、staticTestメソッドはMyChildクラスでも利用できます。では、MyChild経由でstaticTestメソッドを呼び出した場合(❹)、❶❷いずれのshowメソッドを呼び出すのでしょうか。「self::show();」とあるので、一見すると、MyChild::showメソッドが呼び出され、「MyChild」という結果が得られるようにも思えます。しかし、❹の結果は「MyParent」。MyParent::showメソッドが呼び出されています。「self::」による参照は定義時に解決され、MyParentとして解釈されるわけです。では、MyChild::showメソッドを呼び出すには、どうするのか。このときに利用するのがstaticキーワードです。太字を「static」で置き換えてみましょう。今度は、❹の結果は「MyChild」となります。staticキーワードは実行時に直近にコールされたクラスを参照します。staticによって、あとで遅延して評価され、主に静的メソッドで利用される遅延静的束縛と呼ばれる理由です。