【SOLID】依存関係逆転の原則を完全に理解したい


SOLIDの原則とは?

SOLIDは

  • 変更に強い
  • 理解しやすい

などのソフトウェアを作ることを目的とした原則です。

次の5つの原則があります。

  • Single Responsibility Principle (単一責任の原則)
  • Open-Closed Principle (オープン・クローズドの原則)
  • Liskov Substitution Principle (リスコフの置換原則)
  • Interface Segregation Principle (インタフェース分離の原則)
  • Dependency Inversion Principle (依存関係逆転の原則)

上記5つの原則の頭文字をとってSOLIDの原則と言います。
今回の記事では Dependency Inversion Principle (依存関係逆転の原則) について解説します。
その他の原則に関しては下記参照。

簡単に言うと...

使う側と使われる側の関係を見直そう」ということです。
たとえば、画面で保存ボタンを押したらDBに保存されるというシンプルな機能について考えましょう。

この場合、

  • 「画面」が使う側
  • 「DBに保存する処理」が使われる側

になります。
普通処理を書こうとすると、「画面」から「DBに保存する処理」を呼び出すため、
「画面」が「DBに保存する処理」に依存することになります。
たとえば、保存先がDBからCSVファイルに切り替わった場合、その影響が画面 (正確に言うと画面側での呼び出しロジック) にも影響が出てしまいます。
この影響を出ないようにするために、「使う側と使われる側の関係を見直そう」というのです。
では、使う側と使われる側の関係を見直した結果、どのようにすればよいのでしょうか?

少し詳しめに言うと...

Wikipediaでは下記のように説明されています。

  1. 上位モジュールはいかなるものも下位モジュールから持ち込んではならない。双方とも抽象(例としてインターフェース)に依存するべきである。

  2. 抽象は詳細に依存してはならない。詳細(具象的な実装内容)が抽象に依存するべきである。

先程の「画面」から「DBに保存する処理」に上記を適用すると下図のようになります。

  1. 上位モジュールはいかなるものも下位モジュールから持ち込んではならない。双方とも抽象(例としてインターフェース)に依存するべきである。

元々は使う側 (View) が使われる側 (Repository) に依存していましたが、
IRepositoryという抽象 (ここではインターフェース) が現れ、使う側 (View) も使われる側 (Repository) も抽象 (IRepository) に依存しています。

  1. 抽象は詳細に依存してはならない。詳細(具象的な実装内容)が抽象に依存するべきである。

IRepositoryにSaveというメソッドが定義されており、Repositoryはそれを継承して使っています。これは、詳細 (Repository) の具体的な実装内容が、抽象 (IRepository) に依存しているということです。

もう少し具体例を用いながら見ていきましょう。

今回使用する例

テレビの録画を例に考えてみましょう。

テレビの録画機器として、ビデオ、ブルーレイディスク、HDDなどがあります。
これをコードに落とし込んでいきましょう。

依存関係逆転の原則に違反した例

ビデオ、ブルーレイディスク、HDDの3つのクラスを作り、
テレビから呼び出すという想定で考えてみましょう。

Television.cs
class Television
{
	public void Record()
    {
        if (ConnectsVideo())
        {
            Video video = new Video();
			video.Record();
            return;
        }
        
        if (ConnectsBluRay())
        {
            Blu_ray bluRay = new Blu_ray();
            bluRay.Record();
            return;
        }
        
        HDD hdd = new HDD();
        hdd.Record();
    }
}
Video.cs
public sealed class Video
{
    public void Record()
    {
        // ビデオに録画
    }
}
Blu_ray.cs
public sealed class Blu_ray
{
    public void Record()
    {
        // ブルーレイに録画
    }
}
HDD.cs
public sealed class HDD
{
    public void Record()
    {
        // HDDに録画
    }
}

テレビ側で、逐一「記録媒体が何であるか」を確認して、各記録媒体に記録指示を出すことになります。
これが、使う側が使われる側に依存している状態です。

依存関係逆転の法則を適用する

テレビと記録媒体の間にインターフェースをかませることで解決できます。

コードでの実装例は下記のようになります。

Television.cs
class Television
{
	public void Record()
    {
        IRecord recorder = Factories.CreateRecord();
        recorder.Record();
    }
}
IRecord.cs
public interface IRecord
{
    enum Recorder 
    {
        VIDEO,
        BLU_RAY,
        HDD
    }

    public void Record();
}
Video.cs
public sealed class Video : IRecord
{
    public void Record()
    {
        // ビデオに録画
    }
}
Blu_ray.cs
public sealed class Blu_ray : IRecord
{
    public void Record()
    {
        // ブルーレイに録画
    }
}
HDD.cs
public sealed class HDD : IRecord
{
    public void Record()
    {
        // HDDに録画
    }
}
Factories.cs
public static class Factories
{
    public static IRecord CreateRecord()
    {
        // テレビに接続されている機器取得
        var recorder = GetRecorder();

        if (recorder == IRecord.Recorder.VIDEO)
        {
            return new Video();
        }

        if (recorder == IRecord.Recorder.BLU_RAY)
        {
            return new Blu_ray();
        }

        return new HDD();
    }
}

Factoryを使うことで、テレビ側での呼び出しを簡素にしています。
これにより、ビデオなどの具象クラスになにかの変更があったとしても、その変更の影響が使う側には伝わりにくくなります。
また、それ以外のメリットもあります。

テスト容易姓

依存関係逆転の法則を適用することは、テストをしやすくなることにも繋がります。
例えば、上記例では、

  • ビデオ
  • ブルーレイディスク
  • HDD

などの具体的なものに繋いでの動作確認が必要になります。
もし、それらの機器が手元にない場合、テストをすることができません。
このように、外部機器との接続が必要なものの場合、テスト用のクラス (Fakeクラスなど) を用意しておくことで
テストがしやすくなります。

例えば、Factoriesにこのように書いておくことで、デバッグ時は常にFakeから値を返すということができます。

Factories.cs
public static class Factories
{
    public static IRecord CreateRecord()
    {
        // ここを追加
#if DEBUG
        return new Fake();
#endif

        // テレビに接続されている機器取得
        var recorder = GetRecorder();

        if (recorder == IRecord.Recorder.VIDEO)
        {
            return new Video();
        }

        if (recorder == IRecord.Recorder.BLU_RAY)
        {
            return new Blu_ray();
        }

        return new HDD();
    }
}

まとめ

依存関係逆転の法則とは「使う側と使われる側の関係を見直そう」ということでした。
具体的には、

  • 使う側も使われる側も抽象に依存させる
  • 詳細の処理内容を抽象に依存させる

ということです。
そうすることで下記のようなメリットが得られます。

  • 変更に強い
  • 理解しやすい

尚、今回使用したソースはこちらに上がっています。

参考文献