この記事は PHP Advent Calendar 2020 - Qiita の 8 日目 の記事です
ポリモーフィズムは言葉による説明だけでは恩恵を実感しにくいので、
実際に手を動かして体感してみましょう、という趣旨のエントリです。
なお、ある程度の規模を持ったプログラムでないと、ポリモーフィズムの恩恵を実感しにくいところを、敢えて、ショートコードで実装しているので、一部現実的ではない設計になっているところがあります。
タイトルが大げさなわりに中身やコードそのものはしょうもないです。
対象読者 / 留意点
- オブジェクト指向の初歩的なエントリです。
- 主に、Interface, Trait, Abstractにフォーカスを当てています。
- あくまで筆者自身の現時点での捉え方にすぎませんので誤り点ありましたらコメント欄にて教えて頂けると幸いです。
本エントリで扱わないこと
- 「ポリモーフィズムとは何か」という説明は多くの優良記事があるので、
そちらにお任せし、あくまで「実装を通してメリットを体験すること」に重きを置きます。
免責事項
本エントリをご覧頂いている方々へ。
本エントリ内で、触れているポケットモンスター関連の著作権・商標登録を有している権利団体や、メディア媒体Qiita関連の権利団体から、削除/編集要請などがあった際には、速やかに削除対応/編集対応させて頂きますので、ご理解のほどよろしくお願いします。
それでは本題へ移りましょう!
抽象クラス、インターフェース、トレイトを使うと何が嬉しいの?
「具象クラスの継承のみで構成してもいいんじゃないの?」という観点に対して
1. 実装漏れを防ぐことができる。
1匹のポケモンに対して、具象クラスを1つずつ付けるとして、ピチューに逃げるを実装し損ねてしまうと、Fatal Errorを吐いてくれて、抽象メソッドを実装してくださいと怒ってくれる。 => "contains 1 abstract method and must therefore be declared abstract or implement the remaining methods" 。
2. パーツとして切り出せる。
単一継承の制約をしてもらっている中で、「部分的に」複数回記述が被っている部分をパーツとして切り出すことができる。「余分なメソッド」を下位の具象クラスに継承させなくて済む。
3. テストの際にモックしやすくなる
(=> Dependency Injectionが使える)
本エントリ内で取り扱う例を使って触れることのできる話ではないので別エントリに書きます。
今から実装するミニゲームの設定
-
野生のポケモンの行動をテロップとして流す既存のPHPコードに仕様追加していきます。
- 「たたかう」「にげる」のみ実装済
-
追加実装:1回目
- ポケモンの数を151匹から251匹に増やすことになり、自分はピチューを実装するように頼まれた。
-
追加実装:2回目
- 捕獲困難な伝説のポケモン・ライコウの実装を頼まれた。
大雑把に実装条件を列挙
- 151匹のポケモンを実装したPHPコードがすでに存在している。(後述のbase.php)
- stringでテロップを返す。
- 野生のポケモンは「たたかう」「にげる」ができる
- 技には使えるポケモンと使えないポケモンの区別がある。
- 10まんボルトを使えるポケモンと使えないポケモンが居る。
- [使える] ピカチュウ、ライチュウ、ピチュー、ライコウ
- [使えない] アーボック
- 10まんボルトを使えるポケモンと使えないポケモンが居る。
主人公エンジニアが本コードを設計時に考えたこと。
Interface (インターフェース)
- 野生のポケモンとプレイヤーのポケモンで共通するものを抽象メソッドで宣言。
- 継承先にて、そのインターフェイスを実装。
- 例えば、プレイヤーのポケモンには「やせいのXXXがあらわれた」という実装は不要。
Trait (トレイト)
- 技(10まんボルト, なみのり)は、ポケモンによって使える使えないが異なるので、共通部品として切り出してあげたい。
Abstract Class (抽象クラス)
- 野生のポケモンのみに共通する挙動は、この階層にて抽象メソッドで制約を加え、その他の共通処理はメソッド内部の実装まで行ってしまい、具象メソッドとして書く。
自分の好きな関係図 (abstract, trait, interface)
全体像を俯瞰するにはとても見やすい関係図とその掲載元のリンクを貼っておきます。
実装前に、一度ご覧ください。
)
出典元:https://coinbaby8.com/php-class-abstract-interface-trait-di.html
長い長い前置きが終わり、ようやく手を動かせます!
体験方法
(手順1/2) まずベースとなるコードをbase.phpとしてコピー&ペーストして保存します。
編集前のコードは以下です。
<?php
// ポケモン (Interface)
interface PokemonInterface
{
// [Interface内で抽象メソッド]
public function chooseAction($rate): string;
// [Interface内で抽象メソッド]
public function fight(): string;
// [Interface内で抽象メソッド]
public function run(): string;
}
// 10まんボルト (トレイト)
trait ThunderboltTrait
{
protected static $power = 100;
protected static $type = 'でんき';
protected static $name = '10まんボルト';
public function fight(): string
{
return $this->useThunderbolt();
}
public function useThunderbolt(): string
{
return 'てきの ' . static::NAME . ' の ' . self::$name;
}
}
// 野生のポケモン (抽象クラス)
abstract class WildPokemonAbstract
{
// [抽象クラス内で抽象メソッド]
abstract public function appear(): string;
// [抽象クラス内で具象メソッド]
public function chooseAction($rate = 99): string
{
if (mt_rand(1, 100) <= $rate) {
return $this->fight();
}
return $this->run();
}
// [抽象クラス内で具象メソッド]
public function fight(): string
{
// 遅延静的束縛 (Late Static Binding)
return 'てきの ' . static::NAME . ' は ' . 'わざ' . ' を くりだした';
}
// [具象クラス内で具象メソッド]
public function run(): string
{
// 遅延静的束縛 (Late Static Binding)
return 'てきの ' . static::NAME . ' は にげだした';
}
}
// ポケモン (具象クラス)
class WildPokemon extends WildPokemonAbstract implements PokemonInterface
{
// ポケモン図鑑の番号
protected const INDEX = 'none';
// ポケモンの日本語名
protected const NAME = 'ポケモン';
// ポケモンの逃走率
protected const FLEERATE = 2;
// [具象クラス内で具象メソッド]
public function appear(): string
{
return 'やせいの ' . '[' . static::INDEX . '] ' . static::NAME . ' が あらわれた';
}
}
/*
|--------------------------------------------------------------------------
| 具体的なポケモンの省略
|--------------------------------------------------------------------------
|
| ポケモン図鑑001-023は省略
| ポケモン図鑑026-151は省略
|
*/
// アーボック
class WildArbok extends WildPokemon
{
// ポケモン図鑑の番号
protected const INDEX = '024';
// ポケモン日本語名
protected const NAME = 'アーボック';
}
// ピカチュウ
class WildPikachu extends WildPokemon
{
use ThunderboltTrait;
// ポケモン図鑑の番号
protected const INDEX = '025';
// ポケモン日本語名
protected const NAME = 'ピカチュウ';
}
// ライチュウ
class WildRaichu extends WildPokemon
{
// ポケモン図鑑の番号
protected const INDEX = '026';
// ポケモン日本語名
protected const NAME = 'ライチュウ';
}
/*
|--------------------------------------------------------------------------
| 具体的なポケモンの省略
|--------------------------------------------------------------------------
|
| ポケモン図鑑001-023は省略
| ポケモン図鑑026-151は省略
|
*/
// インスタンス生成
$wildPikachu = new WildPikachu();
// 草むらにて野生のピカチュウとエンカウント
echo $wildPikachu->appear() . PHP_EOL; // やせいの [025] ピカチュウ が あらわれた
// 野生のピカチュウが確率で行動を選択
echo $wildPikachu->chooseAction() . PHP_EOL;
/*
|--------------------------------------------------------------------------
| (99%の確率で) てきの ピカチュウ の 10まんボルト
| (1%の確率で) てきの ピカチュウ は にげだした
|--------------------------------------------------------------------------
*/
// 野生のピカチュウがたたかうメソッド
echo $wildPikachu->fight() . PHP_EOL; // てきの ピカチュウ の 10まんボルト
// 野生のピカチュウが逃げるメソッド
echo $wildPikachu->run() . PHP_EOL; // てきの ピカチュウ は にげだした
(手順2/2) ターミナルアプリで、下記コマンドを実行すると出力されます。
$ php base.php
いかがでしょう?
4行の文字出力は正常に出ましたか?
やせいの [025] ピカチュウ が あらわれた
てきの ピカチュウ の 10まんボルト
てきの ピカチュウ の 10まんボルト
てきの ピカチュウ は にげだした
それでは追加実装に、移って行きます。
追加実装 : 1回目
251匹に増えたので、ピチューを担当することになりました。
- ピチューにも10まんボルトが使えるように実装したいです。
base.phpに下記追記をします。
<?php
// 既存コードの一番下に追記
/*
|--------------------------------------------------------------------------
| // ピチュー (No.172) 追加
|--------------------------------------------------------------------------
*/
// ピチュー (172) 追加
class WildPichu extends WildPokemon
{
use ThunderboltTrait;
// ポケモン図鑑の番号
protected const INDEX = '172';
// ポケモン日本語名
protected const NAME = 'ピチュー';
}
$wildPichu = new WildPichu();
// 草むらにて野生のピチューとエンカウント
echo $wildPichu->appear() . PHP_EOL; // やせいの [172] ピチュー が あらわれた
// 野生のピチューが確率で行動を選択
echo $wildPichu->chooseAction() . PHP_EOL;
/*
|--------------------------------------------------------------------------
| (99%の確率で) てきの ピチュー の 10まんボルト
| (1%の確率で) てきの ピチュー は にげだした
|--------------------------------------------------------------------------
*/
// 野生のピチューがたたかうメソッド
echo $wildPichu->fight() . PHP_EOL; // てきの ピチュー の 10まんボルト
// 野生のピチューが逃げるメソッド
echo $wildPichu->run() . PHP_EOL; // てきの ピチュー は にげだした
下記コマンドの実行
$ php base.php
いかがでしょう?
8行の文字出力は正常に出ましたか?
やせいの [025] ピカチュウ が あらわれた
てきの ピカチュウ の 10まんボルト
てきの ピカチュウ の 10まんボルト
てきの ピカチュウ は にげだした
やせいの [172] ピチュー が あらわれた
てきの ピチュー の 10まんボルト
てきの ピチュー の 10まんボルト
てきの ピチュー は にげだした
追加実装 : 2回目
伝説のポケモン・ライコウの実装
- ライコウは、1ターン目ですぐに逃げてしまうので、それをchooseActionの中で反映させてあげたいです。
base.phpに下記追記をします。
<?php
// 既存コードの一番下に追記
/*
|--------------------------------------------------------------------------
| ライコウ (No.243) 追加
|--------------------------------------------------------------------------
*/
// ライコウ (243) 追加
class WildRaikou extends WildPokemon
{
use ThunderboltTrait;
// ポケモン図鑑の番号
protected const INDEX = '243';
// ポケモン日本語名
protected const NAME = 'ライコウ';
// ポケモンの逃走率
protected const FLEERATE = 99;
// [抽象クラス内で定義した具象メソッドのオーバーライド]
public function chooseAction($rate = self::FLEERATE): string
{
if (mt_rand(1, 100) > $rate) {
return $this->fight();
}
return $this->run();
}
}
$wildRaikou = new WildRaikou();
// 草むらにて野生のライコウとエンカウント
echo $wildRaikou->appear() . PHP_EOL; // やせいの [243] ライコウ が あらわれた
// 野生のライコウが確率で行動を選択
echo $wildRaikou->chooseAction() . PHP_EOL;
/*
|--------------------------------------------------------------------------
| (1%の確率で) てきの ライコウ の 10まんボルト
| (99%の確率で) やせいの ライコウ は にげだした
|--------------------------------------------------------------------------
*/
// 野生のライコウのたたかうメソッド
echo $wildRaikou->fight() . PHP_EOL; // てきの ライコウ の 10まんボルト
// 野生のライコウの逃げるメソッド
echo $wildRaikou->run() . PHP_EOL; // てきの ライコウ は にげだした
下記コマンドの実行
$ php base.php
いかがでしょう?
12行の文字出力は正常に出ましたか?
やせいの [025] ピカチュウ が あらわれた
てきの ピカチュウ の 10まんボルト
てきの ピカチュウ の 10まんボルト
てきの ピカチュウ は にげだした
やせいの [172] ピチュー が あらわれた
てきの ピチュー の 10まんボルト
てきの ピチュー の 10まんボルト
てきの ピチュー は にげだした
やせいの [243] ライコウ が あらわれた
てきの ライコウ は にげだした
てきの ライコウ の 10まんボルト
てきの ライコウ は にげだした
いかがでしたでしょうか?
インターフェースやトレイトを耳にしたことはあるけれど、
実際にコードに入れる機会がなかった、という方が
イメージするのにお役に立てたら嬉しいです。
おっと...読者の方に、次の実装依頼がきたようです
実装追加 : 3 (体験: トレイト)
実装要件
- アーボック(Arbok)が、どくばり(PoisonSting)を標準出力できるように実装
実装する際の1つの提案
- テロップでどくばりを流すために、10まんボルトの時と同じく、PoisonStingをtraitとして実装してみるのは1つの実装方法かと思います。
実装追加 : 4 (体験: トレイト + 具象クラス追加)
実装要件
- 第3世代のNo.258 ミズゴロウ(Mizugorou)をキャラクタとして実装
- なみのりをテロップとして流してあげる実装。
実装する際の1つの提案
- 具象クラスとしてミズゴロウを実装して
- なみのり(Surf)をtraitとして実装するのは1つの実装方法かと思います。
実装追加 : 5 (体験: インターフェース + 抽象クラス)
実装要件
- ライチュウがまひ状態に陥る実装
- 「ライチュウ は からだが しびれて うごけない」をテロップとして流してあげる実装。
実装する際の1つの提案
- 全てのポケモン(トレーナーのポケモンも野生のポケモンも)まひ状態に陥る可能性があるので、InterfaceにParalysisを抽象メソッドとして加えてあげる。
- 宣言した抽象メソッドは必ず「インターフェースの実装」として中身を書いてあげないといけないので、2つの抽象クラス(WildPokemonAbstract と TrainerPokemonAbstract)の中で実装してあげる。
- 実際に、実装済みのポケモンクラスをインスタンス化し、まひ状態のテロップを流すメソッドを呼び出すのは1つの実装方法かと思います。
あとがき
ご覧頂きありがとうございました。
今年もお互いに実りあるクリスマスを過ごせますように