| この記事の要点 |
|
基本
abstract class Shape
{
public function __construct(protected string $label) {}
abstract public function area(): float; // 中身は書かない(; で終わる)
public function describe(): string // 通常のメソッドも持てる
{
return sprintf('%s の面積は %.2f', $this->label, $this->area());
}
}
class Circle extends Shape
{
public function __construct(private float $r)
{
parent::__construct('円');
}
public function area(): float // 実装が必須
{
return M_PI * $this->r ** 2;
}
}
echo (new Circle(2))->describe(); // 円 の面積は 12.57
// new Shape('図形');
// Fatal error: Cannot instantiate abstract class Shape
実装しないとエラーになる
class Broken extends Shape
{
// area() を実装していない
}
// Fatal error: Class Broken contains 1 abstract method and must therefore be
// declared abstract or implement the remaining methods (Shape::area)
このエラーはクラスを読み込んだ時点で出ます。実行してそのメソッドに到達するまで気づけない、ということがありません。これが抽象メソッドの最大の利点です。
// 中間クラスとして「まだ実装しない」ことは宣言できる
abstract class Polygon extends Shape
{
abstract public function sides(): int; // 抽象メソッドを追加してもよい
}
class Square extends Polygon
{
public function __construct(private float $side)
{
parent::__construct('正方形');
}
public function area(): float { return $this->side ** 2; }
public function sides(): int { return 4; }
}
書き方のルール
| できること | できないこと |
|---|---|
| 抽象メソッドと通常メソッドを混ぜる | 抽象メソッドに中身(波かっこ)を書く |
| プロパティ・定数を持つ | private abstract にする(子から実装できない) |
| コンストラクタを持つ | final abstract にする(矛盾している) |
| 抽象クラスを継承した抽象クラスを作る | 抽象メソッドが 1 つでもあれば new する |
| static な抽象メソッドを宣言する | — |
abstract class Sample
{
abstract public function a(): void; // OK
abstract protected function b(): void; // OK
// abstract private function c(): void; // Fatal error
// abstract public function d(): void {} // Fatal error: 中身を書けない
}
テンプレートメソッドパターン
抽象クラスの代表的な使い方です。処理の流れは親が決め、変わる部分だけ子に書かせます。
abstract class Importer
{
final public function run(string $path): int // 流れは固定(final)
{
$rows = $this->read($path);
$valid = array_filter($rows, fn($r) => $this->isValid($r));
foreach ($valid as $row) {
$this->save($row);
}
return count($valid);
}
abstract protected function read(string $path): array; // 形式ごとに違う
abstract protected function save(array $row): void; // 保存先ごとに違う
protected function isValid(array $row): bool // 既定の実装あり
{
return $row !== [];
}
}
class CsvImporter extends Importer
{
protected function read(string $path): array
{
$rows = [];
$fp = fopen($path, 'r');
while (($cols = fgetcsv($fp)) !== false) { $rows[] = $cols; }
fclose($fp);
return $rows;
}
protected function save(array $row): void
{
// DB に入れる
}
}
echo (new CsvImporter())->run('data.csv');
run() に final を付けると、順番を勝手に変えられません。共通の手順を守らせたいときに有効です。
インターフェースとの違い
| 抽象クラス | インターフェース | |
|---|---|---|
| 実装を持てる | 持てる | 持てない |
| プロパティを持てる | 持てる | 持てない(定数のみ) |
| コンストラクタ | 持てる | 宣言だけ可能 |
| 1 クラスがいくつ使えるか | 1 つだけ | いくつでも |
| メソッドの可視性 | public / protected | public のみ |
| 表すもの | 「〜の一種である」 | 「〜ができる」 |
interface Exportable
{
public function export(): string;
}
abstract class Report implements Exportable // 併用できる
{
public function __construct(protected array $rows) {}
protected function header(): string // 共通の実装は抽象クラスに
{
return '=== ' . static::class . ' ===';
}
}
class CsvReport extends Report
{
public function export(): string
{
return $this->header() . "\n"
. implode("\n", array_map(fn($r) => implode(',', $r), $this->rows));
}
}
function save(Exportable $e, string $path): void // 型はインターフェースで受ける
{
file_put_contents($path, $e->export());
}
受け取る側の型はインターフェースにしておくと、抽象クラスを継承していない実装も渡せるようになります。
トレイトとの使い分け
trait Loggable
{
protected function log(string $m): void { error_log($m); }
}
abstract class Job
{
use Loggable; // 実装を取り込む
abstract public function handle(): void;
}
| やりたいこと | 選ぶもの |
|---|---|
| 共通の手順を強制し、一部だけ差し替えさせたい | 抽象クラス |
| 「この操作ができる」ことを型で保証したい | インターフェース |
| 継承関係に関係なく同じ実装を配りたい | トレイト |
| 実装が 1 種類しかない | 普通のクラス(抽象化しない) |
実装が 1 つしかないうちは抽象クラスにしないでください。2 つ目が出てきて共通部分が見えた時点で切り出すほうが、正しい抽象になります。
実例: 支払い方法を差し替える
abstract class PaymentMethod
{
abstract public function name(): string;
abstract protected function charge(int $amount): string; // 決済 ID を返す
public function pay(int $amount): string
{
if ($amount <= 0) {
throw new InvalidArgumentException('金額が不正です');
}
$id = $this->charge($amount);
error_log("{$this->name()} で {$amount} 円を決済: {$id}");
return $id;
}
}
class CreditCard extends PaymentMethod
{
public function name(): string { return 'クレジットカード'; }
protected function charge(int $amount): string
{
return 'cc_' . bin2hex(random_bytes(4));
}
}
class BankTransfer extends PaymentMethod
{
public function name(): string { return '銀行振込'; }
protected function charge(int $amount): string
{
return 'bt_' . bin2hex(random_bytes(4));
}
}
function checkout(PaymentMethod $m, int $amount): void
{
echo $m->pay($amount);
}
checkout(new CreditCard(), 1000);
checkout(new BankTransfer(), 1000);
金額の検証とログ出力はどの支払い方法でも同じなので親に置き、実際の課金処理だけを子に書かせています。新しい支払い方法を足すときは PaymentMethod を継承するだけで、checkout() は変更不要です。
定数とインターフェース定数
abstract class Job
{
public const MAX_RETRY = 3; // 子からも self::MAX_RETRY で使える
abstract public function handle(): void;
}
interface HasStatus
{
public const OPEN = 'open'; // インターフェースにも定数を置ける
public const DONE = 'done';
}
class Task extends Job implements HasStatus
{
public function handle(): void
{
echo self::MAX_RETRY; // 3
echo self::OPEN; // open
}
}
状態を表す文字列の集合は、PHP 8.1 以降なら enum のほうが型安全です。
enum Status: string
{
case Open = 'open';
case Done = 'done';
public function label(): string
{
return match ($this) {
Status::Open => '未着手',
Status::Done => '完了',
};
}
}
echo Status::from('open')->label(); // 未着手
var_dump(Status::tryFrom('x')); // null(不正値でも例外にしない)
抽象クラスにしないほうがよい場合
- 実装が 1 つしかない — 2 つ目が出るまで普通のクラスでよい
- 共通部分が「たまたま同じ」だけ — 別々の理由で変わるものを 1 つにまとめると、片方の変更が他方を壊す
- 継承の階層が 3 段以上になる — どこに何が書かれているか追えなくなる。委譲かトレイトを検討する
- 子ごとに親のメソッドを空実装している — インターフェースを分けるべき合図
関連
- 文法 — 親カテゴリ
- 継承の構文
- オーバーライド
- ポリモーフィズム(多様性)の具体例
- カプセル化の具体例
- クラスとインスタンス
子ページ
子ページはありません
同階層のページ
人気ページ
- 1 Eclipseで「サーバーに追加または除去できるリソースがありません。」の原因と対処法
- 2 tomcat の起動 / 停止ログと catalina.log・catalina.out の違い
- 3 JavaScript で base URL を取得する方法|window.location.origin
- 4 YouTube Data API v3 エラー一覧|403・400・404 の原因と対処
- 5 Laravel エラー一覧|500/Blade/DB 接続/ルーティングの代表エラー
- 6 3Dグラフィックスとは|モデリング/レンダリング/主要ソフトウェア (Blender / Maya)
- 7 Spring Frameworkのアノテーション一覧
- 8 【Spring】@Valueアノテーションとは
- 9 CATALINA_HOME の確認方法 (Linux / Mac)
- 10 【Spring】@Autowiredアノテーションとは
最近更新/作成されたページ
- プロジェクトをTomcatプロジェクトとして認識させる方法 2026-10-07 22:32:50
- MySQLの1366 Incorrect string value|Laravelの文字コード・絵文字エラー 2026-10-07 21:54:03
- curlの証明書ホスト名不一致|旧エラー51・現行60の確認と対処 2026-10-07 21:54:03
- LaravelのMassAssignmentException|fillableの原因と安全な対処 2026-10-07 21:54:03
- Eclipse で Tomcat の起動ログがコンソールに出ない時の確認手順 2026-10-07 21:54:02
- MySQLにおける中央値(Median)の導き方(バージョン8未満) 2026-10-07 13:49:45
- getInputForward 2026-10-07 13:41:15
- JSONから配列に変換 2026-10-07 13:41:15
- ビューから値をモデルに格納しコントローラーで受け取る方法 2026-10-07 13:23:41
- Laravelのテーブル作成と定義変更|マイグレーション・up/down・注意点 2026-10-07 13:23:41
- NumPy 配列に要素を追加する方法 (append / concatenate) 2026-10-07 13:23:41
- MariaDB・MySQLで現在日時を取得する方法|NOW・タイムゾーン・保存型 2026-10-07 13:13:36
- 【django】テンプレートで定数を使用する方法 2026-10-07 13:10:15
- Spring BootにおけるApplication.propertiesの環境依存設定の分割方法 2026-10-07 12:09:35
- Not supported for DML operations【Springエラー】 2026-10-07 11:09:38