PHP をある程度深くやってるとインターフェイス (interface) っていうのが出てきますよね。アレって何なのかご存知でしょうか?実はすごく頼れるやつなんですよ。アプリケーションの設計をするときはもちろん、いろいろなライブラリを組み込むときにもインターフェイスを使うことのメリットを知っていると知らないとではプロダクトの質に大きな違いが出てきます。
本稿では PHP のインターフェイスの使い方とメリットを物語形式で解説します。
本稿の対象読者
- PHP のクラスは理解しているけどインターフェイスについてはよくわからない
- PHP のインターフェイスの仕組みは理解しているけど使い所がわからない、あるいは何のためにあるのかわからない
- PHP のインターフェイスは実際に使っているけどそのメリットについて上手く説明できない
- イ、インターフェイス!?もももちろん知ってるぞ、インター・フェイスだろ?いんたーふぇいすぅ…
上記いずれかの悩みをもつ PHP プログラマーの方々
※クラスがまだわからない人はそちらを先に勉強しましょう。クラスについての解説はインターネットにわかりやすい記事がたくさんあると思います。
ある町工場のインターフェイス物語
あなたはおもちゃのロボットを製造しています
あなたは小さな町工場の従業員です。この工場では毎日子供向けのおもちゃをいくつか製造しており、その中でも マシンガンパンチロボット は年頃の男の子に大変人気のある商品で、会社の主要な収益源となっています。
このロボットには Blue 社製の (充電可能な) バッテリーが内蔵されており、ロボットの製造には Blue 社からの電池の供給が不可欠となっています。
このロボットの設計は次のように書かれています。
class Robot
{
// バッテリー
protected $battery;
// 蓄電量
protected $storage = 0;
public function __construct(BlueBattery $battery)
{
$this->battery = $battery;
}
// バッテリーを使って充電する
public function charge(int $minutes)
{
$this->storage = $this->battery->charge($minutes);
}
// ためたパワーを使ってパンチする
// 終わったら蓄電量は0になる
public function punch()
{
for ($i = 0; $i < $this->storage; $i++) {
echo 'オラ';
}
echo 'ァ!', PHP_EOL;
$this->storage = 0;
}
}
Blue 社のバッテリー仕様は下記のように書かれています。 charge()
によって1分あたり1だけ充電されるということです。
class BlueBattery
{
public function charge(int $minutes): int
{
return 1 * $minutes;
}
}
これを動作させるコードは下記のようになります。
$battery = new BlueBattery;
$robot = new Robot($battery);
$robot->charge(10);
$robot->punch();
// オラオラオラオラオラオラオラオラオラオラァ!
競合製品登場!
人気ロボット製造であぐらをかいていたのも束の間、なんとライバル会社がパクリ製品をリリースしました。しかもただのパクリ製品ではなく、バッテリーを10倍強化したロボットではありませんか!またたく間にあなたの会社の製品の購買層はライバル会社に流れ、売上は一気に悪化しました。
このままではまずいと判断したあなたの元請け会社の社長が「相手が10倍ならこっちは100倍だ!」と鶴の一声を上げ、100倍の蓄電性能をもつ Green 社のバッテリーを使うように急遽指示されました。
やれ現場は大混乱。これまで Blue 社のバッテリーが使われることを想定してロボット設計していたので、ロボットの設計を見直さなければなりません。問題となっている箇所はここです:
public function __construct(BlueBattery $battery)
{
$this->battery = $battery;
}
これを Green 社のバッテリーに置き換える場合は下記のようになるでしょう:
public function __construct(GreenBattery $battery)
{// ^^^^^^^^^^^^ ココを書き換える!
$this->battery = $battery;
}
幸いなことに GreenBattery
は BlueBattery
と同名のメソッド、同じ型の引数、同じ型の返り値を持っているのでそれ以外のところで Robot
を修正する必要はありません。
class GreenBattery
{
public function charge(int $minutes): int
{
return 100 * $minutes; // 100倍性能 ⭐️
}
}
しかし、 GreenBattery
を搭載するようにするとコンストラクタを書き換えるので、今までの Robot
の設計は使えません。今後も安価な BlueBattery
を搭載したロボットは販売し続けたいので、新しくロボットを設計するしかないのでしょうか…?
バッテリーの規格を決めよう
1週間悩んだ末、あなたは画期的なアイデアをひらめきました!バッテリーを規格化するのです。その規格に沿っていればどんなバッテリーもロボットに搭載できるようになります。 BlueBattery
でも GreenBattery
でも YellowBattery
でも!そこで早速規格の仕様書を書きました。それこそがインターフェイスです:
interface Battery
{
public function charge(int $minutes): int;
}
これは 「 Battery
というインターフェイスを実装するのであれば、それは必ず charge(int): int
というメソッドを持つ」という規約です。別の言い方をすれば、
Battery
インターフェイスを実装しているクラスは charge(int): int
を持っていることが保証されている
ということになります。インターフェイスに書かれるのは外部から呼ばれるメソッドの定義 (そのメソッドでは何が入力され、何が出力されるか) のみです (したがって public のみ宣言可) 。内部でどんな処理をするかは、インターフェイスを実装したクラスに委ねられます。
BlueBattery
と GreenBattery
はどちらもこのインターフェイスに従って実装されているので、下記のように書き換えられます。
class BlueBattery implements Battery
{
public function charge(int $minutes): int
{
return 1 * $minutes;
}
}
class GreenBattery implements Battery
{
public function charge(int $minutes): int
{
return 100 * $minutes;
}
}
新たに登場した implements Battery
というキーワードは、 Battery
インターフェイスを実装しているよ、という意味です。
Robot
のコンストラクタ引数の型には Battery
を書きます。これで Battery
インターフェイスを実装したクラスのオブジェクトは何でも渡すことができるようになりますね!
public function __construct(Battery $battery)
{
$this->battery = $battery;
}
動作確認をしてみましょう:
// Blue 社バッテリーを搭載した旧ロボット
$blue_battery = new BlueBattery;
$blue_robot = new Robot($blue_battery);
$blue_robot->charge(10);
$blue_robot->punch();
// オラオラオラオラオラオラオラオラオラオラァ!
// Green 社バッテリーを搭載した新ロボット
$green_battery = new GreenBattery;
$green_robot = new Robot($green_battery);
$green_robot->charge(10);
$green_robot->punch();
// オラオラオラオラオラオラオラオラオラオラオラオラオラオラオラオラオラオラオラオラオラオラオラ (略)
ここまでの流れを見て、インターフェイスって便利だと思いましたか?え?インターフェイスなんて面倒な縛りはなくして、どんな型でも受け入れられるようにすればいいんじゃないかって?つまり:
public function __construct($battery) // どんなバッテリーもオッケー👌
{
$this->battery = $battery;
}
こうしろ、ということでしょうか?本当にそれで大丈夫なのでしょうか?次の章まで読み進めて考え直してみてください。
規格外のバッテリー!
あなたのおかげで、100倍性能のバッテリーをもつロボットを無事市場に流通させ、さらに多様なバッテリーに対応させることによって売上を回復させ、バッテリー業界の中でもそれなりの存在の会社となりました。
そんなところに新たなニュースが飛び込んできました。なんと Red 社がこれまでのバッテリーの常識を覆す大容量バッテリーを発表したのです!その容量はなんと Blue 社製の1000倍! (しかも比較的安価!) なんとしてもこのバッテリーを仕入れてロボットの性能をアップさせたいところです!
しかしこの新しく発表されたバッテリーには致命的な問題がひとつありました。それは、 Battery
インターフェイスの仕様に沿っていない、規格外バッテリーだったのです:
class RedBattery
{
public function supercharge(int $minutes, int $max): int
{
return min(1000 * $minutes, $max);
}
}
メソッド名は charge
ではなく supercharge
だし、何か引数も追加されています。 $max
というのは蓄電量を制御できるオプションだそうです (いらない)。こんなトンデモバッテリーにあなたは怒りを通り越して呆れてしまいます。それでも何とかして対応しないと先にライバル社の製品がこれを搭載して盛り返してくるでしょう。
インターフェイスの縛りをなくして、 supercharge()
を使うようにしましょうか?もちろん、これまでのロボットとの互換性を保たなければならないので Robot::charge()
メソッドには分岐処理が必要です:
class Robot
{
// バッテリー
protected $battery;
// 蓄電量
protected $storage = 0;
public function __construct($battery) // どんなバッテリーも受け入れる
{
$this->battery = $battery;
}
// バッテリーを使って充電する
public function charge(int $minutes): void
{
// 🔥Red 社のバッテリーは supercharge メソッドを使う
if ($this->battery instanceof RedBattery) {
$this->storage = $this->battery->supercharge($minutes, 10000);
return;
}
// それ以外の仕様通りのバッテリーは charge メソッドを使う
$this->storage = $this->battery->charge($minutes);
}
public function punch()
{
// 省略
}
}
これはもうすでにヤバいコードになりつつあります。何がヤバいかと言うと、今後も規格外のバッテリーを受け入れるたびに条件分岐が増えていくということです。分岐が増えれば当然コードは読みづらくなり、分岐ごとに正しさを検証しなければならないのでメンテナンスコストがかかり、そして最悪バグを生み出す源になることでしょう。インターフェイスは各社のバッテリーへの依存を減らし、依存による複雑さを排除する仕組みでもあったのですね。
アダプターを挟め!
バッテリー業界の大御所である Red 社に対して田舎の町工場が仕様どおりのものを作ってくれと口を挟むことは不可能です。何とかしてこちら側で対応するしかないのです。
(ロボット製造の話から逸れますが) プログラミングの世界にはたくさんのライブラリがあり、たくさんの製品が外部のライブラリに依存しています。なので自分のところが独自に開発しているアプリケーションの仕様にだけ合わせてくれとライブラリの開発者に要望を送るのはおかしな話です。
あなたが工場の仕事から帰宅し、パソコンの前で悩んでいると、パソコンに繋がれている USB Type-C と USB Type-A の変換アダプターにふと目が行きました。「これだ!」とあなたはその場でひらめいた設計をパソコンで書き出し、次の朝上司に見せました。上司も納得し、 Battery
を実装した RedBatteryAdapter
を搭載することになりました:
class RedBatteryAdapter implements Battery
{
public function __construct(int $max)
{
$this->max = $max;
$this->battery = new RedBattery;
}
public function charge(int $minutes): int
{
return $this->battery->supercharge($minutes, $this->max);
}
}
このように仕様が合致しないインターフェイスとクラスの中間にアダプタークラスをかませる (実際にはアダプタークラスに対象のクラスを内包させる) ことによって間接的にインターフェイスを実装できます。このテクニックはライブラリの抽象化でよく用いられる手法なので知っておいて損はないでしょう。
では、動作確認をしてみましょう:
// Red 社バッテリーを搭載した最強ロボット
$red_battery = new RedBatteryAdapter(10000);
$red_robot = new Robot($red_battery);
$red_robot->charge(10);
$red_robot->punch();
// オラオラオラオラオラオラオラオラオラオラオラオラオラオラオラオラオラオラオラオラオラオラオラ (長過ぎるので略)
この結果あなたの会社はさらに売上を大きく伸ばし、またあらゆるバッテリーに対応した製品を出せるということで業界では大きな存在となりました。
もしインターフェイスがなかったら、もしその上たくさんの種類のバッテリー規格に対応するとしたらどんな状況になっていたでしょうか?最後にそのことについて考えてみましょう。
インターフェイスは実装を教えてくれない
ここまでの完全なコードをまず載せておきます。仕様を統一させるため、 BlueBattery
も GreenBattery
もアダプターを通すようにし、 Battery
インターフェイスは BatteryAdapter
としました。
<?php
class Robot
{
// バッテリー
protected $battery;
// 蓄電量
protected $storage = 0;
public function __construct(BatteryAdapter $battery)
{
$this->battery = $battery;
}
// バッテリーを使って充電する
public function charge(int $minutes): void
{
$this->storage = $this->battery->charge($minutes);
}
// ためたパワーを使ってパンチする
// 終わったら蓄電量は0になる
public function punch()
{
for ($i = 0; $i < $this->storage; $i++) {
echo 'オラ';
}
echo 'ァ!', PHP_EOL;
$this->storage = 0;
}
}
class BlueBattery
{
public function charge(int $minutes): int
{
return 1 * $minutes;
}
}
class BlueBatteryAdapter implements BatteryAdapter
{
public function __construct()
{
$this->battery = new BlueBattery;
}
public function charge(int $minutes): int
{
return $this->battery->charge($minutes);
}
}
class GreenBattery
{
public function charge(int $minutes): int
{
return 100 * $minutes;
}
}
class GreenBatteryAdapter implements BatteryAdapter
{
public function __construct()
{
$this->battery = new GreenBattery;
}
public function charge(int $minutes): int
{
return $this->battery->charge($minutes);
}
}
class RedBattery
{
public function supercharge(int $minutes, int $max): int
{
return max(1000 * $minutes, $max);
}
}
class RedBatteryAdapter implements BatteryAdapter
{
public function __construct(int $max)
{
$this->max = $max;
$this->battery = new RedBattery;
}
public function charge(int $minutes): int
{
return $this->battery->supercharge($minutes, $this->max);
}
}
interface BatteryAdapter
{
public function charge(int $minutes): int;
}
// Blue 社バッテリーを搭載した旧ロボット
$blue_battery = new BlueBatteryAdapter;
$robot = new Robot($blue_battery);
$robot->charge(10);
$robot->punch();
// オラオラオラオラオラオラオラオラオラオラァ!
// Green 社バッテリーを搭載した新ロボット
$green_battery = new GreenBatteryAdapter;
$green_robot = new Robot($green_battery);
$green_robot->charge(10);
$green_robot->punch();
// オラオラオラオラオラオラオラオラオラオラオラオラオラオラオラオラオラオラオラオラオラオラオラ (略)
// Red 社バッテリーを搭載した最強ロボット
$red_battery = new RedBatteryAdapter(10000);
$red_robot = new Robot($red_battery);
$red_robot->charge(10);
$red_robot->punch();
// オラオラオラオラオラオラオラオラオラオラオラオラオラオラオラオラオラオラオラオラオラオラオラ (略)
注目すべきは Robot
クラスの charge
メソッドです。
// バッテリーを使って充電する
public function charge(int $minutes): void
{
$this->storage = $this->battery->charge($minutes);
}
この処理はバッテリーを使うので本来はバッテリーの処理に依存します。つまりバッテリーに変更があったら、 Robot
に影響が出る可能性があります。しかしこのバッテリーはインターフェイスで抽象化されているため、その心配はありません。インターフェイスに実装は書かれていないのですから。Robot クラスはインターフェイスによって保護されているのです。Robot クラスは各バッテリーの実装を知ることはできないのでどんなにバッテリーの種類があっても責任が増えることはありません。プログラミングの設計の世界では関心事が減ることは美徳なのです。「無知は罪」ではなく「知らぬが仏」です。
そうなればアプリケーションのテストも単純になります。例えば極端な話、 Robot
クラスが100種類のバッテリーに直接依存していたら最低でも100種類の結合テストケースを用意しないといけません。これはアプリケーションが大きくなればなるほど負債が増えていくことを意味します。しかしインターフェイスによってバッテリーに対する入力と出力の型が保証されているのであれば、バッテリーがいくら増えても、各バッテリーにどのような変更があっても、インターフェイスが変更されない限りは Robot
に対するテストを変更する必要はないのです (ただし、各バッテリーの単体テストは必要) 。
まとめ
- インターフェイスは規格の仕様書のようなもの
- インターフェイスから実装の中身を知ることはできないが、それが最大のメリットである
- クラスはインターフェイスを実装できる
- インターフェイスを実装したクラスはそのインターフェイスの型として認められる
- インターフェイスを実装したクラスから作られたオブジェクト (インスタンス) には、インターフェイスに定義されているとおりのメソッドをもつことが保証されている
- インターフェイスを通じて別のクラスXにアクセスできるが、その場合クラスXに直接依存しないので、クラスXが変わってもアクセス元は影響を受けない (クラスXを監視する責任を負わない)
終わりに
インターフェイスは Java や Go などでも言語レベルで備わっている仕組みで PHP だけのものではありませんし、その魅力はここでは語りきれないほどまだたくさんあります。本稿を読んでみてなんとなくわかったら、自分が実際に書いているコードでも応用できそうなところがないか探してみてください。また Laravel のようなフレームワークを使っているとインターフェイスが出てくる場面も多いかと思うので、そういった有名どころのソースコードなども参考にしながらぜひ理解を深めてみてください!