[デザインパターン] Dependency injection(依存性の注入)を実装付きで解説 [初心者向け]

2021年6月13日日曜日

Architecture

依存性の注入と訳されるDependency injectionの概念やメリットをJavaの実装付きで解説します。
アイキャッチ


Dependency injectionとは?

はじめに、言葉の定義を見ていきます。

WikiPediaの説明によるとDependency injectionとは下記のように定義されています。

依存性の注入とは、コンポーネント間の依存関係をプログラムから排除するため、外部の設定ファイルなどでオブジェクトを注入できるようにするソフトウェアパターンである。
ここで気になるのは、Dependency injectionの和訳は依存性の注入であるにもかかわらず、説明ではオブジェクトを注入するデザインパターンであると記載されている点です。

ここにDependency injectionの理解を難しくしている要因があると思います。

依存性の注入という和訳にもかかわらず、その実態は依存性(依存関係)をソースコード排除するためのデザインパターンなのです。実際に注入するものはオブジェクトであり依存性ではないのです。

Dependency injection(依存性の注入)

  1. 依存性は注入しない
  2. ソースコードから依存関係を排除するためのデザインパターン
  3. 依存関係は外部の設定ファイル等で、外部からオブジェクトを注入する

Dependency injectionの必要性

クラス図を基になぜDependency injectionが求められるのかを説明します。

始めに下図のようなクラス構成があるとします。
下図は、Class AInterface Bを呼び出しInterface Bの実装はClass Bが提供することを表しています

インターフェースの概念図

また、このようなクラス図の書き方はUMLと呼ばれる記法です。
UMLについて詳しく知りたい方は下記の書籍が網羅的に分かりやすく記載されていますのでご一読をお勧めします。


話を本題に戻します。
上記クラス図を、Javaの実装で簡易的に表すと下記のようになります

class ClassA {
    InterfaceB interfaceb;
    public ClassA(){
        interfaceb = new ClassB();
    }
    public void MethodA(){
        System.out.println("Call MethodB for ClassA");
        interfaceb.MethodB();
    }
}

interface InterfaceB{
    void MethodB();
}

class ClassB implements InterfaceB{
    public void MethodB(){
        System.out.println("Called MethodB()");
    }
}


ClassAがInterface Bを呼び出し、Interface Bの実装をClass Bで行っています。

ここで改めて一つ考えてみたいと思います。なぜInterface Bを定義しなければならなかったのでしょうか?

Interface Bを定義する理由は、 Class AからClass Bの実装を隠蔽するためです。

Class AInterface Bさえ知っていればClass Bの機能を利用でき
Interface Bさえ保守していればClass Bの変更をClass Aが受けないという状態を作りたいわけです。

簡単に言えば、Class AClass B疎結合に保っていたいのです。

しかしながら、上記は実装レベルで真に疎結合になり得ていないという問題があります。

なぜならClass AClass Bをインスタンス化している下記の実装に問題があります。

interfaceb = new ClassB();

Class Bのインスタンス化をClass Aが行っているために

実装レベルではClass AClass Bに依存してしまっているのです。
これではInterface Bを定義した目的が損なわれてしまいます。

これに対して、Dependency injectionはClass AからClass Bのインスタンス化を行う実装を除くことが可能となります。

Dependency injectionのメリット

この問題に対する解決策がDependency injectionです。
Class AClass Bを疎結合に保つため下記の変更を加えます。

Dependency injectionの概念図


上図のようにClass A, Class Bのインスタンス化を外部(Injector)に移譲します
実装は簡易的には下記のようになります

 import java.util.*;

public class Main {
    public static void main(String[] args) throws Exception {
        // 仮想的なInjector
        System.out.println("Inject object");
        ClassB class_b = new ClassB();
        ClassA class_a = new ClassA(class_b);

        class_a.MethodA();
    }
}

class ClassA {
    InterfaceB interface_b;
    public ClassA(InterfaceB instance){ # 外部から"オブジェクト"を注入する
        this.interface_b = instance;
    }
    public void MethodA(){
        System.out.println("Call MethodB for ClassA");
        interface_b.MethodB();
    }
}

