10分で依存性注入を理解する

The English version of this article is here.

これは記事の日本語版です。

この記事で、たった10分で依存性の注入を説明してみます。

依存性の注入とは?

「依存性の注入」とは、コードの書き方のことです。あるコードの外部リソース、すなわち「依存性」を固定することより、そのリソースを引数として受けることです。そう書く理由は、主にそのコードのテスト容易性を高めるためです。

下記は典型的な例です。

fetch_data

ある日、データベースと接続して情報と取ってくる、のようなコードを上司に依頼されたとしましょう。次を書きます。

def fetch_data(criteria):
    database = connect_to_real_database('1.2.3.4:5555')

    # 何かの特別な条件に対応してからqueryしなくては。
    if criteria.has_as_special_case():
        criteria.fix_the_special_case()

    return database.query(criteria)

私たちは「終わったぞ!」と頭の中で宣言します。でも、上司のところに行ったら、「ミシャ、この関数が動くかもしれないが、この特別なケースは結構複雑だな。テストも書いてくれる?」と弾かれます。

fetch_dataをテストする

今の実装では、データベースの接続が内包されているせいで、テストするのは相当難しいです。あるいは本番のデータベースだったらなおさら使えませんよね!

関数を工夫することにします。次のように書き直します。

def fetch_data(criteria, host):
    database = connect_to_real_database(host)

    # 何かの特別な条件に対応してからqueryしなくては。
    if criteria.has_as_special_case():
        criteria.fix_the_special_case()

    return database.query(criteria)

すっきりしました。これでテスト用環境を立ち上げて、そのデータベースに対してテストできるようになりました。危機が回避できました!

しかし、上司に見せたら、「ミシャ、あの特別な条件なんだけど。色んな組み合わせをテストして欲しい。この関数って、数万回テストするには遅すぎない?」

fetch_dataを効率的にテストする

稀ですが、今回も上司の言う通りです。データベースとやり取りするのは基本的に遅いです。テスト用のインメモリーデータベースが必要です。メモリーならとてつもなく早くなるでしょう。

とすると、次のように書き直します。

def fetch_data(criteria, database):

    # 何かの特別な条件に対応してからqueryしなくては。
    if criteria.has_as_special_case():
        criteria.fix_the_special_case()

    return database.query(criteria)

この最後の書き直しを上司に見せたら、褒めてもらいました。「よくできたね!お疲れ。」って。

まとめ

fetch_dataの最終版は、データベースはどこなのか、実際にデータベースなのか、気にしていません。テストする時、複雑なロジックのみ働かされます。そのテストは早くて簡潔です。

このようにリソースを引数として受けるコードの書き方は、「依存性の注入」と言います。上記の例と同様で、テストしやすいコードをもたらします。

注意点

確かに、データベースを作るコードは少しテストしにくくなりました。しかし、fetch_dataのような関数と比べて、そういうプログラム構築コードはめったに変わりません。修正されないものに対して、そんな複雑なテストは不要なのではないでしょうか?この妥協は納得できます。

他にもテストしやすいコードの書き方があります。例えば、構文で副作用が管理できる言語は暗に「依存性注入」で書かれているからさらに意識する必要はありません。