#04 オブジェクト指向プログラミング入門

ポリモーフィズム——同じメソッド名で違う動作をする

ポリモーフィズムとは

ポリモーフィズム(多態性)は、OOPの四大原則のひとつです。難しそうな言葉ですが、概念はシンプルです。

同じメソッド名を呼び出しても、オブジェクトの種類によって異なる動作をする

前回の継承で少し触れましたが、ポリモーフィズムはその先にある考え方です。「呼び出す側はオブジェクトの具体的な型を知らなくても使える」という強力な設計が生まれます。


NG例:ポリモーフィズムなしのコード

// NG: 種類が増えるたびに if/switch が増える
function makeSound(string $type, string $name): string
{
    if ($type === 'dog') {
        return "{$name}がワンワン鳴いた";
    } elseif ($type === 'cat') {
        return "{$name}がニャーニャー鳴いた";
    } elseif ($type === 'cow') {
        return "{$name}がモーモー鳴いた";
    }
    return '...';
}

新しい動物を追加するたびにこの関数を修正しなければなりません。これは開放・閉鎖原則(OCP)に違反します(「既存コードを変えずに機能追加できるべき」という原則)。


OK例:ポリモーフィズムを使う

abstract class Animal
{
    public function __construct(
        protected string $name
    ) {}

    // 子クラスで必ず実装する(抽象メソッド)
    abstract public function sound(): string;

    public function makeSound(): string
    {
        return "{$this->name}が{$this->sound()}と鳴いた";
    }
}

class Dog extends Animal
{
    public function sound(): string { return 'ワンワン'; }
}

class Cat extends Animal
{
    public function sound(): string { return 'ニャーニャー'; }
}

class Cow extends Animal
{
    public function sound(): string { return 'モーモー'; }
}
$animals = [
    new Dog('ポチ'),
    new Cat('タマ'),
    new Cow('ベッシー'),
];

foreach ($animals as $animal) {
    echo $animal->makeSound() . "\n";
}
// ポチがワンワンと鳴いた
// タマがニャーニャーと鳴いた
// ベッシーがモーモーと鳴いた

呼び出し側は $animal->makeSound() と書くだけ。どの動物かを知る必要がありません。新しい動物を追加しても、呼び出し側のコードは一切変更不要です。


インターフェースを使ったポリモーフィズム

継承を使わなくてもポリモーフィズムは実現できます。インターフェースを使うと、関係のないクラス同士にも同じメソッド名を「約束」させられます。

支払い方法の例

interface Payable
{
    public function pay(int $amount): string;
}

class CreditCard implements Payable
{
    public function __construct(
        private string $cardNumber
    ) {}

    public function pay(int $amount): string
    {
        $masked = '---' . substr($this->cardNumber, -4);
        return "クレジットカード({$masked})で{$amount}円を支払いました。";
    }
}

class BankTransfer implements Payable
{
    public function __construct(
        private string $accountNumber
    ) {}

    public function pay(int $amount): string
    {
        return "銀行振込(口座:{$this->accountNumber})で{$amount}円を送金しました。";
    }
}

class PayPay implements Payable
{
    public function pay(int $amount): string
    {
        return "PayPayで{$amount}円を支払いました。";
    }
}
// 支払い処理をする関数(Payableなら何でも受け取れる)
function checkout(Payable $paymentMethod, int $amount): void
{
    echo $paymentMethod->pay($amount) . "\n";
}

checkout(new CreditCard('1234567890123456'), 3000);
checkout(new BankTransfer('123-456-789'), 3000);
checkout(new PayPay(), 3000);

checkout() 関数は Payable インターフェースを受け取ります。クレジットカードでも銀行振込でも PayPay でも、Payable を実装していれば何でも渡せます。将来 ApplePay を追加しても checkout() は変更不要です。


PHPの型宣言とポリモーフィズム

PHP 8.x では型宣言が強化されており、ポリモーフィズムを安全に使えます。

// 型宣言でインターフェースを指定
function processAnimals(Animal ...$animals): void
{
    foreach ($animals as $animal) {
        echo $animal->makeSound() . "\n";
    }
}

processAnimals(new Dog('ポチ'), new Cat('タマ'));

型宣言により、Animal を継承していないクラスを渡すとエラーになります。安全性と読みやすさが両立します。

ユニオン型(PHP 8.0)

function describe(Dog|Cat $pet): string
{
    return $pet->getName() . 'は ' . $pet->sound() . 'と鳴く';
}

インターセクション型(PHP 8.1)

interface Loggable
{
    public function log(): string;
}

// Payable かつ Loggable を満たすオブジェクトのみ受け取る
function auditPayment(Payable&Loggable $method, int $amount): void
{
    echo $method->pay($amount);
    echo $method->log();
}

ポリモーフィズムのメリットまとめ

観点効果
拡張性新しい型を追加しても既存コードを変更しない
可読性呼び出し側がシンプルになる
テストモックオブジェクトを使いやすい
保守性変更の影響範囲を限定できる

よくある誤解:型チェックはポリモーフィズムの失敗サイン

// NG: instanceof でチェックしているのはポリモーフィズムを使えていないサイン
function handlePayment(object $method): void
{
    if ($method instanceof CreditCard) {
        // ...
    } elseif ($method instanceof BankTransfer) {
        // ...
    }
}

// OK: インターフェースで型を保証する
function handlePayment(Payable $method): void
{
    $method->pay(1000);
}

instanceof での分岐が増えてきたら、それはポリモーフィズムを使うチャンスです。


まとめ

  • ポリモーフィズムは「同じメソッド名で異なる動作」を実現する仕組み
  • 継承のオーバーライドまたはインターフェースの実装で実現する
  • 呼び出し側はオブジェクトの具体的な型を意識しなくて済む
  • 新しい型を追加しても既存コードを変えなくてよい(OCP)
  • instanceof の多用はポリモーフィズムへの切り替えサイン

次回は抽象クラスとインターフェース——今回さらっと登場した abstractinterface の違いを詳しく学びます。