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(); }
実装したメソッドを使用する側のコードに注目です。
インターフェイスであるAttackInterface
をbuttle()
メソッドの引数として型指定して受け取ることで、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());