🎉

【PHP】安定依存の原則について SDP

に公開

はじめに

安定依存の原則は、パッケージの依存は常により安定したパッケージに向くという原則[1]です。
「電話をかける」という処理を例に出してこの原則について解説します。
まずは、この原則に違反している例から見ていきます。

違反例

<?php

namespace Telephone;

class TelephoneCaller
{
    /**
     * 電話をかける
     */
    public function call(string $type, int $phoneNumber): void
    {
        if ($type === 'landline') {
            $telephone = new LandLinePhone();
        } elseif ($type === 'mobile') {
            $telephone = new SmartPhone();
        } else {
            throw new \InvalidArgumentException("Unknown telephone type: $type");
        }

        $telephone->call($phoneNumber);
    }
}

class LandLinePhone
{
    public function call(int $phoneNumber): void
    {
        echo "固定電話で電話をかけます: " . $phoneNumber;
    }
}

class SmartPhone
{
    public function call(int $phoneNumber): void
    {
        echo "スマートフォンで電話をかけます: " . $phoneNumber;
    }
}
<?php

namespace Client;

use Telephone\TelephoneCaller;

/**
 * TelephoneCallerのクライアント側の処理です
 */
class TelephoneClient
{
    public function __construct(
        private TelephoneCaller $telephoneCaller
    ) {}

    public function makeCall(string $type, int $phoneNumber): void
    {
        // 他にも処理がある想定です
    
        $this->telephoneCaller->call($type, $phoneNumber);
        
    }
}

ClientパッケージとTelephoneパッケージの依存関係
図1: ClientパッケージとTelephoneパッケージの依存関係
固定電話もしくはスマホから電話をかけるという処理を想定して実装しました。
ここで、ガラケーからも電話をかけられるように機能を追加したいと思います。
修正した実装を下記に記載します。


namespace Telephone;

class TelephoneCaller
{
    public function call(string $type, int $phoneNumber): void
    {
        if ($type === 'landline') {
            $telephone = new LandLinePhone();
        } elseif ($type === 'mobile') {
            $telephone = new SmartPhone();
        } elseif ($type === 'flip') {
            // ガラケーのインスタンスを生成する処理を追加
            $telephone = new FlipPhone();
        } else {
            throw new \InvalidArgumentException("Unknown telephone type: $type");
        }

        $telephone->call($phoneNumber);
    }
}

// 新しいガラケーのクラスを追加
class FlipPhone
{
    public function call(int $phoneNumber): void
    {
        echo "ガラケーを使って電話をかけます: " . $phoneNumber;
    }
}

ガラケーの場合の判定条件とクラスを1つ追加しました。このように、電話機の種類を増やす度に、TelephoneCallerクラスに判定条件を追加する対応と、新しい電話機のクラスを追加する対応を行う必要があります。また、電話機の種類を増やす対応を行うと、TelephoneCallerクラスに依存しているClientパッケージにも変更の影響が出てしまいます。少なくてもClientパッケージに全く変更の影響が出ていないと言い切ることができなくなってしまいます。このままだと、電話機の種類を増やす際の変更の影響範囲が無駄に広いです。

この実装の問題は、Clientパッケージが安定度が低い(変更が起きやすい)Telephoneパッケージに依存してしまっていることです。Telephoneパッケージの安定度を高く(変更を起きづらく)できれば、修正の影響範囲を狭くできるので、改修作業をより簡単にできます。

安定依存の原則の適用例

Clientパッケージが安定度の高いパッケージに依存するように、意図的にTelephoneパッケージの安定度が高くなるようにリファクタリングしたいと思います。

<?php

namespace Telephone;

interface ITelephone
{
    public function call(int $phoneNumber): void;
}

interface ITelephoneFactory
{
    public function create(string $type): ITelephone;
}


class TelephoneCaller
{
    public function __construct(
        private ITelephoneFactory $telephoneFactory
    ) {}

    /**
     * 電話をかける
     */
    public function call(string $type, int $phoneNumber): void
    {
        $telephone = $this->telephoneFactory->create($type);
        $telephone->call($phoneNumber);
    }
}
<?php

namespace Device;

use Telephone\ITelephone;
use Telephone\ITelephoneFactory;

