ryota21silvaの技術ブログ

Funna(ふんな)の技術ブログ

これまで学んだ技術の備忘録。未来の自分が救われることを信じて

PHPでポリモーフィズム、インターフェイスを理解する

ポリモーフィズム多態性)とは

ポリモーフィズムとは「同名のメソッドを異なるクラス間で使用できるようにすること」を意味しています。

例えば以下のようなSoldierクラス、Wizardクラス、Warriorクラスが存在し、これらのクラスに「攻撃をするメソッド」を定義したとします。

(※この例は以下の記事を参照しています。)

<?php
class Soldier {
    private const SOLDIER_POWER = 2;
    
  // 攻撃をするメソッド
    public function slash(): int {
        return self::SOLDIER_POWER;
    }
}
<?php
class Wizard {
    private const WIZARD_POWER = 1;
    
  // 攻撃をするメソッド
    public function cast(): int {
        return self::WIZARD_POWER;
    }
}
<?php
class Warrior {
    private const WARRIOR_POWER = 3;
    
  // 攻撃をするメソッド   
    public function knuckle(): int {
        return self::WARRIOR_POWER;
    }
}

これらの各メソッドは「攻撃をする」という同じ目的を持っていますが、クラスごとに別名で定義されています。

そこでポリモーフィズムを用いてみます。
まずCharacterというスーパークラスを作成し、Characterクラスを継承する複数のサブクラスではattack()という同名のメソッドを定義してあげます。

(Character.php)

<?php
class Character {
    // この記述はポリモーフィズムとしては不完全である(後述)
    public function attack(): int {
        return 0;
    }
}
<?php
require_once 'Character.php'

class Soldier {
    private const SOLDIER_POWER = 2;
        
    public function atack(): int {
        return self::SOLDIER_POWER;
    }
}
<?php
require_once 'Character.php'

class Wizard {
    private const WIZARD_POWER = 1;
        
    public function atack(): int {
        return self::WIZARD_POWER;
    }
}
<?php
require_once 'Character.php'

class Warrior {
    private const WARRIOR_POWER = 3;
        
    public function atack(): int {
        return self::WARRIOR_POWER;
    }
}

このようにポリモーフィズム同名のメソッドで異なる挙動を実現してくれるため、同じ目的を持った機能を呼ぶために各クラスにわざわざ異なる名前のメソッドを定義する必要が無くなります。

これによってコードの書き直しが減る&クラスごとにメソッド名を覚える必要が無くなるため、記述の間違いが減るなどのメリットが得られます。大規模な開発を行う場合は特に恩恵が受けられそうです。

抽象クラス/抽象メソッド

ただし、上記の定義では「サブクラスがattack()メソッドを実装してくれることが保証されていない」ため、ポリモーフィズムとしては不完全です。

スーパークラスとしてはサブクラスがattack()メソッドを実装してくれることを期待していたとしても、サブクラスはattack()メソッドと同じ目的のメソッドを別に定義してしまう可能性があります。サブクラスがスーパークラス側の都合を理解しているとは限りません。

そこで使用するのが抽象メソッドです。
抽象メソッドとは「それ自身が機能を持たない空のメソッドのこと」です。

抽象メソッド自体は機能を持たないため、外部(サブクラス)から機能を与えてあげる必要があります。つまり抽象メソッドとは「サブクラスでオーバオライドされることを期待したメソッド」と言えます。(独習PHP第3版より)

使い方としては、まず抽象メソッドを含んだ抽象クラスを定義してあげます。

(CharacterAbstract.php)

<?php
abstract class CharacterAbstract {
    // 抽象メソッドなので中身を持たない
    protected abstract function attack(): int;
}

サブクラスでの定義は先ほどと同じ通りです。

最初の例と異なるポイントは、抽象クラスを継承したサブクラスは「全ての抽象メソッドをオーバーライドしなければならない義務を負う」という点です。
もし全てのメソッドをオーバーライドしていない場合はエラーが表示され、サブクラスをインスタンス化できなくなります。

このように抽象メソッドを使用すれば「特定のメソッドがサブクラスで実装されることを保証できる」ようになります。

インターフェイス

ただしこれでもまだ問題があります。

PHPでは多重継承ができない(複数のクラスを同時に継承できない)ため、ポリモーフィズムを実現したい全ての機能を1つの抽象クラスにまとめなければならない上、サブクラスが必要としない機能までオーバーライドする必要があるというデメリットが存在します。

例えば、

  • Soldierクラスではattack()メソッドとguard()メソッドが必要

  • Wizardクラスではattack()メソッドだけが必要

