2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

へっぽこプログラマがDIの利用例を考えてみる(C#)

Last updated at Posted at 2021-07-31

はじめに

前回の投稿ではDIって何かを考えてみました。

今回は新たなサンプルプログラムを元に、DIを使用する場面について考えてみたいと思います。

※ この記事に掲載しているサンプルプログラムは.NET Core 3.1上で動作することを確認しています。

非DIのサンプル

今回のサンプルプログラムは指定したサーバにPingを定期的に打ち、そのサーバが生きているかどうかを調べるプログラムになっています。

クラス構成は下図のようになっています。赤色の矢印は依存の向きを示しています。例えばProgramクラスはHeartBeatクラスに依存しています。

クラス図非DI.png

サンプルプログラムの中核をなすのは、HeartBeatクラスとPingCheckerクラスです。

  • HeartBeatクラスは定期的にPingを打ち、結果を画面に表示する機能を利用者(Programクラス)に提供しています。
  • PingCheckerクラスはHeartBeatクラスから使われ、実際にPingを打つ機能を提供しています。
  • HeartBeatクラスとPingCheckerクラスはクラスライブラリ(DLL)として作られ、利用者(Programクラス)に提供されています。

以下にサンプルプログラムを掲載します。ちょっと長いですが、全文を記載します。

ClassLibrary1 (DLL)

まずはクラスライブラリ側から。HeartBeatクラスのStart()メソッドでチェックを開始し、Stop()メソッドで停止します。

HeartBeat.cs
using System;
using System.Net.NetworkInformation;
using System.Threading.Tasks;

namespace ClassLibrary1
{
    public class HeartBeat
    {
        private volatile bool _running;

        public async Task Start(string url, int interval)
        {
            var checker = new PingChecker();

            _running = true;
            while (_running)
            {
                try
                {
                    var (result, time, message)  = checker.Check(url);
                    var resultText = result ? "成功" : "失敗";
                    Console.WriteLine($"{DateTime.Now}\t{resultText}\t{time}ms\t{url}\t{message}");
                }
                catch (Exception e)
                {
                    Console.WriteLine($"{e.Message}");
                }
                await Task.Delay(interval);
            }
        }

        public void Stop()
        {
            _running = false;
        }
    }

    public class PingChecker
    {
        public (bool success, long time, string message) Check(string url)
        {
            var ping = new Ping();
            PingReply reply = ping.Send(url, timeout: 1000);
            return (reply.Status == IPStatus.Success, reply.RoundtripTime, reply.Status.ToString());
        }
    }
}

HeartBeat.Start()メソッドを呼び出すと、PingCheckerクラスのインスタンスをnewしてからwhileループに突入。その後定期的にPingChecker.Check()メソッドを呼び出してその結果を表示します。

whileループは_runningフラグがfalseになるまで継続されます。HeartBeat.Stop()メソッドが呼び出されると、フラグがfalseになりwhileループを抜け出します。_runningメンバ変数は複数のスレッドからアクセスされる可能性があるためvolatile修飾子をつけています。

※ あくまでサンプルプログラムなのでエラー処理など不完全な部分があります。ご了承ください。

ConsoleApp1 (EXE)

次にHeartBeatを使う側、Programクラスを示します。

やっていることはHeartBeatクラスをnewして、Start()メソッドを呼び出し、コンソール上で何かキー入力があったらStop()メソッドを呼び出して実行を終了しているだけです。シンプルですね。

Program.cs
using ClassLibrary1;
using System;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            var beat = new HeartBeat();
            var task = beat.Start("abc.example.com", 3000);

            Console.ReadKey();
            beat.Stop();
            task.Wait();
        }
    }
}

実行結果

実行結果は以下のようになります。

2021/07/29 13:49:34     成功    17ms    abc.example.com        Success
2021/07/29 13:49:37     成功    15ms    abc.example.com        Success
2021/07/29 13:49:40     成功    15ms    abc.example.com        Success

ICMPパケットを受け付けていないサーバを指定すると以下のようになります。

2021/07/29 13:55:09     失敗    0ms     www.example.co.jp       TimedOut
2021/07/29 13:55:13     失敗    0ms     www.example.co.jp       TimedOut
2021/07/29 13:55:17     失敗    0ms     www.example.co.jp       TimedOut

DIのサンプル

さて、非DIのサンプルを見てきましたが、これで何か問題があるのでしょうか?