class TelephoneFactory implements ITelephoneFactory
{
    public function create(string $type): ITelephone
    {
        return match($type) {
            'landline' => new LandLinePhone(),
            'smartphone' => new SmartPhone(),
            default => throw new \InvalidArgumentException("Unknown telephone type: $type"),
        };
    }
}

class SmartPhone implements ITelephone
{
    public function call(int $phoneNumber): void
    {
        echo "スマートフォンで電話をかけます: " . $phoneNumber;
    }
}

class LandLinePhone implements ITelephone
{
    public function call(int $phoneNumber): void
    {
        echo "固定電話で電話をかけます: " . $phoneNumber;
    }
}
<?php

namespace Telephone\Client;

use Telephone\TelephoneCaller;

/**
 * TelephoneCallerのクライアント側の処理です
 */
class TelephoneClient
{
    public function __construct(
        private TelephoneCaller $telephoneCaller
    ) {}

    public function makeCall(string $type, int $phoneNumber): void
    {
        // 他にも処理がある想定です

        $this->telephoneCaller->call($type, $phoneNumber);
        
    }
}

ClientとTelephoneとDeviceパッケージの依存関係
図2:ClientとTelephoneとDeviceパッケージの依存関係
変更が起こりづらそうな(安定度が高い)処理をTelephoneパッケージに、変更が起こりやすそうな(安定度が低い)処理をDeviceパッケージに集めました。
ここで、またガラケーから電話をかける処理を追加してみます。

namespace Device;

use Telephone\ITelephone;
use Telephone\ITelephoneFactory;

class TelephoneFactory implements ITelephoneFactory
{
    public function create(string $type): ITelephone
    {
        return match($type) {
            'landline' => new LandLinePhone(),
            'smartphone' => new SmartPhone(),
            'flipphone' => new FlipPhone(), // 追加
            default => throw new \InvalidArgumentException("Unknown telephone type: $type"),
        };
    }
}

// 追加
class FlipPhone implements ITelephone
{
    public function call(int $phoneNumber): void
    {
        echo "ガラケーを使って電話をかけます: " . $phoneNumber;
    }
}

電話機の種類を増やす改修を行っても、Telephoneパッケージ内のコードは修正していません。そのため、Telephoneパッケージに依存しているClientパッケージには変更の影響が出ていないと確実に言い切ることができています。Deviceパッケージ内のコードは修正していますが、Deviceパッケージは他のパッケージから依存されていないので、コード修正しても他のパッケージに変更の影響が出てしまうことはありません。

Telephoneパッケージには意図的に変更が起こりづらい安定度が高い処理を集めています。そして、ClientやDeviceパッケージから安定度が高いTelephoneパッケージに依存の向き先が向くようにしています。このように、依存関係が安定度が高いパッケージに向くようにすることで、電話機の種類を増やす改修を行っても、変更の影響範囲を狭い範囲に閉じ込めることができています。これが、「パッケージの依存は常により安定したパッケージに向く[1:1]」とする安定依存の原則がある理由です。安定依存の原則を守ることによって、システムを改修した際の影響範囲を小さい範囲に閉じ込めることができます。

安定度の計測方法

Instability(不安定さ)という指標を使って、パッケージ間の安定度を計測できます。
下記にInstabilityの公式を記載します[2]

ファン・イン: 依存入力数。パッケージ内のクラスに依存している外部のパッケージのクラス数
ファン・アウト: 依存出力数。パッケージ内にある、外部のパッケージに依存しているクラス数
Instability = ファン・アウト / (ファン・イン + ファン・アウト)

Instabilityは0以上1以下の値を取り、Instability = 0が最も安定しているパッケージであることを示していて、Instability = 1が最も不安定なパッケージであることを示しています。
図3に、図2のパッケージ間の安定度を計測した結果を示します。
パッケージの安定度
図3:パッケージの安定度
図3より、パッケージ間の依存の向き先が安定度が高いパッケージに向かっていることが分かります。

まとめ

パッケージの依存の向き先は常により安定したパッケージに向かわないといけないとするのが安定依存の原則です。この原則を守ることにより、システム改修の際の影響範囲をより小さい範囲に閉じ込めることができ、変更容易性を高めることができます。

脚注
  1. 田中ひさてる, PHPで理解するオブジェクト指向の活用 ちょうぜつソフトウェア設計入門, 2022年12月22日, 技術評論社, P30 ↩︎ ↩︎

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

Discussion