👋

【PHP】依存関係逆転の原則について DIP

に公開

はじめに

依存関係逆転の原則について下記にまとめます。

ソースコードの依存関係が(具象ではなく)抽象だけを参照しているもの。それが最も柔軟なシステムである。これが「依存関係逆転の原則(DIP)」の伝えようとしていることである。
引用: Robert C. Martin他, Clean Architecture, 2018年7月27日, 株式会社ドワンゴ, P103

文章だけだと理解しづらいと思います。
ここでは、「価格を計算する」処理を例に挙げてDIPについて解説します。
まず、この原則の違反例についてまとめます。

違反例

<?php

namespace Calculate;

use Discount\FiveHundredYenDiscountCalculator;

class PriceCalculator
{
    public function __construct(
        private float $taxRate = 0.1
    ) {}

    public function calculate(int $basePrice): int
    {
        // 500円引きを行う
        $discountCalculator = new FiveHundredYenDiscountCalculator();
        $discountedPrice = $discountCalculator->calculate($basePrice);
        
        // 割引後の価格に対して税金を計算
        $tax = floor($discountedPrice * $this->taxRate);
        return $discountedPrice + $tax;
    }
}
<?php

namespace Discount;

class FiveHundredYenDiscountCalculator
{
    private const DISCOUNT_AMOUNT = 500;

    public function calculate(int $basePrice): int
    {
        return max(0, $basePrice - self::DISCOUNT_AMOUNT);
    }
}
// 使用例
$priceCalculator = new PriceCalculator();
print $priceCalculator->calculate(1000); // 550

PriceCalculatorクラスのcalculateメソッドでは、元の価格から500円引きした後に、割引後の価格に対して税金を計算して最終的な価格を出力しています。
上記の処理の依存関係を下記に示します。
パッケージ間の依存関係
図1: パッケージ間の依存関係
ここで、500円引きではなく3割引きに変更したい場合を考えてみます。
先ほどの実装の割引処理を3割引きに変更したものを下記に記載します。

<?php

namespace Calculate;

use Discount\ThirtyPercentDiscountCalculator;

class PriceCalculator
{
    public function __construct(
        private float $taxRate = 0.1
    ) {}

    public function calculate(int $basePrice): int
    {
        // $discountCalculator = new FiveHundredYenDiscountCalculator();
        // 500円引きではなく3割引を行う
        $discountCalculator = new ThirtyPercentDiscountCalculator();
        $discountedPrice = $discountCalculator->calculate($basePrice);
        
        // 割引後の価格に対して税金を計算
        $tax = floor($discountedPrice * $this->taxRate);
        return $discountedPrice + $tax;
    }
}
<?php

namespace Discount;

class ThirtyPercentDiscountCalculator
{
    private const DISCOUNT_RATE = 0.3;

    public function calculate(int $basePrice): int
    {
        return floor($basePrice * (1 - self::DISCOUNT_RATE));
    }
}

3割引きに変更後のパッケージ間の依存関係
図2: 3割引きに変更後のパッケージ間の依存関係
PriceCalculatorクラスのcalculateメソッドの中身を、500円引きではなく3割引きできるように修正しました。

図1・図2より、依存関係の矢印は、CalculateパッケージからDiscountパッケージに向かっている事が分かります。つまり、PriceCalculatorクラスは、どのように割引を行うのかの詳細(500円引きなのか3割引きなのか)に依存してしまっています。そのため、今回のように割引額を変更する度にPriceCalculatorクラスを修正する必要があり大変です。
もし、PriceCalculatorクラスを一切修正することなく、割引額を柔軟に変更できたらとても楽です。
果たしてそんな事ができるのでしょうか?依存関係逆転の原則を使えば可能です。

依存関係逆転の原則の適用例

PriceCalculatorクラスを一切修正することなく、割引額を変更できるようにリファクタリングした例を下記に記載します。

<?php

namespace Calculate;

class PriceCalculator
{
    public function __construct(
        private IDiscount $discount,
        private float $taxRate = 0.1
    ) {}

    public function calculate(int $basePrice): int
    {
        $discountedPrice = $this->discount->calculate($basePrice);
        // 割引後の価格に対して税金を計算
        $tax = floor($discountedPrice * $this->taxRate);
        return $discountedPrice + $tax;
    }
}

interface IDiscount
{
    /**
     * 割引額を計算する
     */
    public function calculate(int $basePrice): int;
}
<?php

namespace Discount;

use Calculate\IDiscount;

class FiveHundredYenDiscountCalculator implements IDiscount
{
    private const DISCOUNT_AMOUNT = 500;

    public function calculate(int $basePrice): int
    {
        return max(0, $basePrice - self::DISCOUNT_AMOUNT);
    }
}
// 使用例
// 500円引きされます
$priceCalculator = new PriceCalculator(
  new FiveHundredYenDiscountCalculator()
);
print $priceCalculator->calculate(1000); // 550

上記の処理の依存関係を図3に示します。
パッケージ間の依存関係(DIP適用後
図3: パッケージ間の依存関係(DIP適用後)
割引処理の部分をIDiscountインターフェイスとして抽象化し、Calculateパッケージに含めました。そして、PriceCalculatorクラスがこのインターフェイスに依存するように修正しました。また、FiveHundredYenDiscountCalculatorクラスをIDiscountインターフェイスを実装する形に修正しました。
ここで、先ほどと同じく、500円引きではなく3割引きされるように割引処理を変更したいと思います。

<?php

namespace Discount;

use Calculate\IDiscount;

class ThirtyPercentDiscountCalculator implements IDiscount
{
    private const DISCOUNT_RATE = 0.3;

    public function calculate(int $basePrice): int
    {
        return floor($basePrice * (1 - self::DISCOUNT_RATE));
    }
}
// 使用例
// 3割引きされます
$priceCalculator = new PriceCalculator(
  new ThirtyPercentDiscountCalculator()
);
print $priceCalculator->calculate(1000); // 770

変更後の依存関係を図4に示します。
図4: 変更後のパッケージ間の依存関係(DIP適用後)
図4: 変更後のパッケージ間の依存関係(DIP適用後)
目論見通り、PriceCalculatorクラスを一切修正せずに、3割引きを行う新しいクラスを追加するだけで、500円引きから3割引きに割引処理を変更することができました。

図3・図4より、DiscountパッケージがCalculateパッケージに依存していることが分かります。
そのため、PriceCalculatorクラスはどのように割引を行うのかの詳細に依存していないので、PriceCalculatorクラスを一切修正することなく割引額の変更が行えたのです。

ここで、図1・図2と図3・図4を見比べてみてください。図1・図2では、依存の矢印がCalculateパッケージから Discountパッケージに向かっています。しかし、図3・図4では、依存の矢印がDiscountパッケージからCalculateパッケージに向かっており、依存関係が逆転していることが分かります。これが「依存関係逆転の原則」という原則名の「依存関係逆転」と名付けられている理由です。

interfaceなどの抽象クラスを使うことにより、依存の方向を自由自在にコントロールすることができます。今回の場合は、IDiscountインターフェイスをCalculateパッケージの中に置くことによって、依存の向き先を逆転させて安定依存の原則に従うようにしました。※安定依存の原則についてはこちらの記事にまとめました。

今回の例のように、適切に依存関係逆転の原則を適用することにより、システムの変更容易性が大幅に高まります。

まとめ

interfaceなどの抽象クラスを使うことにより、依存の方向を自由自在にコントロールすることができます。依存関係を適切に逆転させることで、システムの変更容易性が大幅に高まります。

Discussion