sh1’s diary

プログラミング、読んだ本、資格試験、ゲームとか私を記録するところ

DI パターンと DI コンテナについて

DI とは「dependency」を「injection」するプログラミングのデザインパターンのこと。日本語訳だと「依存性の注入」となっており、どのように意味を解釈したものかむつかしくなっています。

この部分を個人的に理解を整理した(つもりな)ので、その内容をメモ。

DI

f:id:shikaku_sh:20211011132544j:plain:w200
Martin Fowler

まず、「DI」という用語は Martin Fowler の「Inversion of Control Containers and the Dependency Injection pattern」という記事が元になっているようです。(Martin Fowler は、リファクタリング、XP、アジャイルなんかでも有名ですね)

As a result I think we need a more specific name for this pattern. Inversion of Control is too generic a term, and thus people find it confusing. As a result with a lot of discussion with various IoC advocates we settled on the name Dependency Injection.

ざっくりと雑訳:

結論をいえば、このパターンにはもっと具体的な名前が必要だと思います。Inversion of Control (制御の反転) では、あまりにも一般的な用語なので、人々を混乱させてしまう。なので、IOC を支持するさまざまな人と議論を重ねた結果、Dependency Injection という名前に落ち着きました。

で、DI の意味について WIKI を参照すると:

In software engineering, dependency injection is a technique in which an object receives other objects that it depends on, called dependencies.

DI は、あるオブジェクトが依存している他のオブジェクトを受け取るテクニックのこと、となります。

なので、DI は、デザインパターンと同じで「DI パターン」と書かれる(呼ばれる)こともあります。コーディングのテクニックなので。ただここで注意したいのは、上述の概念を満たしたテクニックはすべて DI パターンと呼ぶことができます。

DI を満たしたコーディング方法として「DI コンテナ」というものがあります。

DI コンテナ

あるオブジェクトは、受け取るオブジェクトが(n個)あると、生成コードも複雑になるので、DI を楽に実現するためにコンテナを用意して、コンテナから必要なオブジェクトを取り出して使おう、ということになります。

static void Main(string[] args)
{
    var container = GetContainer();

    var client = container.Resolve<Client>();
    client.Do1();
}

static IContainer GetContainer()
{
    var builder = new ContainerBuilder();

    builder.RegisterType<Client>();
    builder.Build();

    return builder;
}

やっていること自体は単純なのだけど、単純ゆえに自由度が高くていろいろな使い方がされたりするので、目的を区切るような注意が必要です。(以下、アンチパターンの例)

サービスロケータのアンチパターン

class Client
{
    private Container _Container;
    private ILogger _Logger;
    private IDo1 _Do1;
    private IDo2 _Do2;

    public Client(Container container)
    {
        _Container = container;

        _Logger = _Container["logger"];
        _Do1 = _Container["do1"];
        _Do2 = _Container["do2"];
    }

    public void Write(string text) => _Logger.Write(text);
    public void DoSomething1() => _Do1.Do();
    public void DoSomething2() => _Do2.Do();
}

DI コンテナをコンストラクタに渡して、クラスの中でコンテナ内のオブジェクトを探すパターンを、DI パターンの DI コンテナ を利用した手法の中でも、サービスロケータと呼んでいます。(歴史的な流れから別パターン扱いすることもあります)

サービスロケータは、DI コンテナをクラスの中に組み込んだことで、(DI コンテナ自体と)より蜜月な依存関係を作りこんでしまいます。なので、あまり推奨されない傾向にあります。

言い換えると、これは DI コンテナから依存性のあるオブジェクトを受け取っておらず、自ら DI コンテナに問い合わせて、依存性のある必要なオブジェクトを取得しています。

ただし、これを許容する場合もあります:

class Client
{
    [Dependency("logger")]
    private ILogger _Logger;
    [Dependency("do1")]
    private IDo1 _Do1;
    [Dependency("do2")]
    private IDo2 _Do2;

    public void Write(string text) => _Logger.Write(text);
    public void DoSomething1() => _Do1.Do();
    public void DoSomething2() => _Do2.Do();
}

次のようにリファクタリングすることもあります:

class Client
{
    private ILogger _Logger;
    private IDo1 _Do1;
    private IDo2 _Do2;

    public Client(ILogger logger, IDo1 do1, IDo2 do2)
    {
        _ILogger = logger;
        _Do1 = do1;
        _Do2 = do2;
    }

    public void Write(string text) => _Logger.Write(text);
    public void DoSomething1() => _Do1.Do();
    public void DoSomething2() => _Do2.Do();
}

実際のところは、サービスロケータでも必要なオブジェクトを受け取ることができてしまっているので、両者の違いを区別しづらいところはある。

DI はインターフェースに依存するため、「具体ではなく、抽象に依存する」というSOLID 原則の D(依存性逆転の原則)を満たすことから、テストがやりやすくなるメリットを主に受けることができます。

サービスロケータは、WPF の場合だと VM に DI を利用してテストしやすくするかと言えば微妙なので、そういうところでは Prism なんかはサービスロケータ寄りになってたりもする。(けど、DB からデータ(レコード)を受け取る部分を DI で作るなら、サービスロケータっぽくはしません。ケースバイだと思います)

テストと DI

テストにも少し触れたので補足します。

こういう記事があって、 DI はテストを簡単にするんでエライという内容です。でも、コメントで否定的な意見が結構ついてる。

なお、最初のころから DI を支持する共通の理由に「テストを簡単にする」というものは含まれています。(2004 年の記事です)

A common reason people give for preferring dependency injection is that it makes testing easier.

Inversion of Control Containers and the Dependency Injection pattern - Martin Fowler

なので、DI を説明する際には、テストから話を進めてもいいと思うし、SOLID 原則から話を進めてもよくて、好みの問題だったり、鶏が先か卵が先か (chicken or the egg) の話だと思います。

まとめ

  • DI (DI パターン)
    あるオブジェクトが依存している他のオブジェクトを受け取るテクニックのこと
  • DI コンテナ
    DI を楽に実現するためにコンテナを用意して、コンテナから必要なオブジェクトを取り出して使おう、という考え
  • サービスロケータ
    DI コンテナを利用したコーディングテクニックのひとつで、注意が必要なアンチパターン(あるいは DI とは別のパターンとも言われる。歴史的な順序の問題などで)

なによりも重要なことは、サービスの構成とその使用を確実に分離すること。これは、「インターフェースを実装から分離する」原則と同様の基本的な設計です。

The important issue in all of this is to ensure that the configuration of services is separated from their use. Indeed this is a fundamental design principle that sits with the separation of interfaces from implementation.

Inversion of Control Containers and the Dependency Injection pattern - Martin Fowler

参考