i-Vinci TechBlog
株式会社i-Vinciの技術ブログ

DIパターンについて

はじめまして、去年の10月に入社しましたジャミーです。
まだまだエンジニアとしての経験が浅い中、日々の業務を通して様々な知識習得に励んでおります!
今回は、とくに難しく感じた概念「DIパターン」について、自分なりに理解を深めるための記事を書いていこうと思います。

目次

  1. DIパターンとは
  2. 実際に使ってみよう
  3. 最後に
  4. 参考資料

DIパターンとは

「DI」とは何なのか?簡単に言うと、依存関係を外側から定義することで、モジュール間の結びつきを弱め、それぞれを独立させるデザインパターンのことです。

「依存関係を定義する」とは、あるクラスが別のクラスの機能やデータを使用するために必要な関連付けを行うプロセスを指します。
例を挙げると、クラスAがクラスBの機能を必要とする場合を考えてみましょう。通常のアプローチでは、クラスAの内部でクラスBの新しいインスタンスを直接生成し、そのメソッドを呼び出して利用します。
しかしこれでは、クラスAはクラスBなしには機能しません。さらに、クラスBに変更があった際、クラスAにも影響が出る可能性があります。これは大きな制約となります。

ここで「外側からの定義」の出番です。このアプローチでは、クラスA内で直接インスタンス化を行わず、代わりに引数などを通じてクラスBのインスタンスを受け取ります。そして受け取る際にクラスBそのものではなく、クラスBのインターフェイスや抽象クラスを指定することで、クラスAはクラスBの具体的な実装ではなく、そのインターフェイスや抽象クラスに依存することになります。結果としてクラスBの具体的な実装が変更されても、クラスAは影響を受けにくくなります。これにより、コードの柔軟性が向上し、メンテナンスやテストが容易になるのです。

また、DIパターンのもうひとつの大きなメリットは、クラスAの機能拡張の容易さです。クラスAには大まかな流れのみを記述し、実際の処理は外部から注入されるクラスに委ねます。これにより、異なる機能や処理方法を持つ新しいクラスをクラスAに注入することで、簡単に機能を拡張することが可能になります。クラスAのコードを変更することなく、新しい要件に対応できるようになるのです。

このように、DIパターンを利用することで、アプリケーションの柔軟性と拡張性が大きく向上します。これは、とくに複雑なシステムや大規模なプロジェクトにおいて、非常に重要な利点です。

実際に使ってみよう

自分はとくに機能拡張の容易さに感動したので今回はそちらに着目しながら、簡単なプログラムにDIを利用していこうと思います。

プログラムの概要

計算をおこなうコンソールアプリ。

  1. 最初にどういった計算を行うのか選択する。

    • 足し算
    • 引き算
  2. 数値を入力する(2回)

  3. 結果を受け取る。

  4. もう一度最初の状態に戻る。

DIを使用しない書き方

Mainメソッドでは行いたい処理を受け取り、それをもとに呼び出すコントローラーを決めるようにします。

class Program
{
    static void Main(string[] args)
    {
        while (true) {
            Console.WriteLine("操作を選択してください: 1.足し算, 2.引き算");
            int choice = Convert.ToInt32(Console.ReadLine());

            Controller controller;
            switch (choice)
            {
                case 1:
                    controller = new AddController();
                    break;
                case 2:
                    controller = new SubtractController();
                    break;
                default:
                    Console.WriteLine("処理を中断します。");
                    Console.ReadLine();
                    return;
            }

            controller.Run();
        }
    }
}

コントローラーは抽象クラスと具象クラスを作成し、具象クラスには全体の流れが実装されるようにします。

    public abstract class Controller
    {
        public abstract void Run();
    }

    public class AddController : Controller
    {
        public override void Run()
        {
            AddCalculationService service = new AddCalculationService();
            var param = service.GetNumber();
            var ans = service.CalculateNumber(param);
            service.ShowAnswer(ans);
        }
    }

    public class SubtractController : Controller
    {
        public override void Run()
        {
            SubtractCalculationService service = new SubtractCalculationService();
            var param = service.GetNumber();
            var ans = service.CalculateNumber(param);
            service.ShowAnswer(ans);
        }
    }