話がこれだけで済むなら、このままでも問題ありません。だって、Pingを打ってチェックするという機能要件は満たしているし、シンプルで分かりやすいでしょ?

ただ、将来の拡張性を考慮する必要がある場合は、ちょっと工夫したほうがよさそうです。

ある日の会話

例えば、あなたがこのクラスライブラリの作者だとして、ライブラリを使ってくれているユーザに「私はPingじゃなくて、Httpで通信できるかどうかをチェックしたいんだけど、なんとかならない?」って言われたらどうします?

クラスライブラリをHttpでもチェックできるように書き換えますか? まあ、一度だけならそれもいいかもしれません。

でも次の日に別のユーザから「俺はデータベースにアクセスできるかどうかをチェックしたいんだけど、なんとかならない?」と言われたらどうします? またその次の日には噂を聞きつけた他の人から「FTPサーバの死活チェックしたいんだけど?」って言われたら?

要求があるたびにクラスライブラリをアップデートしてすべての要求に対応できるように作り替える、っていうのも一つの選択肢です。そうすべき時もあるでしょう。

ただ、いちいち要求に応えるのもしんどいでしょ? 嫌じゃない? そう? 私は嫌です。

DIを使った解決策

そんなわけでDIを使ってそんな悩みを解決してみましょう。欲しい機能があれば、欲しい人に作ってもらうのが一番です。

まずはDIを使った場合のクラス図を見ていきましょう。

クラス図DI.png

非DIの場合のクラス図と比べると、クラスライブラリ中のHeartBeatとPingCheckerの間にICheckerインターフェイスが追加されていることが分かります。

PingCheckerと新たに作成するHttpCheckerはこのICheckerインターフェイスを実装する形にします。HeartBeatクラスはICheckerインターフェイスにのみ依存しているため、ICheckerを実装しているオブジェクトであれば実体がなんであろうと構いません。

新たに作るHttpCheckerクラスがConsoleApp2モジュールの中にあることに注意してください。これは、HttpCheckerは利用者側(クラスライブラリのユーザ)が作ることを意味しています。

では、このサンプルソースを見ていきましょう。これも長いですが、全文を掲載します。

ClassLibrary2 (DLL)

まずはクラスライブラリ側から。HeartBeatクラスのStart()メソッド、Stop()メソッドの使い方は変わりません。ソースの下の方でICheckerインターフェイスが定義され、それを実装する形でPingCheckerクラスが定義されています。PingCheckerの中身は変わっていません。

HeartBeat.cs
using System;
using System.Net.NetworkInformation;
using System.Threading.Tasks;

namespace ClassLibrary2
{
    public class HeartBeat
    {
        private IChecker _checker;
        private volatile bool _running;

        public HeartBeat(IChecker checker)
        {
            _checker = checker;
        }

        public async Task Start(string url, int interval)
        {
            _running = true;
            while (_running)
            {
                try
                {
                    var (result, time, message) = _checker.Check(url);
                    var resultText = result ? "成功" : "失敗";
                    Console.WriteLine($"{DateTime.Now}\t{resultText}\t{time}ms\t{url}\t{message}");
                }
                catch (Exception e)
                {
                    Console.WriteLine($"{e.Message}");
                }
                await Task.Delay(interval);
            }
        }

        public void Stop()
        {
            _running = false;
        }
    }

    public interface IChecker
    {
        (bool success, long time, string message) Check(string url);
    }

    public class PingChecker : IChecker
    {
        public (bool success, long time, string message) Check(string url)
        {
            var ping = new Ping();
            PingReply reply = ping.Send(url, timeout: 1000);
            return (reply.Status == IPStatus.Success, reply.RoundtripTime, reply.Status.ToString());
        }
    }
}

ではDIしている部分を見てみましょう。

コンストラクタでICheckerインターフェイスの引数を受け取って、それを後で使うために_checkerメンバ変数に格納していますね。

        private IChecker _checker;

        public HeartBeat(IChecker checker)
        {
            _checker = checker;
        }

実際にチェックを行うときには、この_checkerメンバ変数を通してCheck()メソッドを呼び出しています。_checkerの実体は新しく作るHttpCheckerクラスのインスタンス(オブジェクト)になるのですが、HeartBeatクラスにしてみればコンストラクタに渡されるオブジェクトの実体が何であろうとICheckerインターフェイスさえ実装していればいいわけで、実体が何であるかには頓着しません。
                try
                {
                    var (result, time, message) = _checker.Check(url);
                    // 中略
                }

