【本質を理解】『依存性注入』とは何か?簡単サンプルで実感しよう

依存性注入(Dependency Injection:DI)。コードの保守性やテスト性を向上させる、強力なデザインパターンです。

よく知られる便利なテクニック。しかしこの中身、難しい説明がとても多いです。

ここでは、ごく簡単なサンプルを題材にして、依存性注入が持つ本質を、体感してみましょう。

目次

まずは『依存性注入』のポイントを押さえよう

依存性注入は、ソフトウェア設計におけるデザインパターンの一つです。

ソフトウェアの依存関係における問題を解決する、方針のひとつです。

まず誤解なきように言うと、

依存性注入は

  • コーディングをラクにするためのものじゃ、ありません

どちらかといえば、

依存性注入は

  • テストをラクにするもの
  • メンテナンスをラクにするもの

なので、コーディング自体はちょっと大変になります。でもテストはすごくラクになる。

どういうことなのか?実例で見てみましょう。

『依存性注入』を導入する過程を追う

新人くんの作る、簡単Javaプログラムを例題にします。

プログラムに、依存性注入を導入するまでの過程を、見ていきましょう。

テストをどうしよう?

新人くん。Javaで、かんたんカレンダークラスをコーディングしています。

新人くん

MyCalenderクラス作りました!

現在の日付をプリントする、シンプルなカレンダークラスです。

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class MyCalender {
    // 現在の日付を表示する
    public void printCurrentDate() {
        // 現在の日時を取得
        LocalDateTime now = LocalDateTime.now();
        // フォーマットに従って出力
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");
        String formattedDate = now.format(formatter);
        System.out.println(formattedDate);
    }
}

先輩が声をかけてきました。

イケイケくん

ようし、それじゃこのクラスをテストするぞ!

新人くん、MyCalenderクラスを使ったアプリを作って、テストします。

新人くん

はい、今日の日付が表示できましたー♪

public class Main {
    public static void main(String[] args) {
        // MyCalenderクラスのインスタンスを作成
        MyCalender myCalender = new MyCalender();
        // 現在の日付を表示
        myCalender.printCurrentDate();
    }
}
2024/11/03

先輩はさらに、こんなことを言ってきます。

イケイケくん

ようし、それじゃ次は、表示パターンの確認だ!
2028年2月29日、の表示を確認して

新人くん

……え?

そんなこと言ったって、このMyCalenderクラス、今日の日付しか表示することができません。

どうしようかな?

依存部分を分離する

表示パターンを確認するために、日付を取得している部分、ここを何とかしよう。

新人くん

日付を取得するメソッドを分けようかな…

現在日時を取得するところを、getCurrentDateメソッドで分離します。

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class MyCalender {
    // 現在の日付を表示する
    public void printCurrentDate() {
        // 現在の日時を取得
        LocalDateTime now = getCurrentDate();
        // フォーマットに従って出力
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");
        String formattedDate = now.format(formatter);
        System.out.println(formattedDate);
    }

    // 現在日付を取得する
    private LocalDateTime getCurrentDate() {
        return LocalDateTime.now();
    }
}
新人くん

で、このメソッドをちょっと直して、日付を変えれば……

    // 現在日付を取得する
    private LocalDateTime getCurrentDate() {
        return LocalDateTime.of(2028, 2, 29, 0, 0, 0);   // 2028年2月29日
    }
イケイケくん

No-!
テスト中のクラスを編集するなんてダメだー!

そうですね。テスト中に、テスト対象のソースを書き換えちゃいけません。

依存部分を別クラスに分離する

新人くん、別の手を考えます。

新人くん

じゃあ、日付を取るところを、別のクラスにすればいいかな?

日付を取得するための、DateProviderクラスを新たに作ります。

import java.time.LocalDateTime;

public class DateProvider {
    // 現在日付を取得する
    public LocalDateTime getCurrentDate() {
        return LocalDateTime.now();
    }
}