interface InterfaceB{
    void MethodB();
}

class ClassB implements InterfaceB{
    public void MethodB(){
        System.out.println("Called MethodB()");
    }
}


先ほどとの変更点は下記2点です。
  1. Injector(コード上ではMain)がClass Bをインスタンス化する
  2. Class AInterface Bのインスタンスをコンストラクタで受け取る

この変更によりClass A内は、インスタンス化するコードが排除されClass B知らなくてよくなります。
簡単に言えばClass Aからnewを排除した訳です

この設計がDependency Injectionであり
Class AClass Bに依存しなくなることがメリットとなります

Dependency injection(依存性の注入)のメリット

  1. 実装視点ではインスタンス化するコードの排除
  2. 設計視点ではクラス間の疎結合を確保

依存関係を排除するメリット

もう少しだけ詳細に記載します。
Dependency injectionを適用することによってClass AのプログラムからClass Bへの依存関係は排除されました

実際、このことにはどのようなメリットがあるか記載していきます。

インスタンスの切り替えが容易

これは実装レベルにおける、特に実装変更時に痛感するメリットになります
例えばInterface Bの実装をClass BからClass Cに切り替える場合を想定します

Dependency injectionによって実装を切り替える図

このような場合において、Class Aには何ら変更の影響が及びません
Class Aが意識しているのは Interface BのみでありClass Bではありません。
Interface Bに対するインスタンスの切り替えはInjectorのみの変更で完結します

このケースはClass Bの実装が仕上がるまでの間StubとしてClass Cを使うといった場合に良く用いられます

依存関係の明確化

これは設計におけるメリットです。大規模開発において依存関係の複雑化は非常に厄介な問題です。

設計方針としては切り離せるはずの2つのクラスが、大量のクラスが生み出す大量の依存関係によって実は依存しており、もはや切り離すことが不可能であるというケースは少なくありません。

各クラスが2つのクラスを参照しているだけだとしてもシステム全体で見れば大量の依存関係となります。 アーキテクチャというルールだけで管理することは非常に困難です。

Dependency injectionはプログラムから依存関係を排除します。 このため原則的にInjector以外に依存関係が発生していれば、 一律設計違反と判断できます。

つまり、Dependency injectionというシステム上の仕組みで依存関係の氾濫を防止します。

このことはアーキテクチャの維持に効果をもたらします

クリーンアーキテクチャなど依存関係の方向を限定することに重きを置いているアーキテクチャではDependency injectionが適用されやすくなります

ライブラリ

最後にDependency injectionを実現するライブラリを紹介します
Dependency injectionを自力で実装する場合反復した実装が続くため作業的で退屈です
可能であればライブラリの使用をお勧めします

Android
Dagger

.NET
Microsoft.Extensions.DependencyInjection

C++
Hypodermic

また設計者視点ですが、これらのライブラリの実装を見てみると非常に参考になります。
ライブラリを使いやすくするため、インターフェースを整理し、複雑な実装は隠蔽されています。

多くのユーザーに利用されるライブラリは、それだけの理由があると言えそうです。

まとめ

本記事では、Dependency injection(依存性の注入)の概念や、使う意義、メリットなどをJavaのソースコードを用いて解説しました。
いきなりDIライブラリの使用法を見ると複雑に見えて混乱しがちですが、実現したいことは非常にシンプルです。

またDependency injection以外にも様々なデザインパターンが存在します。
下記の書籍はもはやデザインパターンのバイブルともいえる昔からある書籍ですが、最近改定されDependency Injectionも盛り込まれています。
エンジニアであれば一度は読んでおくべき一冊です。

AIで副業ならココから!

まずは無料会員登録

プロフィール

メーカーで研究開発を行う現役エンジニア
組み込み機器開発や機会学習モデル開発に従事しています

本ブログでは最新AI技術を中心にソースコード付きでご紹介します


Twitter

カテゴリ

このブログを検索

ブログ アーカイブ

TeDokology