見出し画像

C#初心者を卒業しよう(第3回)Dependency Injection


はじめに

 今回は前回のサンプルプログラムに DI コンテナを適用してみます。Dependency Injection は依存関係の注入などと翻訳される事が多く、難しそうに感じてしまうと思いますが、現状必須の技術となってきているので、頑張って使えるようになってください。
 では、早速始めましょう。

パッケージを追加する

 まずは、NuGet パッケージを追加しましょう。
 ターミナルから、以下のコマンドを入力してください。

dotnet add package Microsoft.Extensions.Hosting

早速始めましょう

 前回の Factory Method から、冒頭部分(クラスで囲っていない部分)など大分変わっているところがあるので面食らってしまうかも知れません。
 簡単に説明すると、オブジェクトを「new」で生成する代わりに、DI コンテナにオブジェクトを登録し、生成する代わりに GetService() で取得します。
 以下のサイトに詳しく説明されているので、これを参考に頑張ってみてください。

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services
    .AddTransient<DistanceService>()
    .AddScoped<ToYards>()
    .AddScoped<IDistance, ToYards>()
    .AddScoped<ToMeters>()
    .AddScoped<IDistance, ToMeters>();

using var host = builder.Build();

var service = host.Services.GetService<DistanceService>() ??
    throw new NotImplementedException(
        "Faild to create 'DistanceService' object.");
service.Run();

public interface IDistance
{
    void Calculate(double distance);
    string CreateAnswerMessage();
}

public class DistanceService(IServiceProvider serviceProvider)
{
    public void Run()
    {
        var convertNumber = InputConvertNumber();
        var distanceObject = Create(convertNumber) ??
            throw new NotImplementedException(
                "Faild to create 'IDistance' object.");

        var distance = InputDistance();
        distanceObject.Calculate(distance);

        OutputAnswer(distanceObject);
    }

    protected virtual int InputConvertNumber()
    {
        Console.WriteLine("Please input convert number.");
        Console.WriteLine("1 : Convert meters to yards.");
        Console.WriteLine("2 : Convert yards to meters.");
        if (!int.TryParse(Console.ReadLine(), out var convertNumber))
        {
            throw new FormatException("Invalid input convert number.");
        }
        return convertNumber;
    }

    protected virtual IDistance? Create(int convertNumber)
    {
        switch (convertNumber)
        {
            case 1:
                return (IDistance?)serviceProvider
                    .GetService(typeof(ToYards));
            case 2:
                return (IDistance?)serviceProvider
                    .GetService(typeof(ToMeters));
            default:
                throw new ArgumentOutOfRangeException(
                    $"{convertNumber} is out of range.");
        }
    }

    protected virtual double InputDistance()
    {
        Console.WriteLine("Please input distance.");
        if (!double.TryParse(Console.ReadLine(), out var distance))
        {
            throw new FormatException("Invalid input distance.");
        }
        return distance;
    }

    protected virtual void OutputAnswer(IDistance distance)
    {
        var outputMessage = distance.CreateAnswerMessage();
        Console.WriteLine(outputMessage);
    }
}

public class ToMeters : IDistance
{
    private double _yards;
    private double _meters;

    public void Calculate(double yards)
    {
        _yards = yards;
        _meters = yards / 1.0936133d;
    }

    public string CreateAnswerMessage()
    {
        return $"{_yards} yards is {_meters} meters.";
    }
}

public class ToYards : IDistance
{
    private double _yards;
    private double _meters;

    public void Calculate(double meters)
    {
        _meters = meters;
        _yards = meters * 1.0936133d;
    }

    public string CreateAnswerMessage()
    {
        return $"{_meters} meters is {_yards} yards.";
    }
}

ダウンロード

 ダウンロード用に置いてあるものは、クラスごとにファイルを分けてあります。

解説

 なぜこんな事をしているのか疑問に思うかも知れないところが見つかったので、少しだけ解説します。
 「NotImplementedException」を throw している所についてです。

var service = host.Services.GetService<DistanceService>() ??
    throw new NotImplementedException(
        "Faild to create 'DistanceService' object.");
service.Run();

 GetDistance() は、host.Services への登録
.AddTransient<DistanceService>()
を忘れると、null を返してしまうので、
service.run();
で NullReferenceException 例外が発生してしまいます。
var service = host.Services.GetService<DistanceService>();
としてしまうと、
service.run();
でビルド時に警告が発生します。これを安直に
service?.run();
として警告が出るのを抑制してしまうと、host.Services への登録を忘れた時に何もしないままエラーも出力せずにシレッと終了してしまいます。
 host.Services への登録を忘れるのは明らかにプログラミングミスなので、NotImprementedException 例外を throw するようにしています。
 もう一箇所同様の箇所がありますが、理由は同じです。

練習問題

 より理解を深めるために、以下の練習問題に取り組んでみてください。

練習問題 03

 今回の練習問題も、前回と全く同じです。

 このサンプルプログラムに
「メートル」を「フィート」に、
「フィート」を「メートル」に
変換できる機能を追加してください。

 機能を追加したら、前回、前々回の練習問題で作成したプログラムと比較してみてください。

おわりに

 次回は、このサンプルをリファクタリングしていきます。お楽しみに。

よろしければサポートをお願いします。 いただいたサポートは、書籍代、開発機器購入などに充てさせていただきます。