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のような関数と比べると、そういうプログラム構築コードはめったに変わりません。修正されないものに対して、そんな複雑なテストは不要なのではないでしょうか?この妥協は納得できます。

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