MyCalenderクラスは、DateProviderクラスを使って、日時を取得するようにします。

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class MyCalender {
    // 現在の日付を表示する
    public void printCurrentDate() {
        // 現在の日時を取得
        DateProvider provider = new DateProvider();
        LocalDateTime now = provider.getCurrentDate();
        // フォーマットに従って出力
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");
        String formattedDate = now.format(formatter);
        System.out.println(formattedDate);
    }
}

そして、DateProviderクラスを書き換えて、テスト用の日時を返してあげればOK。

import java.time.LocalDateTime;

public class DateProvider {
    // 現在日付を取得する
    public LocalDateTime getCurrentDate() {
        // テスト用
        return LocalDateTime.of(2028, 2, 29, 0, 0, 0);   // 2028年2月29日
    }
}
2028/02/29

これなら、MyCalenderクラス本体はいじらなくても、テストできます。

新人くん

よし、表示確認できたぞ

イケイケくん

OK、じゃあ次は、
1月1日、12月31日、も確認して!

新人くん

ええー…………

表示パターンが増えました。まあ、パターンを色々確認するもの、大事ではありますね。

ただ、そのためには、こんな作業が必要になります。

  • DateProviderクラスを、1月1日を返すように改造
  • コンパイル
  • できたら実行
  • DateProviderクラスを、12月31日を返すように改造
  • コンパイル
  • できたら実行

毎回直してコンパイルして…と、いかにも面倒

これからテストケースがどんどん増えていったら、手間がかかってしょうがない。

依存部分を外から設定できるようにする

ここで、先輩からのアドバイス。

イケイケくん

日付のオブジェクトを、外から設定できるようにするといいぞ!

これをすると、コンパイルすることなしに、日時を差し替えて動かせるようになる、らしい。

新人くん、MyCalenderクラスを、さらに改造します。

新人くん

コンストラクタを追加するといい、って言ってたな…

MyCalenderクラスのコンストラクタで、DateProviderオブジェクトを入力するようにします。

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class MyCalender {
    private DateProvider provider;

    // コンストラクタ
    MyCalender(DateProvider provider) {
        this.provider = provider;
    }

    // 現在の日付を表示する
    public void printCurrentDate() {
        // 現在の日時を取得
        LocalDateTime now = provider.getCurrentDate();
        // フォーマットに従って出力
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");
        String formattedDate = now.format(formatter);
        System.out.println(formattedDate);
    }
}

DateProviderクラスは、テスト用に改造しておきます。任意の日付を設定できるようにしておきましょう。

import java.time.LocalDateTime;

public class DateProvider {
    public LocalDateTime dateTime;
    // 現在日時を取得する
    public LocalDateTime getCurrentDate() {
        // テスト用データを返却
        return dateTime;
    }
}

そしてテストコードは、こう。

import java.time.LocalDateTime;

public class Main {
    public static void main(String[] args) {

        // 2025年2月29日
        DateProvider provider1 = new DateProvider();
        provider1.dateTime = LocalDateTime.of(2028, 2, 29, 0, 0, 0);
        MyCalender calender1 = new MyCalender(provider1);
        calender1.printCurrentDate();

        // 2025年1月1日
        DateProvider provider2 = new DateProvider();
        provider2.dateTime = LocalDateTime.of(2028, 1, 1, 0, 0, 0);
        MyCalender calender2 = new MyCalender(provider2);
        calender2.printCurrentDate();

        // 2025年12月31日
        DateProvider provider3 = new DateProvider();
        provider3.dateTime = LocalDateTime.of(2028, 12, 31, 0, 0, 0);
        MyCalender calender3 = new MyCalender(provider3);
        calender3.printCurrentDate();
    }
}

こうすると、たくさんある日付確認のテストを、一気に流せます。

新人くん

おおー、いろいろなパターンを、まとめてテストできるようになった!

依存部分をインターフェースにする

テストがだいぶラクになってきました。

でも、まだ手間な部分が残ってますDateProviderクラスです。

DateProviderクラス。実際の日付を返す、この本物

import java.time.LocalDateTime;

public class DateProvider {
    // 現在日付を取得する
    public LocalDateTime getCurrentDate() {
        return LocalDateTime.now();
    }
}

テスト用に準備した、このダミー

