LoginSignup
1
2

More than 3 years have passed since last update.

Dependency Injection入門

Posted at

はじめに

以下では、Dependency Injectionとは何なのか、またその意義について記述する。コード例にはPythonとGoを用いる。

まず、訳語について

Dependency Injectionという用語は、日本語の文脈では「依存性の注入」と訳されることが多い。たとえば、このWikipediaの項目も「依存性の注入」という名前になっている。

英語でDependency Injectionという言葉を考えると、明らかに日常的な状況では使用しない専門用語であるという印象を与えはするが、個別の単語に分解して考えれば、プログラマにとってはそれほど難解ではないだろう。

まず、プログラミングの文脈でDependencyといえば、あるソフトウェアが内部で使用している別のソフトウェアのことがすぐに頭に浮かぶはずだ。つまり、「あるプログラムが機能するために必要な別のプログラム」という意味で用いる、使用頻度の高い言葉であるといえる。

一方、Injectionについては、英語の文脈で最初に頭に浮かぶイメージは「注射」だろう。この言葉はプログラミングの文脈において頻繁に使用されるものとはいえないが、「外部に存在する物質を、何かの内部に取り込む」という感覚が、注射などの物理的な経験を通じて身体に染み付いている。

このように、英語を用いるプログラマにとっては、DependencyもInjectionも、個別の単語としてはそれほど珍奇な印象を与える言葉ではないのだ(もちろん、最初に述べたように、これらが合わされば、少なくとも初学者には奇妙に映るだろうが)。

これに対して、日本語訳である「依存性の注入」についてはどうだろうか。

「注入」については、「外部のものを、何かの内部に注ぎ入れる」という意味である。対応するInjectionとそれほど違いはないと考えられる。

だが、「依存性」という言葉については、普通の日本語の感覚からすると、「お酒も依存性のある薬物だ」といった言い方のように、薬物依存に関するものだという印象が強いのではないだろうか。あるいは、「依存性パーソナリティ障害」のような、こころの病気を思い浮かべる人もいるかも知れない。いずれにせよ、これらはプログラミングの文脈からはかけ離れた話題である。また、こうしたイメージがDependency Injectionという概念を理解するために有用であるかといえば、まったくそんなことはない。つまり、端的に言えば、「依存性」という訳語は不適切である。

後述するように、Dependency InjectionにおけるDependencyの意味は、「あるクラスや関数が機能するために必要な別のオブジェクト」のことである。したがって、Dependency Injectionの訳は、多少曖昧さを残すなら「依存対象の注入」、より具体的には「依存オブジェクトの注入」とでもするべきだったのだ。

残念ながら、日本語としては「依存性の注入」という訳語がすでに広く流通してしまっているが、上に述べたような意味で、この語は使用するべきではないと考えられる。そこで以下では、日本語訳は使用せず、Dependency Injectionの省略形であるDIによって、この語を表わすこととする。

DIとは何か

言葉による説明

ここではコードを用いずにDIについて説明する。ここでの説明をもとに、あとで具体的なコードを見ていく。

まず、少しフォーマルな定義から確認しよう。

In software engineering, dependency injection is a technique whereby one object supplies the dependencies of another object. A "dependency" is an object that can be used, for example as a service. Instead of a client specifying which service it will use, something tells the client what service to use. The "injection" refers to the passing of a dependency (a service) into the object (a client) that would use it.

これは英語版Wikipediaからの引用だ。DIについて何も知らずにこの定義を呼んだ場合、その意味と意義を理解することはかなり困難だと思うが、理解したあとに読むと実は上手くまとまっている。ポイントを抜き出そう:

  • DIとは、あるオブジェクトが別のオブジェクトに、後者が依存しているものを与えること
  • DIのDにあたるDependency「依存しているもの」とはオブジェクト
  • DIのIにあたるInjectionとは、「依存している側のオブジェクト」に「依存対象のオブジェクト」をわたすこと
  • 何かのサービスに依存しているオブジェクトはクライアントと呼ばれる

少し抽象的だが、登場人物は2つだ。図示すると、

Service (dependency) -> <Client>

のようになる。非常に単純だ。

続いて、James Shoreの一行の定義を見てみよう:

Dependency injection means giving an object its instance variables. Really. That's it.

具体的かつ簡潔だ。先ほどの話を敷衍すれば、instance variablesがdependencyに対応する。つまり、

instance variables -> <Client>

となる。Clientのインスタンス変数を外部から与えるということだ。

以上の説明では、「オブジェクト」や「インスタンス変数」という語が登場することからも明らかなように、オブジェクト指向プログラミング、すなわちOOPの思想が強く反映されている。しかし、たとえばGoなどのオブジェクト指向ではない言語においても、DIという語が用いられることはある。重要なことは、上で図示したような、Clientに依存対象を贈与するという共通構造だ。

まとめると、DIとは、あるクラスや関数が機能するために必要なコンポーネントを、その外部から提供するという構造のことだといえる。

コードを用いた説明

Python

ここまでの理解をもとに、ここからは具体的なプログラムで説明する。次のPythonの関数を見てほしい:

