インターフェース vs 抽象クラス
CoddyのPHPジャーニー「オブジェクト指向プログラミング」セクションの一部 — レッスン 30/91。
インターフェースと抽象クラスの両方を学んだところで、それぞれをいつ使うべきかを明確にしましょう。これらは似ているように見えるかもしれませんが、目的が異なり、それぞれ独自の機能を持っています。
| 特徴 | インターフェース | 抽象クラス |
|---|---|---|
| メソッド | メソッドのシグネチャのみ | 抽象メソッドと具象メソッドの両方 |
| プロパティ | 定数のみ | プロパティを持つことができる |
| 多重継承 | クラスは複数を実装できる | クラスは1つだけ継承できる |
| コンストラクタ | 不可 | コンストラクタを持つことができる |
関連のないクラスが満たすべき契約を定義したい場合は、インターフェースを使用します。例えば、PrinterとLoggerは共通の祖先を持っていなくても、どちらもWritableインターフェースを実装する場合があります。
共通のコードを共有する関連クラスがある場合は、抽象クラスを使用します。Dog クラスと Cat クラスの両方が同じ eat() メソッドの実装を必要とする場合、抽象 Animal クラスを使用することで、そのコードを一度書くだけで済むようになります。
両方のアプローチを組み合わせることもできます。クラスは、複数のインターフェースを実装しながら、1つの抽象クラスを継承することができます:
<?php
abstract class Animal {
protected $name;
public function eat() {
return $this->name . " is eating";
}
}
interface Swimmable {
public function swim();
}
class Duck extends Animal implements Swimmable {
public function __construct($name) {
$this->name = $name;
}
public function swim() {
return $this->name . " is swimming";
}
}
重要なポイント: 関連のないクラス間で機能を定義する場合は、インターフェースを選択してください。関連するクラス間でコードを共有する必要がある場合は、抽象クラスを選択してください。
チャレンジ
簡単抽象クラスとインターフェースをいつ使い分けるか、そして両方のアプローチを効果的に組み合わせる方法を示す車両システムを構築しましょう。
継承を通じて共通のプロパティを共有しつつ、インターフェースを通じて追加の機能を得ることができるシステムを作成します。これは現実世界の設計判断を反映しています。例えば、車は車両の「一種である(is-a)」(継承)ですが、燃料を補給できるという「機能を持っている(has the capability)」(インターフェース)でもあります。
コードを4つのファイルに分けて整理してください:
Refuelable.php—refuel($amount)という単一のメソッドシグネチャを持つRefuelableインターフェースを定義します。この機能は、継承階層に関係なく、燃料補給が必要なあらゆるクラスに適用できます。Vehicle.php— すべての車両で共有される機能を提供する抽象クラスVehicleを定義します。保護された(protected)$brandプロパティと、保護された$fuelLevelプロパティ(初期値は 0)を持つ必要があります。コンストラクタはブランド名を受け取ります。ブランドを返す具体的なgetBrand()メソッド、現在の燃料レベルを返す具体的なgetFuelLevel()メソッド、および子クラスが実装しなければならない抽象メソッドdrive()を含めてください。Car.php—Vehicleを継承し、Refuelableを実装するCarクラスを作成します。冒頭で必要な2つのファイルをインクルードしてください。コンストラクタはブランドを受け取り、それを親のコンストラクタに渡します。drive()を実装して"[brand] car is driving"を返すようにします。refuel($amount)を実装して、燃料レベルにその量を追加し、"[brand] refueled to [fuelLevel] liters"を返すようにします。main.php— Carファイルをインクルードします。ブランド名と燃料の量(整数に変換)の2つの入力を受け取ります。そのブランドでCarインスタンスを作成します。1行目にdrive()の結果を出力し、2行目に燃料の量を引数に指定したrefuel()の結果を出力してください。
このチャレンジは、重要な違いを説明しています。抽象クラス Vehicle はコードを共有し「is-a」関係を確立しますが、Refuelable インターフェースは、車両である必要のない他の無関係なクラス(発電機やボートなど)にも適用できる機能を追加します。
チートシート
インターフェースと抽象クラスは、それぞれ異なる目的で使用されます:
| 機能 | インターフェース | 抽象クラス |
|---|---|---|
| メソッド | メソッドのシグネチャのみ | 抽象メソッドと具体的なメソッドの両方 |
| プロパティ | 定数のみ | プロパティを持つことができる |
| 多重継承 | 1つのクラスで複数を実装できる | 1つのクラスで1つのみ継承できる |
| コンストラクタ | 不可 | 持つことができる |
関連のないクラスに対して契約を定義するにはインターフェースを使用します。関連のあるクラス間で共通のコードを共有するには抽象クラスを使用します。
クラスは、複数のインターフェースを実装しながら、1つの抽象クラスを継承することができます:
<?php
abstract class Animal {
protected $name;
public function eat() {
return $this->name . " is eating";
}
}
interface Swimmable {
public function swim();
}
class Duck extends Animal implements Swimmable {
public function __construct($name) {
$this->name = $name;
}
public function swim() {
return $this->name . " is swimming";
}
}
自分で試してみよう
<?php
require_once 'Car.php';
// 入力を読み込む
$brand = trim(fgets(STDIN));
$fuelAmount = intval(trim(fgets(STDIN)));
// TODO: ブランドを指定してCarを作成する
// TODO: 1行目にdrive()の結果を出力する
// TODO: 2行目に指定された燃料量でrefuel()の結果を出力する
?>このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。