抽象メソッドで実装を強制する
抽象メソッドは、本体を持たずサブクラスに実装を義務付ける仕組みです。共通処理を親にまとめつつ変わる部分だけを子に委ね、実装忘れを定義時点で弾けます。
abstractは「必ず実装しろ」という約束
抽象クラスと抽象メソッドは、共通処理を親にまとめつつ、一部だけをサブクラスに任せたいときに使う道具ですね。abstract を付けたメソッドは本体を書けず、継承したクラスに実装を義務付けます。実装し忘れるとクラスを定義した時点でエラーになるので、書き忘れをコンパイル時に近いタイミングで潰せるのが利点だと思います。
abstract class Shape
{
abstract public function area(): float;
public function describe(): string
{
return sprintf('面積は %.2f です', $this->area());
}
}
class Circle extends Shape
{
public function __construct(private float $r) {}
public function area(): float
{
return 3.14159 * $this->r ** 2;
}
}
describe() は親で共通化しつつ、area() だけはサブクラスに委ねています。この「決まっている部分」と「変わる部分」を分ける感覚が抽象クラスの肝ですね。
テンプレートメソッドパターン
処理の大枠は親で固定して、途中の一手だけを差し替える、というのがテンプレートメソッドと呼ばれる形です。手順の流れは崩したくないけれど、一部の振る舞いだけ切り替えたい、というときに素直にハマります。
abstract class ReportExporter
{
public function export(array $rows): string
{
$header = $this->header();
$body = implode("\n", array_map(fn($r) => $this->formatRow($r), $rows));
return $header . "\n" . $body;
}
abstract protected function header(): string;
abstract protected function formatRow(array $row): string;
}
export() の骨組みは親で固定し、header() と formatRow() だけをサブクラスが埋めます。CSV版とTSV版を作るなら、変える必要があるのはその二つだけで済むわけですね。
interfaceとの使い分け
「実装を強制する」だけならインターフェースでも足ります。抽象クラスを選ぶのは、共通の実装や状態を親に持たせたいときですね。逆に共有したい実装が何もないなら、インターフェースのほうが縛りが緩くて扱いやすいと思います。多重継承できないぶん、abstract は一つの系統に強く縛る覚悟が要る、という感覚で選ぶといいです。
// 実装を共有したい → abstract class
// 契約だけ定めたい → interface
interface Exportable
{
public function export(array $rows): string;
}
まとめ
抽象メソッドは、共通の流れを保ちながら差分だけをサブクラスに任せるための仕組みだと思います。実装忘れを早い段階で弾けるのが安心材料ですね。ただ継承は結合が強いので、共有する実装が本当にあるのかを一度立ち止まって考え、無いならインターフェースに寄せるくらいの温度感がちょうどいい気がします。
よくある質問
Q. 抽象メソッドを実装し忘れるとどうなりますか。
A. サブクラスを定義した時点でエラーになります。実装忘れを早い段階で弾けるのが利点です。
Q. 抽象クラスとインターフェースはどう選びますか。
A. 共通の実装や状態を親に持たせたいなら抽象クラス、契約だけ定めたいならインターフェースです。
Q. テンプレートメソッドとは何ですか。
A. 処理の大枠を親で固定し、途中の一手だけを抽象メソッドとしてサブクラスに差し替えさせる形のことです。