それぞれのトレードオフを知ったうえで、正しい設計の道具を選びましょう。契約、アルゴリズムの骨組み、あるいは水平的な再利用のどれかです。
このレッスンを Kodokon で開く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
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');トレイトは水平的な共有を解決します。共通の祖先を持たないクラスに、同じメソッドを注入するのです。二つのトレイトが同じ名前のメソッドを提供すると、PHPはあなたのために選ぶことを拒み、明示的な解決を求めます。insteadofは残すバージョンを指定し、asはもう一方をアクセス可能に保つためのエイリアスを作ります。これは意図的に冗長です。衝突はコードの中で見えたままでなければならないからです。
<?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');log()メソッドを宣言しています。何が起きますか?insteadofで明示的に解決しない限り、致命的エラー