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

抽象クラスとインターフェース——設計の「約束」を作る

「約束」としての設計

クラス設計で大切なのは「このクラスは必ずこのメソッドを持つ」という約束を明示することです。チームで開発するとき、その約束がないと誰かが必要なメソッドを実装し忘れたり、メソッド名がバラバラになったりします。

PHPには約束を強制する仕組みが2つあります。

  • 抽象クラス(abstract class)
  • インターフェース(interface)

どちらも「このメソッドを実装しなければならない」という約束を作りますが、使い方が異なります。


抽象クラス(abstract class)

抽象クラスは「共通の実装を持ちつつ、一部のメソッドは子クラスに実装を委ねる」クラスです。

abstract class Shape
{
    // 抽象メソッド:子クラスで必ず実装する
    abstract public function area(): float;
    abstract public function perimeter(): float;

    // 共通の実装:すべての子クラスで使える
    public function describe(): string
    {
        return sprintf(
            '%sの面積は%.2f、周の長さは%.2fです。',
            static::class,
            $this->area(),
            $this->perimeter()
        );
    }
}
class Circle extends Shape
{
    public function __construct(
        private float $radius
    ) {}

    public function area(): float
    {
        return M_PI * $this->radius ** 2;
    }

    public function perimeter(): float
    {
        return 2 * M_PI * $this->radius;
    }
}

class Rectangle extends Shape
{
    public function __construct(
        private float $width,
        private float $height
    ) {}

    public function area(): float
    {
        return $this->width * $this->height;
    }

    public function perimeter(): float
    {
        return 2 * ($this->width + $this->height);
    }
}
$circle    = new Circle(5);
$rectangle = new Rectangle(4, 6);

echo $circle->describe();
// Circleの面積は78.54、周の長さは31.42です。

echo $rectangle->describe();
// Rectangleの面積は24.00、周の長さは20.00です。

describe() は抽象クラスに共通実装があるので、子クラスで書く必要がありません。area()perimeter() だけを実装すれば済みます。

抽象クラスのルール

  • abstract メソッドを1つでも持つクラスは abstract class にしなければならない
  • 抽象クラスは new でインスタンス化できない
  • 子クラスはすべての abstract メソッドを実装しなければならない

インターフェース(interface)

インターフェースは「このメソッドを持つことを保証する契約」です。実装は一切持ちません。

interface Exportable
{
    public function toCsv(): string;
    public function toJson(): string;
}

interface Printable
{
    public function print(): void;
}
class Report implements Exportable, Printable
{
    public function __construct(
        private string $title,
        private array $data
    ) {}

    public function toCsv(): string
    {
        $lines = [];
        foreach ($this->data as $row) {
            $lines[] = implode(',', $row);
        }
        return implode("\n", $lines);
    }

    public function toJson(): string
    {
        return json_encode([
            'title' => $this->title,
            'data'  => $this->data,
        ], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
    }

    public function print(): void
    {
        echo "=== {$this->title} ===\n";
        foreach ($this->data as $row) {
            echo implode(' | ', $row) . "\n";
        }
    }
}

インターフェースは複数実装できるのが大きな特徴です。PHPは多重継承(複数のクラスを継承)をサポートしませんが、インターフェースは何個でも実装できます。


abstract class と interface の違い

比較項目abstract classinterface
インスタンス化不可不可
実装の提供可能不可(PHP 8.0以降はdefaultメソッドなし)
プロパティ持てる定数のみ
継承・実装extends(単一)implements(複数可)
コンストラクタ持てる持てない
アクセス修飾子自由すべて public

どちらを使うか

抽象クラスを使う場面

  • 共通の実装(コード)を子クラスに継承させたい
  • 「同じ種類のもの」を抽象化したい(Shape → Circle/Rectangle)
  • プロパティやコンストラクタが必要

インターフェースを使う場面

  • 「できること」を契約したい(Exportable, Printable)
  • 無関係なクラスに同じ能力を持たせたい
  • 複数の契約を1つのクラスに持たせたい

インターフェースの定数

インターフェースには定数を定義できます。

interface Status
{
    const ACTIVE   = 'active';
    const INACTIVE = 'inactive';
    const PENDING  = 'pending';
}

class User implements Status
{
    private string $status = Status::PENDING;

    public function activate(): void
    {
        $this->status = self::ACTIVE;
    }

    public function getStatus(): string
    {
        return $this->status;
    }
}

インターフェースの継承

インターフェース同士も継承できます。

interface Readable
{
    public function read(): string;
}

interface Writable
{
    public function write(string $content): void;
}

// 両方を継承した新しいインターフェース
interface ReadWrite extends Readable, Writable
{
    public function seek(int $position): void;
}

実用例:リポジトリパターン

Laravelなどのフレームワークでよく使われるパターンです。

interface UserRepositoryInterface
{
    public function findById(int $id): ?User;
    public function findByEmail(string $email): ?User;
    public function save(User $user): void;
    public function delete(int $id): void;
}

// 本番用:データベースを使う実装
class DatabaseUserRepository implements UserRepositoryInterface
{
    public function findById(int $id): ?User
    {
        // DBからユーザーを取得
    }
    // ...
}

// テスト用:メモリ上で動く実装
class InMemoryUserRepository implements UserRepositoryInterface
{
    private array $users = [];

    public function findById(int $id): ?User
    {
        return $this->users[$id] ?? null;
    }
    // ...
}

インターフェースで型を揃えることで、本番環境ではDB版、テストではメモリ版と差し替えられます。


まとめ

  • 抽象クラスは「共通実装+実装強制」が必要なとき
  • インターフェースは「能力の契約」だけが必要なとき
  • インターフェースは複数実装できるのが強み
  • どちらも直接インスタンス化できない
  • リポジトリパターンのようにインターフェースを活用すると差し替え可能な設計になる

次回は静的プロパティとメソッド——インスタンスなしで使えるクラス機能について学びます。