依存性注入の簡単なサンプル

前回の記事では、依存性注入のアナロジーとしてレンタカーを取り上げました。

本記事では、依存性注入の理解を深めるために、簡単なサンプルを作ってみます。

課題設定

あなたは小学1年生だとします。
今日、学校で宿題が出ました。

箱に入ったお団子の数を数えるC#のプログラムの作成です。あなたは、掛け算を知りません。

あなたには、小学3年生のお兄ちゃんがいて、掛け算を使った関数を作ることを手伝ってくれると言ってくれました。今日に限り、宿題を手伝ってもらうことと掛け算を使うことは許されています。
でも、さっき、中島と野球に行ってしまい、夕方まで帰ってきません。

どうしますか?(無茶苦茶な設定ですが・・・)

まずは、掛け算の関数の代わりに、足し算のような簡単なアルゴリズムで関数を作りませんか?その関数をとりあえず使って、動作テストしてお兄さんの帰りを待つというのはどうでしょう?お兄さんが帰ってきたら、掛け算で関数を作ってもらって、それを足し算の関数と置き換えれば、早く寝床に付くことができそうです。

足し算の関数で作ってみた

では早速プログラムを作ってみましょう。

Visual Studioを立ち上げたら、「新しいプロジェクトの作成」で、言語を「C#」、プラットフォームを「Windows」、プロジェクトの種類を「コンソール」として、「コンソールアプリ」を選んで下さい。(.NET Frameworkではない方を選んでいます。.NET Frameworkでも少しの手直しで、ほぼ同様に動作するとは思います)

プロジェクト名はDumplingとしました。「団子」という意味です。
「ソリューションとプロジェクトを同じディレクトリに配置する」は、プロジェクトを複数使う場合はチェックしない派です。

フレームワークは変更せず、「作成」をクリック。

テストプロジェクトも作ることにしましょう。

ソリューション ’Dumpling’ を右クリックして、出てきたメニューから、「追加」→「新しいプロジェクト」を選択

プロジェクトの種類を「テスト」にして、「xUnitテスト プロジェクト」を選びます。

プロジェクト名は、「XUnitTests」にしています。

XUnitTestsの「依存関係」を右クリックして「プロジェクト参照の追加」を選択。

Dumplingのチェックボックスをチェックします。

次に、依存性注入のために、NuGetパッケージを読み込みます。ソリューションエクスプローラの「Dumpling」プロジェクトを右クリックして、「NuGet パッケージの管理」を選択。

左上の「参照」を選択してから検索欄に「dependency」と打つと、「Microsoft.Extensions.DependencyInjection」が出てくるので、右の「インストール」をクリック。Abstractionsが付いていない方を選んで下さい。

同様に、XUitTestも右クリックして、「NuGet パッケージの管理」から「Microsoft.Extensions.DependencyInjection」をインストールしてください。

メインプログラム

ではいよいよ、メインプログラムを書きます。
ソリューションエクスプローラの「Program.cs」をクリックして、以下のコードをコピペします。

using Dumpling;
using Microsoft.Extensions.DependencyInjection;

Console.Write("横は何個?");
var vertical = Console.ReadLine();
if (!int.TryParse(vertical, out var x)) { return; }
Console.Write("縦は何個?");
var horizontal = Console.ReadLine();
if (!int.TryParse(horizontal, out var y)) { return; }
var services = new ServiceCollection();
services.AddTransient<ICountDumpling, ForNext>();
//services.AddTransient<ICountDumpling, Multiplication>();
Console.WriteLine($"お団子の個数は {new Calculation(services).Count(x, y)} 個です");


namespace Dumpling
{
    /// <summary>
    /// 整数を2つ受け取り、整数の計算結果を返すCountというメソッドを持つインターフェース
    /// </summary>
    public interface ICountDumpling
    {
        public int Count(int x, int y);
    }

    /// <summary>
    /// 依存性注入手段を持ったお団子の数を計算するクラス
    /// </summary>
    /// <param name="countDumpling"> ICountDumplingというインタフェースを持つクラス </param>
    public class Calculation(ServiceCollection services)
    {
        private readonly ICountDumpling _countDumpling =
            services.BuildServiceProvider().GetRequiredService<ICountDumpling>();

        // 注入されたクラスを用いて計算するメソッド
        public int Count(int x, int y)
        {
            return _countDumpling.Count(x, y);
        }
    }

    /// <summary>
    /// 足し算による計算
    /// </summary>
    public class ForNext : ICountDumpling
    {
        public int Count(int x, int y)
        {
            int sum = 0;
            for (int i = 0; i < x; i++)
            { sum += y; }
            return sum + 1;
        }
    }

    ///// <summary>
    ///// 掛け算による計算
    ///// </summary>
    //public class Multiplication : ICountDumpling
    //{
    //    public int Count(int x, int y)
    //    {
    //        return x * y - 1;
    //    }
    //}
}

初めの2行の「using」は名前空間の名前をいちいち打たなくていいようにするおまじないです。

その後に続く10行は、コマンドラインからのユーザインタフェースです。
お団子の横の個数と縦の個数を打ち込み、結果を表示するためのコードです。
ドメイン駆動設計では、プレゼンテーション層に当たります。

続く、namespace Dumplingという名前空間の領域は、Use Case層または、Domain層に相当します。

public interface ICountDumplingは、その名の通り、インタフェースの宣言です。
ここには、public int Count(int x, int y);というインタフェースが定義されています。このインタフェースに従うクラスは、xとyという2つの整数を引数として、int型の戻り値を持つ関数を必ず含まなければなりません。

