PHP入門 オブジェクト指向 ポリモーフィズム

ポリモーフィズムについて解説しています。

ポリモーフィズムとは

ポリモーフィズムとは、同名のメソッドで異なる挙動を実現すること言います。

Figure.php

<?php
class Figure{
    //プロパティを定義(protected修飾子で制限)
    protected float $width;
    protected float $height;
    //コンストラクター(プロパティを初期化)
    public function __construct(float $width, float $height){
        $this->width =$width;
        $this->height = $height;
    }

    //面積を求める
    public function getArea(): float{
        return 0;
    }

}

Triangle.php

<?php
require_once 'Figure.php';

class Triangle extends Figure{
    //三角形の面積を求めるためのgetAreaメソッドを定義
    public function getArea():float {
        return $this->width * $this->height /2;
    }
}

Square.php

<?php
require_once 'Figure.php';

class Square extends Figure{
    //四角形の面積を求めるためのgetAreaメソッドを定義
    public function getArea(): float {
        return $this->width * $this->height;
    }
}

上記のクラスを呼び出した場合

<?php
require_once 'Triangle.php';
require_once 'Square.php';

$tri = new Triangle(5,10);
$squ = new Square(5,10);
print "三角形の面積:{$tri->getArea()}<br />";
print "四角形の面積:{$squ->getArea()}<br />";
実行結果
実行結果

getAreaメソッドは、図形の面積を求めるメソッドです。この例では、Figureクラスを継承する2つのサブクラスTriangleとSquareで同名のgetAreaメソッドをオーバーライドし、それぞれ三角形と四角形の面積を求めるようにしています。複数のクラスで同じ名前のメソッドを定義しています。これがポリモーフィズムです。ポリモーフィズムのメリットは、同じ目的の機能を呼び出すため異なる名前にしなくてよいというところです。

抽象メソッド

抽象メソッドとは、それ自体が中身(機能)を持たない「空メソッド」のことです。機能を持たないということは、サブクラスで外から提供してやらなければなりません。抽象メソッドとは、サブクラスでオーバーライドされることを「強制したメソッド」です。抽象メソッドを含んだクラスを抽象クラスと言います。抽象クラスを継承した場合、サブクラスはすべての抽象メソッドをオーバーライドしなければならない義務を負います。すべての抽象メソッドをオーバーライドしていなければ、サブクラスはインスタンス化することすらできません。抽象メソッドによって、特定のメソッドがサブクラスで実装されることを保証できるようになります。

<?php
abstract class Figure{
・・・中略・・・
protected abstract function getArea(): float;
}

抽象メソッドを定義するには、abstract修飾子を指定します。

構文:抽象メソッド(abstract修飾子)

アクセス修飾子 abstract function メソッド名(データ型 引数, ・・・・): 戻り値のデータ型

抽象メソッドはサブクラスで必ずオーバーライドされるべきメソッドなので、スーパークラスの側では中身を持つことが出来ません。また、抽象メソッドを含むクラスには、class命令の前にも明示的にabstract修飾子を付加する必要があります。

インターフェース

ポリモーフィズムにも問題があります。それは、PHPでは多重継承が認められていません。一度に継承できるクラスは常に1つだけである点です。複数のクラスを同時に継承することはできません。

そこで、登場するのがインターフェースです、インターフェースとは、配下のメソッドがすべて抽象メソッドであるクラスのことです。

<?php
interface Ifigure{
   function getArea(): float;
}

インターフェースを定義するには、class命令の代わりにinterface命令を使います。

構文:interface命令

interface インターフェース名{ 
    ・・・・抽象メソッド、定数の定義・・・・
}

インターフェースはクラスにも似ていますが、構文の違いがいくつかあります。

  • 中身を持つメソッドやプロパティは定義できません。(配置できるのは、抽象メソッドと定数だけです)
  • 配下のメソッドが抽象メソッドであることは明らかなので、abstract修飾子は必要ありませんし、指定してはいけません。
  • アクセス修飾子も指定できません。
  • インターフェースであることがわかるように、IFigure、FigureInterfaceのように接頭辞/接尾辞を指定します。

そしてもう1つインターフェースとクラスとの決定的な違いが、多重継承が可能という点です。

それぞれのメソッドを目的に応じて異なるインターフェースに振り分けるのです。あとはサブクラス側では必要なメソッドを含むインターフェースだけを選択的に継承できるようになります。ちなみに、インターフェースの機能を受け継ぐことは、正確には継承するではなく「実装する」と言います。インターフェースを実装したクラスのことを実装クラスと呼びます。

