あっぽログ
← 記事一覧に戻る

PHPのインターフェースと抽象クラスを使いこなす:設計の柔軟性を高める実践ガイド

はじめに

PHPでオブジェクト指向プログラミングを学ぶとき、「インターフェース」と「抽象クラス」の違いに悩む方は多いです。どちらも「継承先のクラスに実装を強制する」仕組みですが、設計上の役割は大きく異なります。

この記事では、それぞれの特徴と使いどころをコード例とともに丁寧に解説します。


インターフェースとは

インターフェース(interface) は、クラスが「何ができるか」を定義する契約書のようなものです。メソッドのシグネチャ(名前・引数・戻り値の型)だけを宣言し、具体的な実装は持ちません。

<?php

interface Notifiable
{
    public function send(string $message): bool;
    public function getChannel(): string;
}

インターフェースを実装するクラスは、implements キーワードを使い、宣言されたすべてのメソッドを実装する必要があります。

<?php

class EmailNotifier implements Notifiable
{
    public function __construct(private string $to) {}

    public function send(string $message): bool
    {
        // メール送信処理(省略)
        echo "メール送信先: {$this->to} / メッセージ: {$message}" . PHP_EOL;
        return true;
    }

    public function getChannel(): string
    {
        return 'email';
    }
}

class SlackNotifier implements Notifiable
{
    public function __construct(private string $webhook) {}

    public function send(string $message): bool
    {
        echo "Slack通知: {$this->webhook} / メッセージ: {$message}" . PHP_EOL;
        return true;
    }

    public function getChannel(): string
    {
        return 'slack';
    }
}

インターフェースの多重実装

PHPはクラスの多重継承をサポートしませんが、インターフェースは複数実装できます。

<?php

interface Loggable
{
    public function log(string $level, string $message): void;
}

class EmailNotifier implements Notifiable, Loggable
{
    public function send(string $message): bool { /* ... */ return true; }
    public function getChannel(): string { return 'email'; }

    public function log(string $level, string $message): void
    {
        echo "[{$level}] {$message}" . PHP_EOL;
    }
}

抽象クラスとは

抽象クラス(abstract class) は、インターフェースと通常のクラスの中間に位置します。抽象メソッド(実装なし)と通常のメソッド(実装あり)を混在させることができます。

<?php

abstract class BaseNotifier
{
    // 共通の実装を持つメソッド
    public function sendWithLog(string $message): bool
    {
        echo "[LOG] 送信開始: チャンネル={$this->getChannel()}" . PHP_EOL;
        $result = $this->send($message);
        echo "[LOG] 送信結果: " . ($result ? '成功' : '失敗') . PHP_EOL;
        return $result;
    }

    // サブクラスに実装を強制する抽象メソッド
    abstract public function send(string $message): bool;
    abstract public function getChannel(): string;
}

サブクラスは extends で継承し、抽象メソッドをすべて実装します。

<?php

class SmsNotifier extends BaseNotifier
{
    public function __construct(private string $phoneNumber) {}

    public function send(string $message): bool
    {
        echo "SMS送信先: {$this->phoneNumber} / メッセージ: {$message}" . PHP_EOL;
        return true;
    }

    public function getChannel(): string
    {
        return 'sms';
    }
}

// 使用例
$notifier = new SmsNotifier('090-1234-5678');
$notifier->sendWithLog('登録が完了しました');
// [LOG] 送信開始: チャンネル=sms
// SMS送信先: 090-1234-5678 / メッセージ: 登録が完了しました
// [LOG] 送信結果: 成功

インターフェース vs 抽象クラス:どう使い分けるか

比較項目インターフェース抽象クラス
実装の共有❌ できない✅ できる
多重継承✅ 複数実装可❌ 単一継承のみ
コンストラクタ❌ 持てない✅ 持てる
プロパティ❌ 持てない✅ 持てる
主な用途「できること」の契約共通処理のテンプレート

使い分けの指針

  • インターフェース:異なる系統のクラスに同じ「能力」を持たせたいとき。たとえば Notifiable(通知できる)、Serializable(シリアライズできる)など。
  • 抽象クラス:共通の処理フローをテンプレートとして提供し、一部の処理だけをサブクラスに委ねたいとき。

実践:インターフェースと抽象クラスを組み合わせる

現実の設計では、両者を組み合わせるのが効果的です。

<?php

interface Notifiable
{
    public function send(string $message): bool;
    public function getChannel(): string;
}

abstract class BaseNotifier implements Notifiable
{
    public function sendWithLog(string $message): bool
    {
        echo "[LOG] [{$this->getChannel()}] 送信開始" . PHP_EOL;
        $result = $this->send($message);
        echo "[LOG] 送信" . ($result ? '成功' : '失敗') . PHP_EOL;
        return $result;
    }
}

class PushNotifier extends BaseNotifier
{
    public function send(string $message): bool
    {
        echo "プッシュ通知: {$message}" . PHP_EOL;
        return true;
    }

    public function getChannel(): string
    {
        return 'push';
    }
}

// 型ヒントにはインターフェースを使う
function notify(Notifiable $notifier, string $message): void
{
    $notifier->send($message);
}

$push = new PushNotifier();
notify($push, 'お知らせがあります');
// プッシュ通知: お知らせがあります

型ヒントに Notifiable インターフェースを使うことで、EmailNotifier でも SlackNotifier でも同じ関数に渡せます。これが依存を抽象に向ける設計の基本です。


まとめ

  • インターフェースは「何ができるか」の契約を定義し、複数実装できる
  • 抽象クラスは共通処理をまとめ、サブクラスに差分だけを実装させる
  • 両者を組み合わせることで、柔軟で保守性の高い設計が実現できる

最初は難しく感じるかもしれませんが、「インターフェースは約束、抽象クラスはテンプレート」と覚えておくと判断しやすくなります。ぜひ自分のコードで試してみてください。

← 記事一覧に戻る