import java.time.LocalDateTime;

public class DateProvider {
    public LocalDateTime dateTime;
    // 現在日付を取得する
    public LocalDateTime getCurrentDate() {
        // テスト用データを返却
        return dateTime;
    }
}
新人くん

テストの度に、本物とダミーを差し替えるのも、面倒だよね…

ここでまた、先輩からのアドバイス。

イケイケくん

日付オブジェクトのインターフェースを作るといいぞ!

DateProviderを、本物とダミー共通の定義を持つ、インターフェースにします。

新人くん、先輩からの教えに従い、インターフェースを作ります。

新人くん

インターフェースって、こんなだっけ?

import java.time.LocalDateTime;

public interface DateProvider {
    // 現在日付を取得する
    public LocalDateTime getCurrentTime();
}

そして、このインターフェースを持つ、本物クラスとダミークラスを作ります。

import java.time.LocalDateTime;

public class DateProviderImpl implements DateProvider {
    // 現在日時を取得する
    public LocalDateTime getCurrentTime() {
        return LocalDateTime.now();
    }
}
import java.time.LocalDateTime;

public class DateProviderTest implements DateProvider {
    public LocalDateTime dateTime;
    // 現在日時を取得する(ダミー)
    public LocalDateTime getCurrentTime() {
        return dateTime;
    }
}

すると、テストコードは、こうなります。

import java.time.LocalDateTime;

public class Main {
    public static void main(String[] args) {

        // 現在日時
        DateProviderImpl provider = new DateProviderImpl(); // 本物
        MyCalender calender = new MyCalender(provider);
        calender.printCurrentDate();

        // 2025年2月29日
        DateProviderTest provider1 = new DateProviderTest(); // テスト用
        provider1.dateTime = LocalDateTime.of(2028, 2, 29, 0, 0, 0);
        MyCalender calender1 = new MyCalender(provider1);
        calender1.printCurrentDate();

        // 2025年1月1日
        DateProviderTest provider2 = new DateProviderTest(); // テスト用
        provider2.dateTime = LocalDateTime.of(2028, 1, 1, 0, 0, 0);
        MyCalender calender2 = new MyCalender(provider2);
        calender2.printCurrentDate();

        // 2025年12月31日
        DateProviderTest provider3 = new DateProviderTest(); // テスト用
        provider3.dateTime = LocalDateTime.of(2028, 12, 31, 0, 0, 0);
        MyCalender calender3 = new MyCalender(provider3);
        calender3.printCurrentDate();
    }
}

これでなんと、

  • 本物のクラスを一切いじらずに
  • いろいろな表示パターンのテストができます
新人くん

すごい!
いちいちコードを書き換えなくても、テストが全部できるぞ

コードを書き換えるという行為は、それだけでプログラムが動かなくなったりする危険があります。

なので、コードを書き換えることなく、色々なパターンのテストができるのは、とても便利なのです。

「依存」するオブジェクトを、外部から「注入」する

ここまでを復習しましょう。

  • MyCalenderクラスは、日付を取得するのに、DateProviderクラスを使っています
  • それはすなわち、DataProviderがいないと困ってしまう

これを

MyCalenderクラスは、DateProviderクラスに依存している状態

といいます。

MyCalenderクラスは、DateProviderオブジェクトを、なんとか準備しないといけないのですが、

  • MyCalenderクラスの中でDateProviderオブジェクトを作るのではなく
  • 外部でDateProviderオブジェクトを作り、コンストラクタなどで入力する

このイメージが

DateProviderオブジェクトを、MyCalenderオブジェクトに「注入する」

という表現になります。

イケイケくん

自分が「依存しているオブジェクト」を、外部から「注入」するのだ!

これが、「依存性を注入する」というテクニックの考え方です。

コンストラクタにより依存性注入する手法を「コンストラクタインジェクション」と呼びます。他にも、メソッドで注入する「セッターインジェクション」などもあります。ただ大抵の場面では、コンストラクタインジェクションのほうが推奨です。

テストがラクになる『依存性注入』

依存性注入とは、ごく簡単にいうと、こういうテクニック。