def greet(name):
    print(f'Hello, {name}')

特に説明はいらないだろう。nameという引数を使用して文字列を生成し、それを標準出力へと出力する関数だ。

ところで、このような関数をテストするためにはどうすればいいだろうか。REPLで挙動を確認するというのは一つの方法だが、それでは自動化できない。

ここで問題となっているのは、greet関数の出力先が標準出力へと固定されていることだといえる。標準出力へと送られた文字列が本当に期待通りになっていたかどうかを確認することが、不可能ではないが面倒なのだ。このことをDIの文脈で言い換えれば、greet関数の「出力先」という構成要素、すなわち依存対象が問題になっているといえる。

そこで、出力先をテストの際に動的に変更し、そこに送信された文字列をキャプチャすることでテストをおこなうというアイデアが出てくる。つまり、依存している出力先を動的に「注入」するのだ:

import io
import sys

def greet(name, out=sys.stdout):
    print(f'Hello, {name}', file=out)

def test_greet():
    buffer = io.StringIO()
    greet('World', buffer)
    assert buffer.getvalue() == 'Hello, World\n'

ここでのポイントは、greet関数の出力先を一時的にメモリ上のバッファbufferへと変更していることだ。もともと関数内部に組み込まれていた出力先を、引数を経由して外部からコントロールできるようにすることで、容易にテストをおこなうことができるようになった。

Go

Goを使用した別の例も見てみよう。次のような構造体があるとする。

type EmailSender struct {
    From string
}

func (sender *EmailSender) Send(to, subject, body string) error {
    // Eメールを送信する。失敗した場合はエラーを返す。
}

Eメール送信を担う構造体だ。これを利用するコードを次に示す:

type Welcomer struct{}

func (welcomer *Welcomer) Welcome(name, email string) error {
    subject := "Welcome"
    body := fmt.Sprintf("Hi, %s", name)
    sender := &EmailSender{
        From: "welcomer@example.com",
    }

    return sender.Send(email, subject, body)
}

少し複雑になってはいるが、先ほどのPythonのコードと同様の問題が生じている。すなわち、ウェルカムメッセージをメール送信するWelcomerは、Eメール送信サービスであるEmailSenderと密結合しており、使用するメール送信サービスを外部から選択できなくなってしまっている。これを解決するには

type EmailSender interface {
    Send(to, subject, body string) error
}

のようにEmailSenderinterfaceへと変更し、そしてWelcomerEmailSenderを渡すようにする:

type Welcomer struct {
    Sender EmailSender
}

func (welcomer *Welcomer) Welcome(name, email string) error {
    subject := "Welcome"
    body := fmt.Sprintf("Hi, %s", name)

    return welcomer.Sender.Send(email, subject, body)
}

interfaceを定義し、それをフィールドとして外部から受け渡し可能にすることで、Welcomerの柔軟性が向上した。これでテストが書ける:

type SpySender struct {
    SentTo string
}

func (spy *SpySender) Send(to, subject, body string) error {
    spy.SentTo = to
    return nil
}

func TestWelcome(t *testing.T) {
    sender := &SpySender{}
    welcomer := Welcomer{
        Sender: sender,
    }

    name := "Foo"
    email := "foo@example.com"
    welcomer.Welcome(name, email)

    if sender.SentTo != email {
        t.Errorf("got %q want %q", sender.SentTo, email)
    }
}

Welcomerは、依存対象として注入されたSpySenderSendメソッドを利用してメール送信を試みる。SpySenderは、実際のEメール送信はおこなわずに、引数の情報を記録しておく。そして最後に、WelcomerEmailSenderSendメソッドを期待通り使用しているかどうかを、記録された値をもとにテストする。以上が大まかな流れだ。

ここではWelcomeメソッドがSendの引数にemailをきちんと使用しているかどうかだけを調べたが、SpySenderを拡張したり、他のMockライブラリを使用したりすれば、もっと多くのことをテストできる。

DIの意義

以上のコード例から、DIの意義が垣間見えたのではないだろうか。上の例に即してまとめると、

  • 関数の挙動の変更・設定が外部から可能となり、柔軟性が向上した
  • その結果として、テストのしやすさが向上した
  • 関数の責任が明確となった、つまり、「メッセージをどこに出力するか」という問題を外部に丸投げすることで、メッセージを作成するという単一の仕事に集中できるようになった(Single Responsibility Principle、単一責務の原則)

などがいえる。このように、DIによりオブジェクト同士が疎結合化することで、SRPやテスト容易性の向上などの利点が生まれてくる。

まとめ

以上、DIの意味を説明し、コードを交えつつその意義について述べてきた。DIの利点は他にも様々あるので、より詳しくは書籍やネット上の他の記事を参照してほしい。

なお、DIはいいことばかりではない。依存対象を注入する側の責務が増大するため、オブジェクトの依存対象が増加していけば、その管理の負担もまた増加する。そうした問題を解消するためのツールとして「DIコンテナ」というものがあるが、それは次のステップで学ぶといいだろう。

参考

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2