非DIバージョンの場合はStart()メソッドの中でPingCheckerをnewしていましたが、その部分がなくなっていることに注意してください。これは、HeartBeatクラスがPingCheckerクラスに依存しなくなったことを意味しています。
        // 非DIバージョン
        public async Task Start(string url, int interval)
        {
            var checker = new PingChecker();
            // 後略

ConsoleApp2 (EXE)

次に利用者側のコードを見ていきましょう。

Programクラスの次に、HttpCheckerクラスが定義されています。HttpCheckerクラスはICheckerインターフェイスを実装しています。

HttpCheckerクラスはHttp通信を行ってサーバと通信できるかをチェックしているのですが、その中身はここではあまり重要ではありません。重要なのはHttpCheckerがICheckerインターフェイスを実装していることです。

Program.cs
using ClassLibrary2;
using System;
using System.Diagnostics;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;

namespace ConsoleApp2
{
    class Program
    {
        static void Main(string[] args)
        {
            IChecker checker = new HttpChecker();
            var beat = new HeartBeat(checker);
            var task = beat.Start("https://www.example.co.jp/", 3000);

            Console.ReadKey();
            beat.Stop();
            task.Wait();
        }
    }

    class HttpChecker : IChecker
    {
        private HttpClient _http;

        public HttpChecker()
        {
            _http = new HttpClient();
            _http.Timeout = TimeSpan.FromMilliseconds(5000);
        }

        public (bool success, long time, string message) Check(string path)
        {
            try
            {
                var sw = new Stopwatch();
                sw.Start();
                var result = _http.GetAsync(path).Result;
                sw.Stop();
                return (result.StatusCode == System.Net.HttpStatusCode.OK, sw.ElapsedMilliseconds, $"{(int)result.StatusCode} {result.StatusCode}");
            }
            catch (AggregateException e)
            {
                if (e.InnerException is TaskCanceledException)
                {
                    return (false, 0, "Timeout");
                }
                else
                {
                    throw;
                }
            }
        }
    }
}

ではProgramクラスの方にうつって、DIしている部分を見てみましょう。

最初にHttpCheckerのインスタンスをnewして、それをHeartBeatクラスのコンストラクタに渡しています。

HeartBeatが依存するオブジェクト(HttpChecker)をコンストラクタ経由でHeartBeatに注入しています。依存するオブジェクトを注入している、依存性の注入、DIしてますね!

        static void Main(string[] args)
        {
            IChecker checker = new HttpChecker();
            var beat = new HeartBeat(checker);
            // 後略

実行結果

実行結果は以下のようになります。

2021/07/29 15:37:10     成功    639ms   https://www.example.co.jp/      200 OK
2021/07/29 15:37:13     成功    279ms   https://www.example.co.jp/      200 OK
2021/07/29 15:37:17     成功    301ms   https://www.example.co.jp/      200 OK

非DIと比べて何がよくなったのか?

DIにすることで利用者から「Httpでチェックしたいんだけど」、「データベースを開けるかチェックしたいだけど」、「FTPサーバが生きてるか調べたいんだけど」とか言われても、「ICheckerインターフェイスを実装すればできるから、自分で作ってね!」と言うことができます。

楽でしょ!? そんなことない?

と、まあこんな感じなのですが、実際はこんなにうまくいかない場面も多いと思います。例えばユーザからの要望の内容によってはICheckerインターフェイス自体を改良する必要がでてくるかもしれません。そうなると、どのみちクラスライブラリを更新する必要がでてきます。

まとめ

ここまでDIの適用例についてサンプルプログラムを通して見てきました。DIすることで機能の変更(PingCheker → HttpChecker)が容易になることが分かったのではないでしょうか。

ただ、前回の投稿でも書きましたが、何が何でもDIしなきゃならないってわけではないことに注意してください。「俺は拡張性なんて求めないからDIなんていらねぇ」っておっしゃるのなら、それもありだと思います。ただ、チームで作業しててDIしてねって言われたら、その妥当性を判断して妥当なら従ってくださいね(笑)

将来的に機能を変更、拡張できるようにしたいとか、単体テストをしやすくしたいとか、DIする動機はいろいろあると思いますが、本当にDIが必要かどうかはケースバイケースだと思います。まずは必要かどうかを検討し、必要であればDIすればいいのではないかと個人的には思います。

長文、乱文失礼しました。
間違い、勘違い等あるかもしれませんがご容赦ください。なにぶん、へっぽこなもので。
この記事が何かの参考になれば幸いです。

2
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?