自分が依存しているオブジェクトを、外部から注入する

先ほどの例でメリットはありそうですね。でも確実に、コードは複雑になります。

そこまでして、「依存性注入」を導入する価値は、どこにあるのでしょうか?

依存性注入の使いどころ

依存性注入を使うと、テストプログラムの動作中に、オブジェクトをいろいろ差し替えできます。

特に効果が高いのは、その時の状況によって結果が変わるオブジェクトに依存する場合です。

状況によって結果が変わるオブジェクト
  • 外部データ(データベース、ファイル)
  • 外部からの入力(ネットワーク通信)
  • 日付や時刻
  • ランダムデータ

こういうクラス、テスト時に本物を使うと、特定の状況を再現するのが、とても難しい

これを、外から挿入できる仕掛けにしておくと、テスト時に任意に差し替えができて、便利です。

ユニットテストと相性抜群

依存性注入のこの仕掛けは、ユニットテストと相性抜群です

ユニットテストでは、あらかじめ仕掛けたテストコードを、まとめて流します。

このとき、依存性注入の仕掛けで、任意にスタブ差し替えができるようにしておきます。

すると、テストプログラムの中で、色々なパターンを一度に流すことができるのです。

新人くん

いつでもすぐにテストを流せるのは、安心だよね!

フレームワークが作れる『依存性注入』

依存性注入が活躍する、もうひとつの大きな舞台。それがフレームワークです。

修正や再コンパイルをすることなく、依存オブジェクトを差し替えられる

この特性が、フレームワークの実装と相性がよいのです。

基本的なフレームワークの仕掛け

フレームワーク」とは、プログラム開発において、よく利用される機能をあらかじめ備えた骨組みのことです。

つまるところ、あらかじめ作られたプログラム部品の集合。似た概念に「ライブラリ」がありますが、両者にはこのような違いがあります。

  • ライブラリ ユーザープログラムが「ライブラリ」を呼び出す
  • フレームワーク 「フレームワーク」がユーザープログラムを呼び出す

フレームワークには、GUIフレームワーク、Webフレームワークといったものがあり、だいたいこういう構造になっています。

フレームワークの構造
  • フレームワーク なんらかのイベントで、ユーザープログラムを呼び出す
  • ユーザープログラム イベントに対応した独自処理を実装する

ここで依存性注入を使うと、フレームワークを再コンパイルすることなく、ユーザープログラムを差し替えられるわけです。

依存性注入を採用するフレームワーク

いまでは、世の中の多くのフレームワークが、依存性注入(DI)を採用しています。

DIを採用するフレームワークの例
  • Spring Framework Javaで圧倒的人気を誇るフレームワーク
  • ASP.NET Core Micosoft .NET Core向けのWebフレームワーク
  • Laravel PHPの人気Webフレームワーク
  • Django Pythonの代表的なWebフレームワーク

これらはDIフレームワークと呼ばれたりもします。「DIコンテナ」「サービスコンテナ」といった仕掛けでDIを実現しています。

具体的な実装方法は、フレームワークによって様々。ただ依存性注入の本質は変わりません。

イケイケくん

それぞれのフレームワークの流儀に沿って、DIを実現するのだ!

依存性注入の本質を念頭に置いて、フレームワークを使いましょう。

依存性注入が関係する原則

この依存性注入は、SOLID原則とよばれるソフトウェア設計原則と、密接な繋がりがあります。

特に関連するのが、「依存性逆転原則」。

上位モジュールが下位モジュールに依存する、という関係。これを逆転させるテクニックが「依存性注入」なのです。

この原則を理解すると、依存性注入の役割がよく分かるようになるでしょう。

まとめ

ここでは、依存性注入を理解するための、はじめの一歩を説明しました。

依存性注入(Dependency Injection:DI)の世界は奥深いです。

例ではJavaを取り上げましたが、言語やフレームワークによって、実現方法は様々

でも本質の考え方はみんな同じ。原則を忘れないようにして、そのメリットを活かしましょう。

イケイケくん

依存性注入を覚えると、テストやメンテナンスが格段にラクになるぞ!

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次