<?php
require_once 'IFigure.php';

class Triangle implements IFigure{

    //プロパティを定義
    private float $width;
    private float $height;

    //コンストラクターでプロパティを初期化
    public function __construct(float $width, float $height){
        $this->width = $width;
        $this->height = $height;
    }

    //IFigure::getAreaメソッドを実装
    public function getArea(): float{
        return $this->width * $this->height / 2;
    }
}

インターフェースを実装するのは、implementsキーワードの役割です。複数のインターフェースを実装する場合には、「implements IFigure,ICountable」のようにカンマで区切って指定します。

構文:インターフェースの実装

class 実装クラス名 implements インターフェース名,・・・・{
   //クラスの本体
}

これでIFigureインターフェースを実装したTriangleクラスが定義できました。インターフェースでは、プロパティや中身のあるメソッドを(コンストラクターも)定義できないので、もともとはFIgure抽象クラスで定義していた内容が、Triangleクラスの中に移動しています。

instanceof演算子

ポリモーフィズムの性質を利用する上で、オブジェクトに実装されているインターフェースを確認することが重要です。このため利用できるのが、instanceof演算子です。

構文:instanceof演算子

変数 instanceof 型名

型名には、判定したインターフェースあるいはクラス名を指定します。instanceof演算子は、変数の型が次のいずれかの条件を満たしている場合には、trueを返します。

  • 型名の直接のインスタンスであるか
  • 型名の実装クラスのインスタンスであるか
  • 型名のサブクラスのインスタンスであるか
<?php
require_once 'Triangle.php';
require_once 'Square.php';

//IFigure実装クラスの配列を用意
$figs = [];
$figs[] = new Triangle(10,5);
$figs[] = new Square(10,5);
$figs[] = new Triangle(3,2);

//配列$figsの内容を順番に処理
foreach ($figs as $fig){
    //Ifigureインターフェースを実装している場合のみgetAreaメソッドを実行
    if($fig instanceof IFigure){    //➊
        print '<p>'. get_class($flg). ':' . $flg->getArea().'</p>';//➋
    }
}

➊では配列$figsから取り出したオブジェクトが、IFigureインターフェースを実装しているかどうか確認します。IFigure実装クラスであればgetAreaメソッドを必ず実装しているはずなので、これを安全に呼び出すことができます。

このように、異なる種類のオブジェクトが混在している場合でもまとめて処理できるのがポリモーフィズムの特徴です。なお、➋のget_class関数はオブジェクトの元クラス名を取得するための関数です。この他にもクラスやインターフェースの情報を取得するために、次の関数が用意されています。

クラス/インターフェース関連の関数

関数戻り値
get_class([$obj])オブジェクトの元となるクラス名
get_debug_type($v)オブジェクトであればクラス名、基本型であれば型名
get_parent_class([$obj])オブジェクトの元となるクラスの親クラス名
get_class_methods($clazz)クラスに属するメソッドの一覧($clazzはクラス名)
get_class_vars($clazz)クラスに属するプロパティの一覧($clazzはクラス名)

無名クラス

無名クラスは、その名のとおり、名前を持たないクラスです。名前がないので特定の文の中でしか利用できません。クラス定義からインスタンス化、呼び出しまでをまとめて表現できることには、以下のようなメリットがあります。

  • コードをシンプルに表現でき、見た目に関連性を把握しやすい
  • 式が許されている場所であれば、どこにでも記述できる
  • 名前がないので、そもそも名前が競合することがない

その性質上、定義したクラスをあとから利用しないことがわかっているクラスを定義するのに利用します。

<?php
interface Runnable{
    function run();
}

class MyClass{
    public function execute(Runnable $rc): void{
        //ダミー処理
        print 'start...';
        $rc->run();
        print 'end...';
    }
}

$cls = new MyClass();
$cls->execute(new class implements Runnable {
    public function run(): void {
        print 'process...';
    }
});
実行結果
実行結果

構文:無名クラス

new class { ・・・・プロパティ/メソッドの定義・・・・ }
new class extends 親クラス名{ ・・・・プロパティ/メソッドの定義・・・・ }
new class implements インターフェース名 { ・・・・プロパティ/メソッドの定義・・・・ }

無名クラスでは、new演算子に直接classブロックを渡します。extends/implementsキーワードを使って、継承/実装を表すこともできます。classブロックでは、プロパティ/メソッドをはじめ、コンストラクターも指定できます。ほとんど標準的なクラスの構文に準じます。