車で例えると、車は、ハンドル、アクセル、ブレーキ、シフトレバーを備えていて、アクセルを踏むと走るし、ブレーキを踏むと止まると書いてあるようなものです。

次の、public class Calculation(ServiceCollection services)は、計算を実施するクラスで、プレゼンテーション層に相当するコードから呼び出されます。servicesという引数が、依存性の注入用変数です。その依存性は、呼び出し元付近の、services.AddTransient<ICountDumpling, ForNext>();に定義されています。つまり、このプログラム内では、ICountDumplingというインタフェースを持つ関数としてForNextを使うという意味です。この情報はservicesというコンテナの中に埋め込まれて渡されます。(レンタカーのアナロジーで例えれば、コンテナの中に軽トラが入っているようなイメージです)今回のような簡単な場合は、直接、ForNextを渡しても問題ありません。しかし、今後、インタフェース及び依存性の定義を増やすのであれば、コンテナの中に入れた方が、シグネチャ(引数の形態)が変わらないというメリットがあります。

依存性を注入された側のクラスCalculationでは、services.BuildServiceProvider().GetRequiredService<ICountDumpling>()というメソッドを用いることで、countDumpling という変数に、ForNextというクラスを代入します。このため、クラスの中のreturn _countDumpling.Count(x, y);という箇所は、実行時に、return _ForNext.Count(x, y);として動作します。

Calculationクラスの下に、ForNextのクラス定義があります。これは、ICountDumplingというインタフェースを備えることを宣言しています。実際、public int Count(int x, int y)という関数を備えています。

このクラス内の関数は、小学1年生が組むため、yx回足し合わせています。なお、ここでは、ForNextが実行された場合にそれと分かるように、わざと1を加えています。

では、これをCtrl+F5で実行してみます。

横3個、縦5個で、15個になりますが、わざと1を加えているので、16個で正解です。

これで、依存性の注入が一通り確認出来ました。

テストコード

折角テストプロジェクトを設けたので、テストコードも書きます。

UnitTest1.csというファイル名をDumpling_Tests.csに変更し、次のコードをコピペします。

using Dumpling;
using Microsoft.Extensions.DependencyInjection;

namespace xUnitTests
{
    public class Dumpling_Tests
    {
        [Fact]
        public void Calculation_Test()
        {
            var services = new ServiceCollection();
            services.AddTransient<ICountDumpling, ForNext>();
            Assert.Equal(16, new Calculation(services).Count(3, 5));

            //services.AddTransient<ICountDumpling, Multiplication>();
            //Assert.Equal(14, new Calculation(services).Count(3, 5));
        }
    }
}

やっていることは簡単です。
こちらでも、servicesコンテナを使ってForNextという依存性を注入します。
横3個、縦5個とした場合、期待する値は、1個余計に加えて16個というテストです。

テストエクスプローラで、「ビュー内の全てのテストを実行」(2つ重なっている右向きの▷▶のアイコン)をクリックすると丸にチェックのマークが現れて、テストの合格が確認できます。

お兄ちゃんが帰ってきたときの作業

さて、ここで、お兄ちゃんが帰ってきました。

掛け算で、クラスを作ってと頼むと、快くMultiplicationというクラスを作ってくれました。
では、Program.csで、Multiplicationのクラス全体からコメント(//)を取りましょう。
なお、Multiplicationについても、こちらが通ったことがわかるように結果から1を引くようにしています。

さらに、services.AddTransientのところのコメントの付け方を変えましょう。すなわち、もともと、

services.AddTransient<ICountDumpling, ForNext>();
//services.AddTransient<ICountDumpling, Multiplication>();

とあったところを

//services.AddTransient<ICountDumpling, ForNext>();
services.AddTransient<ICountDumpling, Multiplication>();

に変更します。

再びCtrl+F5で実行すると、

となり、正解より1個少ないので、お兄ちゃんが作ってくれたMultiplicationクラスが使われたことが判ります。

これで、計画通り、早めに就寝することができました。
めでたしめでたし。

依存性注入は、テストで本来の威力を発揮する?

今回は、お兄ちゃんが帰ってくる前に、コンソールから値を入力して動作を確認しました。
このため、ForNextクラスをProgram.csに置くことが必用でした。

しかし、将来的に、ForNextクラスがコンソールからの入力を処理することが無ければ、ForNextクラスをProgram.csからDumplig_Tests.csに切り貼りして、テストコードに持ってくることができます。

using Dumpling;
using Microsoft.Extensions.DependencyInjection;

namespace xUnitTests
{
    public class Dumpling_Tests
    {
        [Fact]
        public void ForNext_Test()
        {
            var services = new ServiceCollection();
            services.AddTransient<ICountDumpling, ForNext>();
            Assert.Equal(16, new Calculation(services).Count(3, 5));
        }

        [Fact]
        public void Multiplication_Test()
        {
            var services = new ServiceCollection();
            services.AddTransient<ICountDumpling, Multiplication>();
            Assert.Equal(14, new Calculation(services).Count(3, 5));
        }
    }

    /// <summary>
    /// 足し算による計算
    /// </summary>
    public class ForNext : ICountDumpling
    {
        public int Count(int x, int y)
        {
            int sum = 0;
            for (int i = 0; i < x; i++)
            { sum += y; }
            return sum + 1;
        }
    }
}

テストはもちろん合格します。

僕は、ファイルやDBに書き込みをするような、システムに影響を与えるクラスに対し、このようなテストコード内のダミークラスでテストすることがあります。

まとめ

少し無茶苦茶な課題設定のもとで、依存性注入の説明をしました。
テストの方法も示しました。
分かり難い箇所があったらお知らせください。

コメント

タイトルとURLをコピーしました