「クラスはなんとなくわかるけどインターフェイスが良くわからない。」
「インターフェイスの名前は知っているけどいつ使うのかがわからない。」という方向けに
とりあえずオブジェクト指向とか難しそうな話は置いといて、
インターフェイスがどういうものなのかを、ざっくり、自分なりに説明してみました。
広義のインターフェイスとは
インターフェース-Wikipedia
インターフェイス (interface)-わわわIT用語辞典
インターフェイス
とは、おおよそ「界面、接触面、中間面」という意味があるようです。
ユーザーインターフェイス
を例にあげると、
例えばPCを使用している場合であればマウスやキーボードもユーザーインターフェイス
にあたります。
Webアプリケーションを使う場合にはPCやスマホの画面がそれにあたるでしょう。
オブジェクトインターフェイス
プログラミングの世界でインターフェイスといえばそれはおそらくオブジェクトインターフェイスでしょう。
上記の説明を使うならオブジェクト(クラス)とそれを使う人の接触面であるといえると言えるでしょう。
WEBアプリケーションの例で考えると、クラスがアプリケーションの中身(機能)だとするとオブジェクトインターフェイスはアプリケーションの画面ですね。
オブジェクトインターフェイスに記述されているメソッド宣言はさしずめ画面に表示されているボタンといったところでしょうか。
WEBアプリケーションの中身をしらなくても画面を見ればアプリケーションが操作できるように、クラスの中身を知らなくてもオブジェクトインターフェイスを見ればクラスの使い方がわかるのが理想ではないでしょうか。
(以下、オブジェクトインターフェイスをインターフェイスと称します。)
インターフェイスを書いてみる
メッセージをメールで送信するクラスに対してインターフェイスを書いてみます。
このクラスの仕様は
1.クラスを使用する側からメッセージを受け取る。
2.固定の送信先にメッセージを送信する。
の単純なものとします。
まずはインターフェイスを使用しないコードです。
class MailNotifier
{
public function メッセージをセットする(string $message)
{
}
public function メールを送信する()
{
// メールの送信処理
}
public function メールのヘッダを作成する()
{
}
public function メールの署名を作成する()
{
}
// その他、メール送信に必要な処理いろいろ
}
// 使用する側
class Messenger
{
public function 通知を送る(MailNotifier $mailNotifier)
{
$mailNotifier->メッセージをセットする('こんにちは');
$mailNotifier->メールを送信する();
}
}
このMailNotifierクラスにInterfaceを書いてみます。
使う側で必要となるメソッド(Webアプリケーション風にいうとユーザー側に必要なボタン)は
・メッセージをセットする
・メールを送信する
クラスを使う側はそれ以上のメソッドを使う必要も存在を知る必要はありません。
そのため、クラスを使う側で必要となる上記の2点のメソッドをインターフェイスに記述します。
interface Notification
{
public function メッセージをセットする(string $message);
public function メールを送信する();
}
今回の仕様では上記の2点以外のメソッドはクラスを使う側からは触ってほしくありません。
(ユーザーに操作されたくない機能のボタンは実装しませんよね?)
ですので、触ってほしくないメソッドはインターフェイスには記述しません。
ただ、インターフェイスに記述していなくても実際には呼び出せてしまうので、
呼び出してほしくないメソッドはprivate
もしくはprotected
にしておきます。
interface Notification
{
public function メッセージをセットする(string $message);
public function メールを送信する();
}
class MailNotifier implements Notification
{
public function メッセージをセットする(string $message)
{
}
public function メールを送信する()
{
// メールの送信処理
}
private function メールのヘッダを作成する()
{
}
private function メールの署名を作成する()
{
}
// その他、メール送信に必要な処理いろいろ
}
// 使用する側
// インターフェイスを引数で受け取る
public function 通知を送る(Notification $notification)
{
$notification->メッセージをセットする('こんにちは');
$notification->メールを送信する();
}
以上がインターフェイスのざっくりとした説明になります。
そうはいってもすべてのクラスにインターフェイスをつけるべきだという話ではありません。
今回の例のようなインターフェイスを実装するクラスが一つしかない場合だと(外から触られたくないメソッドをprivateにする処理は必要として)インターフェイスを追加するメリットはあまりないでしょう。
では、インターフェイスを書くメリットはなんなのでしょうか?
インターフェイスのメリットそのインターフェイスを実装したクラスに制約を持たせることができることにあります。
インターフェイスを実装するには、implements 演算子を使用し、 このインターフェイスに含まれる全てのメソッドを実装する必要があります。 実装されていない場合、致命的エラーとなります
このインターフェイスに含まれる全てのメソッドを実装する必要があります。
これにどのようなメリットがあるか、具体的な例で説明します。
(下記の例は正確にはインターフェイスを利用したポリフォーフィズムのメリット)
インターフェイスのメリット
今まではメールでメッセージを送信するだけでしたが、状況に応じてSlackでもメッセージの通知をできるようにしたくなったとします。
上記で記述したコードにSlackでの通知の処理を追記します。
interface Notification
{
public function メッセージをセットする(string $message);
public function メールを送信する();
}
class MailNotifier implements Notification
{
public function メッセージをセットする(string $message)
{
}
public function メールを送信する()
{
// メールの送信処理
}
private function メールのヘッダを作成する()
{
}
private function メールの署名を作成する()
{
}
// その他、メール送信に必要な処理いろいろ
}
// MailNotifierクラスと同じようにNotificationインターフェイスを実装する
class SlackNotifier implements Notification
{
public function メッセージをセットする(string $message)
{
}
public function メールを送信する()
{
// メールの送信処理
}
// その他Slackでのメッセージ送信に必要な処理いろいろ
private function API通信を行う()
{
}
}
// 使用する側
public function 通知を送る(Notification $notification)
{
$notification->メッセージをセットする('こんにちは');
$notification->メールを送信する();
}
使用する側のコードをクラスに合わせて変更する必要がない
Notificationを使用する側のメソッドをみてください。
// 使用する側
public function 通知を送る(Notification $notification)
{
$notification->メッセージをセットする('こんにちは');
$notification->メールを送信する();
}
Notificationインターフェイスをメソッドの引数で型指定をして受け取っています。
これにより、このメソッド引数は確実にNotificationインターフェイスを実装しているクラスのオブジェクトとである。ということになります。
(Notificationインターフェイスを実装していないクラスのオブジェクトやプリミティブ型のデータが渡された場合はエラーになるので)
インターフェイスを実装しているクラスでは そのインターフェイスで定義されているメソッドとまったく同じシグネチャを持つメソッドが保証されます。(同じメソッドがないとエラーになるので)
この場合ではメッセージをセットする(string $message)
メソッドとメールを送信する()
メソッドが必ず存在しています。
つまり、使用する側では引数で渡された変数がMailNotifier
クラスのオブジェクトだろうが、SlackNotifier
クラスのオブジェクトだろうがまったく記述を変えることなく扱うことができます。
機能を追加しても既存のコードの変更を最小限にできる
Slackの通知以外にも、新たにTwitterのDMで通知を送りたくなったとしましょう。
そうすると新たにTwitterDMNotification
クラスを実装することになると思います。
class TwitterDMNotification implements Notification
{
public function メッセージをセットする(string $message)
{
}
public function メールを送信する()
{
// メールの送信処理
}
// その他Twitterでのメッセージ送信に必要な処理いろいろ
private function TwitterのAPI通信を行う()
{
}
}
先ほど説明したように、'TwitterDMNotification'クラスも'Notification'インターフェイスを実装しているので、使用する側のメソッドは変更する必要がありません。
もちろん、MailNotifier
クラスもSlackNotifier
クラスも変更する必要はありません。
このように既存のコードの修正を最小限にして新しい機能を追加することができます。
しかし、今までの例では記述していませんでしたが、実際には使用する側のメソッドに引数を渡して呼び出す処理が必要です。
例えば、下記のような
class NotificationGetting
{
private $notificationType
public function __construct(string $notificationType)
{
$this->notificationType = $notificationType;
}
public function getNotification(): Notification
{
if ($this->notificationType === 'Mail') {
return new MailNotifier();
} else if ($this->notificationType === 'Slack') {
return new SlackNotifier();
}
}
}
// 使用する側
public function 通知を送る(Notification $notification)
{
$notification->メッセージをセットする('こんにちは');
$notification->メールを送信する();
}
$getting = new NotificationGetting('Mail');
通知を送る($getting->getNotification());
この場合だと、NotificationGetting
クラスのgetNotification
メソッドは修正をして、
public function getNotification(): Notification
{
if ($this->notificationType === 'Mail') {
return new MailNotifier();
} else if ($this->notificationType === 'Slack') {
return new SlackNotifier();
} else if ($this->notificationType === 'Twitter') {
return new TwitterDMNotifier();
}
}
このようになるかと思いますが、逆に言えばここ以外は修正する必要がありません。
修正を最小限にすることで、修正したさいに起こるバグを減らすことやバグが起こった際にも原因の切り分けをすることができます。
インターフェイスを利用しないコード
インターフェイスを利用しなかった場合に使用する側のメソッドがどのようなコードになるのかを考えてみます。
// 使用する側
/**
* インターフェイスを利用したコード
* ・引数の型指定ができる。
* ・引数で受け取るオブジェクトは必ず同じ形のメソッドを持つ
*/
public function 通知を送る(Notification $notification)
{
$notification->メッセージをセットする('こんにちは');
$notification->メールを送信する();
}
/**
* インターフェイスを利用していないコード
* ・引数に型指定ができないため、型のチェックが必要な場合もある
* ・クラスを実装する人によって自由にメソッド名が付けられてしまう
*/
public function 通知を送る($notification)
{
if (
$notification instanceof MailNotifier
|| $notification instanceof SlackNotifier
|| $notification instanceof TwitterDMNotifier
) {
throw new InvalidArgumentException('引数の型が不正です。');
}
if ($notification instanceof MailNotifier) {
$notification->メールのメッセージをセット('こんにちは');
$notification->メールを送信する();
} else if (notification instanceof SlackNotifier) {
$notification->スラックのメッセージをセット('こんにちは');
$notification->スラックでメッセージを送信する();
} else if ($notification instanceof TwitterDMNotifier) {
$notification->メッセージをセットするよ('こんにちは');
$notification->DMを送信するよ();
}
}
このようにインタフェースを利用することで、それを実装するクラスに制約を持たせることができ、
コードの可読性や品質を助けることが出来ます。
メソッド名などはインターフェイスを使用しなくても、全て同じメソッド名になるようにプログラマー が気を付けていればいいのでは?
と思われるかもしれませんが、
人間であれば必ずミスはするものなので、ミスをすることを前提にして、ミスをしないようにコードを書くのではなく、
できる限りミスが起こりにくいコードを書くことを意識すべきだと思います。
以前に、インターフェイスを利用したテストコードの書き方についての記事を書きました。よければ合わせてぜひ。
【PHP】interfaceを利用してテストコードを書く - Qiita
参考になる記事
ポリモーフィズムとは「モノゴトにいくつかのカタチがありうる性質」のこと - Qiita
初学者でも10分で理解できる依存性逆転の原則(Dependency inversion principle) - Qiita
追記
twitterでこの記事に対してコメントをいただきました。
記事を修正しようかとも思いましたが、この記事を読んだあとでこちらのコメントを読んでいただいた方が、わかりやすいのではと思い、そのまま載せさせていただきました。
要約すると
- この記事ではクラスを実装してからインターフェイスを考えていますが、実際には利用する側のニーズに合わせて作られる。
- インターフェイスのメリットがカプセル化や他の説明と混じってしまっている。インターフェイスの説明だけで言うと「仕様と実装を分けられる」と言うのがメリット。
コメントをいただいた皆様ありがとうございます!
実装する側(MailNotifier)側の視点でインターフェースが作られているように見えましたが、逆で利用(通知送るメソッド)側の都合で作るものではないでしょうか?
— 増田 (@masuda_life) November 2, 2019
読んだ!
— hiro@miraito (@hirodragon112) November 2, 2019
実装例を交えながら詳細に解説していて素晴らしいと思います!
あえて補足するならば、少しカプセル化の説明が混同してしまっていてinterface の説明がぼやけてしまってるかもしれませんね。 (触ってほしくないメソッドはprivateにする...辺り)