Kodokon kodokon.com

継承、インターフェース、トレイト、そして抽象クラス

それぞれのトレードオフを知ったうえで、正しい設計の道具を選びましょう。契約、アルゴリズムの骨組み、あるいは水平的な再利用のどれかです。

9 分 · 3 問

このレッスンを Kodokon で開く

PHPには、一部が重なり合う四つの設計の道具があります。具象クラスの継承、抽象クラス、インターフェース、そしてトレイトです。選択はひとつの問いに行き着きます。あなたは何を共有したいのか、です。契約ならインターフェース。契約に実装の骨組みを加えたものなら抽象クラス。階層のつながりがないクラスどうしで共有するコードならトレイトです。一方で具象クラスの継承は、正当化するのがますます難しくなっています。強く結合してしまいますし、PHPでは親を一つしか持てないからです。

PHP
<?php

declare(strict_types=1);

abstract class Exporter
{
    public function export(array $rows): string
    {
        return $this->head() . $this->body($rows);
    }

    abstract protected function head(): string;

    abstract protected function body(
        array $rows
    ): string;
}

final class CsvExporter extends Exporter
{
    protected function head(): string
    {
        return "id;name\n";
    }

    protected function body(array $rows): string
    {
        $lines = array_map(
            fn (array $r): string => implode(';', $r),
            $rows,
        );

        return implode("\n", $lines);
    }
}

$csv = new CsvExporter();
echo $csv->export([[1, 'Ada'], [2, 'Linus']]);
テンプレートメソッド:親がアルゴリズムを定め、子が細部を与える

インターフェースは純粋な契約です。シグネチャと定数だけで、状態も実装も持ちません。抽象クラスに対する決定的な利点は、一つのクラスがインターフェースを複数実装できることと、既存のどんなクラスもあとから契約を採り入れられることです。対照的に抽象クラスは、状態と共有コードを運べますが、たった一つしかない継承の枠を消費してしまいます。実務では、引数の型をインターフェースで指定しましょう(ArrayCacheではなくCache)。そうすれば、テストでも本番でも実装を差し替えられます。

PHP
<?php

declare(strict_types=1);

interface Cache
{
    public function get(string $key): ?string;

    public function set(
        string $key,
        string $value
    ): void;
}

final class ArrayCache implements Cache
{
    private array $items = [];

    public function get(string $key): ?string
    {
        return $this->items[$key] ?? null;
    }

    public function set(
        string $key,
        string $value
    ): void {
        $this->items[$key] = $value;
    }
}

$cache = new ArrayCache();
$cache->set('lang', 'PHP');
echo $cache->get('lang');
クライアントのコードはCacheに依存する:実装は差し替え可能なまま

トレイトは水平的な共有を解決します。共通の祖先を持たないクラスに、同じメソッドを注入するのです。二つのトレイトが同じ名前のメソッドを提供すると、PHPはあなたのために選ぶことを拒み、明示的な解決を求めます。insteadofは残すバージョンを指定し、asはもう一方をアクセス可能に保つためのエイリアスを作ります。これは意図的に冗長です。衝突はコードの中で見えたままでなければならないからです。

PHP
<?php

declare(strict_types=1);

trait FileLogger
{
    public function log(string $message): void
    {
        echo "[file] $message\n";
    }
}

trait AuditLogger
{
    public function log(string $message): void
    {
        echo "[audit] $message\n";
    }
}

final class PaymentService
{
    use FileLogger, AuditLogger {
        FileLogger::log insteadof AuditLogger;
        AuditLogger::log as audit;
    }
}

$service = new PaymentService();
$service->log('Payment received');
$service->audit('Payment received');
トレイトの衝突:insteadofが決め、asがエイリアスを残す

理解度チェック

このレッスンの要点をしっかり覚えているか確認しましょう。

  1. インターフェースが抽象クラスに対して持つ、決定的な利点は何ですか?
    • 共有されるプロパティを持てる
    • 一つのクラスは複数のインターフェースを実装できるが、継承できるクラスは一つだけ
    • そのメソッドはより速く実行される
  2. 同じクラスで使われる二つのトレイトが、それぞれlog()メソッドを宣言しています。何が起きますか?
    • PHPは最初に並べたトレイトのメソッドを使う
    • insteadofで明示的に解決しない限り、致命的エラー
    • PHPは二つのメソッドを一つに統合する
  3. 抽象クラスが持てて、インターフェースが持てないものは何ですか?
    • 定数
    • プロパティと実装済みのメソッド
    • publicなメソッドのシグネチャ