🐕

【PHP】インターフェイス分離の原則について ISP

に公開

はじめに

インターフェイス分離の原則は、簡単にまとめると「不要なものには依存しないこと[1]」を定めている原則です。ここでは、「電話をかける」処理と「電話に出る」処理を例に挙げて、この原則について解説します。まず、この原則の違反例から見ていきます。

違反例

<?php

namespace Telephone;

interface ITelephone
{
    /**
     * 電話に出る
     */
    public function receiveCall(): void;

    /**
     * 電話をかける
     */
    public function makeCall(int $phoneNumber): void;
}
<?php

namespace Client;

/**
 * 電話をかける処理のクライアント
 */
class TelephoneCallClient
{
    public function __construct(
        private ITelephone $telephone
    ) {}
    
    public function makeCall(int $phoneNumber): void
    {
      // 他にも処理がある想定
    
        $this->telephone->makeCall($phoneNumber);
    }
}
<?php

namespace Device;

use BadMethodCallException;
use Telephone\ITelephone;

/**
 * スマホ
 */
class SmartPhone implements ITelephone
{
    public function receiveCall(): void
    {
        echo "スマートフォンが着信を受けました。\n";
    }

    public function makeCall(int $phoneNumber): void
    {
        echo "スマートフォンで電話をかけます: " . $phoneNumber . "\n";
    }
}

/**
 * 公衆電話
 */
class PublicPhone implements ITelephone
{
    public function receiveCall(): void
    {
        throw new BadMethodCallException('公衆電話は着信を受けることができません。');
    }

    public function makeCall(int $phoneNumber): void
    {
        echo "公衆電話で電話をかけます: " . $phoneNumber . "\n";
    }
}

ITelephoneインターフェイスには、「電話をかける」処理と「電話に出る」処理があります。そして、スマホのクラスと公衆電話のクラスがこのインターフェイスを実装しています。

スマホの場合は、電話をかける事も電話に出る事もできるため両方のメソッドが必要です。しかし、公衆電話はどうでしょうか。公衆電話の場合は、電話をかける事はできますが、電話に出ることは一般的にはできません。そのため、「電話に出る」というメソッドは不要です。
今回の場合、ITelephoneインターフェイスに「電話をかける」処理と「電話に出る」処理の両方を定義してしまっているがために、PublicPhoneクラスが不必要な「電話に出る」メソッドを実装してしまっています。また、電話をかける処理のクライアントであるTelephoneCallClientクラスも、ITelephoneインターフェイス全体に依存しています。そのため、TelephoneCallClientクラスが無関心な「電話に出る」メソッドが隠されていません。

このように、大きい単位でインターフェイスを作ってしまうと、インターフェイスを実装することや使い方が難しくなってしまいます。より小さい単位でインターフェイスを作ることで、これらの問題を解決することができます。

インターフェイス分離の原則の適用例

「電話に出る」処理と「電話をかける」処理をそれぞれ別々のインターフェイスに分離してみます。

namespace Telephone;

interface ICallMaker
{
    /**
     * 電話をかける
     */
    public function makeCall(int $phoneNumber): void;
}

interface ICallReceiver
{
    /**
     * 電話に出る
     */
    public function receiveCall(): void;
}
<?php

namespace Client;

/**
 * 電話をかける処理のクライアント
 */
class TelephoneCallClient
{
    public function __construct(
        private ICallMaker $callMaker
    ) {}

    public function makeCall(int $phoneNumber): void
    {
        $this->callMaker->makeCall($phoneNumber);
    }
}
<?php

namespace Telephone;

/**
 * 公衆電話
 */
class PublicPhone implements ICallMaker
{
    /**
     * 電話をかける
     */
    public function makeCall(int $phoneNumber): void
    {
        echo "公衆電話で電話をかけます: " . $phoneNumber . "\n";
    }
}

/**
 * スマホ
 */
class SmartPhone implements ICallReceiver, ICallMaker
{
    public function receiveCall(): void
    {
        echo "スマートフォンが着信を受けました。\n";
    }

    public function makeCall(int $phoneNumber): void
    {
        echo "スマートフォンで電話をかけます: " . $phoneNumber . "\n";
    }
}

「電話に出る」処理と「電話をかける」処理をそれぞれ、ICallReceiverインターフェイスとICallMakerインターフェイスに分離しました。分離したことによって、PublicPhoneクラスが不必要な「電話をかける」処理を実装せずに済んでいます。また、TelephoneCallClientクラスも「電話に出る」という無関心なメソッドが隠されています。このように小さい単位でインターフェイスを作る事で、インターフェイスを実装することが簡単になります。また、インターフェイスの呼び出し側も、不必要なメソッドが隠されているので、よりシンプルになりますし使い方が簡単になります。

このように、インターフェイス分離の原則が定めている「不必要なものには依存しないこと[1:1] 」を徹底することで、柔軟な実装が可能になります。また、余計な依存もなくせるので変更の影響範囲も小さくできます。

脚注
  1. Robert C. Martin他, Clean Architecture, 2018年7月27日, 株式会社ドワンゴ, P121 ↩︎ ↩︎

Discussion