インターフェースと抽象クラスの使い分けをis-aとcan-doで考える
インターフェースはメソッドの契約だけを定める型、抽象クラスは共通の実装を持ちつつ一部を子に強制する型です。can-doかis-aかで使い分けます。
実装を持てるかどうかが最大の違い
インターフェースと抽象クラス、どちらも「継承先に実装を強制する」点は似ていますが、いちばんの違いは実装コードを持てるかどうかですね。抽象クラスは具体的なメソッドやプロパティを持てるのに対し、インターフェースは基本的にメソッドの署名(契約)だけを定めます。
interface Exportable {
public function toArray(): array; // 契約だけ
}
abstract class Report {
protected string $title = '';
// 共通の実装を持てる
public function header(): string {
return '# ' . $this->title;
}
abstract public function body(): string; // 実装は子に強制
}
is-a なら継承、can-do ならインターフェース
使い分けの目安として、私は「is-a(〜である)関係なら抽象クラス、can-do(〜できる)能力ならインターフェース」と考えるようにしています。PDF レポートは「レポートである」から Report を継承し、「配列に変換できる」という能力は Exportable インターフェースで表す、という具合ですね。
class PdfReport extends Report implements Exportable {
public function body(): string { return '本文'; }
public function toArray(): array {
return ['title' => $this->title, 'body' => $this->body()];
}
}
抽象クラスは一つしか継承できませんが、インターフェースは複数実装できます。だから「能力の付与」は柔軟にインターフェースで重ねられるわけですね。
迷ったらインターフェースを優先
設計に迷ったら、まずインターフェースで契約を切っておくほうが後から拡張しやすいと思います。共通実装が欲しくなったら、そのインターフェースを実装する抽象クラスを間に挟む、という段階的な進め方ができます。型宣言もインターフェースにしておくと、差し替えが効いてテストも書きやすいですね。
function save(Exportable $item): void {
$data = $item->toArray(); // 実装が何であれ契約に従う
// 保存処理...
}
まとめ
実装コードの共有が要るなら抽象クラス、能力の契約だけを定めたいならインターフェース、という軸で考えると迷いにくいと思います。is-a か can-do か、そして複数の能力を重ねたいか、あたりを判断材料にするといいですね。個人的には、まずインターフェースで疎結合に切っておいて、共通実装が必要になった時点で抽象クラスを足す進め方が扱いやすいと感じています。
よくある質問
Q. 一番の違いは何ですか?
A. 実装コードを持てるかどうかです。抽象クラスは具体的な実装を持てますが、インターフェースは原則メソッドの署名だけを定めます。
Q. 迷ったらどちらを選ぶべきですか?
A. まずインターフェースで契約を切っておくと後から拡張しやすいです。共通実装が欲しくなったら抽象クラスを間に挟めます。
Q. 複数の型を組み合わせられますか?
A. 抽象クラスの継承は一つだけですが、インターフェースは複数実装できるので、能力を重ねたいときに向いています。