といったケースの場合、CharacterAbstractクラスにはattack()guard()という抽象メソッドを定義することになります。
そして前述の通り抽象クラスを継承したサブクラスは全ての抽象メソッドをオーバーライドしなければならない義務を負うため、Wizardクラスは抽象クラスであるCharacterAbstractクラスを継承した時点で、必要としないguard()メソッドまでオーバーライドしなければならなくなります。

こうなるとサブクラスの役割がよく分からなくなってしまう上、コードも冗長になります。

そこで、この問題を解決するために使用するものがインターフェイスです。
インターフェイスとは、

(やや強引に言えば)配下のメソッドが全て抽象メソッドであるクラスのこと(独習PHP第3版より)

と言えます。

実際にコードを書いてみます。先ほどのCharacterAbstractインターフェイスで書き換えると以下のようになります。
そしてインターフェイスでは中身を持つメソッドや、プロパティは定義できず、抽象メソッドと定数のみが定義可能です。

(CharacterInterface.php)
<?php
interface AttackInterface {
    function attack(): int;
}

interface GuardInterface {
    function guard(): int;
}

抽象クラスとの大きな違いはインターフェイスは多重継承が可能である点」です。
このインターフェイスの特徴により、以下のようにサブクラスごとに必要なメソッドだけを実装することができます。

そして、インターフェイスに含まれる全てのメソッドを実装する必要がある」という制約上のメリットも受けられます。

(Soldier.php)
<?php
require_once 'Character.php'

// AttackInterfaceとGuardInterfaceを実装している
class Soldier implements AttackInterface, GuardInterface {
    private const SOLDIER_POWER = 2;
    private const SOLDIER_DEFENCE = -1;

    public function atack(): int {
        return self::SOLDIER_POWER;
    }

    public function defence(): int {
        return self::SOLDIER_DEFENCE;
    }
}
(Wizard.php)
<?php
require_once 'Character.php'

// AttackInterfaceだけを実装している
class Wizard implements implements AttackInterface {
    private const WIZARD_POWER = 1;
        
    public function atack(): int {
        return self::WIZARD_POWER;
    }
}

またインターフェイスの機能を受け継ぐことは厳密に言えば「継承する」ではなく「実装する」と呼ぶため、こちらで覚えておくべきかと思います。またインターフェイスを実装するクラスのことを実装クラスと呼びます。

補足

また、以下のコードも見てみます。

<?php
interface AttackInterface {
    function attack(): int;

    function doubleAattack(): int;
}


class Soldier implements AttackInterface, GuardInterface {
    private const SOLDIER_POWER = 2;

    public function atack(): int {
        return self::SOLDIER_POWER;
    }

    public function doubleatack(): int {
        return self::SOLDIER_POWER * 2;
    }
}

class Wizard implements implements AttackInterface {
    private const WIZARD_POWER = 1;
        
    public function atack(): int {
        return self::WIZARD_POWER;
    }

    public function doubleatack(): int {
        return self:: WIZARD_POWER * 2;
    }
}



// 使用する側のコード
public function buttle(AttackInterface $attack) {
    return $attack->atack();
    return $attack->doubleatack();
}

実装したメソッドを使用する側のコードに注目です。
インターフェイスであるAttackInterfacebuttle()メソッドの引数として型指定して受け取ることで、buttle()メソッドの引数はAttackInterfaceを実装しているクラスのオブジェクトであることが確実となります。

そしてbuttle()メソッドで呼び出しているattack()メソッドとdoubleatack()メソッドはAttackInterfaceの抽象メソッドなので、実装クラスには必ず存在するメソッドと言えます。

したがって、使用する側(buttle()メソッド)の引数に渡された変数がSoldierクラスのオブジェクトであろうとWizardクラスのオブジェクトであろうと、使用する側のコードの記述を一切変えること無くメソッドを扱うことが可能になります。使用する側のコードを実装クラスに合わせて変更する必要はありません。

メモ

実際にインターフェイスを使う場合はこんな感じでしょうか?

<?php
class AttackGetting
{
    private $attackType

    public function __construct(string $attackType)
    {
        // Setter
        $this->attackType = $attackType;
    }

     // Getter
    public function getAttackType(): AttackInterface
    {
        if ($this->attackType === 'Soldier') {
            return new Soldier();
        } else if ($this->attackType === 'Wizard') {
           return new Wizard();
        }
    }
}
<?php

// 使用する側のコード
public function buttle(AttackInterface $attack) {
    return $attack->atack();
    return $attack->doubleatack();
}


$attack_getting = new AttackGetting('Soldier');
buttle($attack_getting->getAtacckType());

参考記事