サービスクラスではそれぞれの具体的な処理を記述します。

    public class AddCalculationService
    {
        public List<int> GetNumber() {

            List<int> param = new List<int>();
            Console.WriteLine("一つ目の数字を入力してください");
            param.Add(Convert.ToInt32(Console.ReadLine()));

            Console.WriteLine("二つ目の数字を入力してください");
            param.Add(Convert.ToInt32(Console.ReadLine()));

            return param;
        }

        public int CalculateNumber(List<int> param) {
            int ans = param[0] + param[1];
            return ans;
        }

        public void ShowAnswer(int ans) {
            Console.WriteLine("足し算の答えは {0} です", ans);
        }
    }

    public class SubtractCalculationService
    {
        public List<int> GetNumber()
        {

            List<int> param = new List<int>();
            Console.WriteLine("一つ目の数字を入力してください");
            param.Add(Convert.ToInt32(Console.ReadLine()));

            Console.WriteLine("二つ目の数字を入力してください");
            param.Add(Convert.ToInt32(Console.ReadLine()));

            return param;
        }

        public int CalculateNumber(List<int> param)
        {
            int ans = param[0] - param[1];
            return ans;
        }

        public void ShowAnswer(int ans)
        {
            Console.WriteLine("引き算の答えは {0} です", ans);
        }
    }

このやり方ではたとえば掛け算や割り算を追加するとなると、Mainメソッドのswitch部分を変更し、掛け算と割り算用のコントローラー、サービスをそれぞれ作成しなければいけません。また、コントローラーのテストを行うにはそれぞれが依存しているサービスを用意しなければいけません。これらの手間は、とくに機能の頻繁な追加や変更が求められる大規模プロジェクトで顕著な不便を生み出します。

DIを使用した書き方

ではDIパターンを使った場合どうなるでしょうか。

Mainメソッドではコントローラーを使いわけるのではなく、コントローラーをインスタンス化する際、引数に渡す(依存性の注入)サービスを使い分けるように変更します。

    class Program
    {
        static void Main(string[] args)
        {
            while (true)
            {
                Console.WriteLine("操作を選択してください: 1.足し算, 2.引き算");
                int choice = Convert.ToInt32(Console.ReadLine());

                ICalculationService calculationService;
                switch (choice)
                {
                    case 1:
                        calculationService = new AddCalculationService();
                        break;
                    case 2:
                        calculationService = new SubtractCalculationService();
                        break;
                    default:
                        Console.WriteLine("処理を中断します。");
                        return;
                }

                CalculationController controller = new CalculationController(calculationService);
                controller.Run();
            }
        }
    }

コントローラー内ではサービスのインスタンス化をおこなわず、コンストラクターでサービスのインターフェイスを受け取るようにします。

    public class CalculationController
    {
        private readonly ICalculationService _service;

        public CalculationController(ICalculationService service)
        {
            _service = service;
        }

        public void Run()
        {
            List<int> numbers = _service.GetNumbers();
            int result = _service.Calculate(numbers);
            _service.DisplayResult(result);
        }
    }

※サービスクラスはそれぞれのクラスがICalculationServiceを実装する点以外、DIパターンを使用しない例と一緒のため、割愛します。

このようにDIパターンを使うと、新しい計算機能のたびにコントローラーを新しく作る必要がなくなります。コントローラーは必要なサービスを受け取り、さまざまな計算を扱えるようになるため、プログラムがより柔軟に、再利用しやすくなります。

また、テスト時には、本物のサービスがなくとも、モックやスタブを使ってコントローラーだけをテストできます。新しい計算機能が追加されてもコントローラー自体を変更する必要がないため、テストも簡単になり、効率的な開発を行えます。

最後に

今回は計算機能を例に挙げ、DIパターンの適用を試してみました。

DIパターンのアプローチを利用すれば、様々な共通ロジックを一元的に管理することが可能です。
また新機能追加の際も、既存処理を踏襲しつつ作成することで、既存処理への影響を与えない、大部分の処理のテンプレート化等のメリットを享受することが可能です。

私自身、この方法を学んでみて、開発プロセスの大幅な効率化や、ゼロベースから始めるよりも速やかに作業を進めることを実感し、非常に効果的と感じました。

はじめてこのパターンに触れた時は、その必要性やメリットがすぐには理解できませんでした。しかし、実際にコードに適用してみると、その柔軟性と再利用性の高さ、そしてテストのしやすさに驚かされました。

今後もこのような概念を積極的に学んでいき、日々の開発作業に活かしていければと思います。

ご覧いただきありがとうございました。

参考資料

Dependency Injection Pattern を学び直す