#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 class | interface |
|---|---|---|
| インスタンス化 | 不可 | 不可 |
| 実装の提供 | 可能 | 不可(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版、テストではメモリ版と差し替えられます。
まとめ
- 抽象クラスは「共通実装+実装強制」が必要なとき
- インターフェースは「能力の契約」だけが必要なとき
- インターフェースは複数実装できるのが強み
- どちらも直接インスタンス化できない
- リポジトリパターンのようにインターフェースを活用すると差し替え可能な設計になる
次回は静的プロパティとメソッド——インスタンスなしで使えるクラス機能について学びます。