育児で学ぶ、依存関係と依存注入

育児で学ぶ、依存関係と依存注入

はじめに

本記事では、オブジェクト指向プログラミングにおける依存関係を育児に例えて解説しています。(※多少無理のある表現もあります。)

育児に例えてみた理由として、

  • 親子の関係がオブジェクト指向プログラミングに似ている部分があると感じた。
  • 実際にコードを書いていて、「あれ?この部分は ○○ なところに似ている?」と感じる部分があった。
  • 自分の身近な環境に例えてみるとより理解が深まるかもしれないと思った。

また、オブジェクト指向プログラミングにおいて依存関係は、コードの品質や保守性に大きな影響を与える重要な概念だと考えています。

今回の記事が少しでも誰かの役に立てば幸いです。(※私の認識に誤りがあれば指摘して頂きたいです…🙇)

使用言語

サンプルコードは C#を使用しています。

登場人物

  • ムスコ(3 歳)
  • オット(ムスコの父親)
  • ジィジ(ムスコの祖父)
  • レッド(特撮ヒーロー)

依存関係の考え方

まずは依存関係を理解しましょう。

オットがムスコと遊ぶとき、ムスコはオットに依存しています(※依存度合いは個人差がありますが依存しているとします)。

image01

ただ、遊ぶだけならジィジでも遊ぶことが出来そうです。しかし、親に依存している幼児は一筋縄ではいきません。ムスコはジィジと中々遊んでくれません。

プログラムのコードにおいても A クラスが B クラスに依存している状態だと、「遊ぶ人を切り替える」ようなことが簡単には出来ません。条件分岐を加えるなど、その場しのぎの実装をしてしまうと機能の追加や修正が入った際にバグが発生してしまう可能性があります。

Visual Studio でコンソールアプリケーションを作成してその関係を見ていきます。

otto-ganbare-01

オットクラスを作成します。

"Otto.cs"
namespace Object_Sample
{
    /// <summary>
    /// オットクラス
    /// </summary>
    public class Otto
    {
        /// <summary>
        /// オットの体力
        /// </summary>
        private int _hp;

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="hp">オットの体力</param>
        public Otto(int hp)
        {
            _hp = hp;
        }

        /// <summary>
        /// 遊ぶ
        /// </summary>
        /// <returns>遊べるか否か</returns>
        public bool Play()
        {
            if(_hp < 0)
            {
                Console.WriteLine("オットはもう遊べない");

                return false;
            }

            _hp -= 20;

            return true;
        }
    }
}

Musuko クラスを作成します。

"Musuko.cs"
namespace Object_Sample
{
    /// <summary>
    /// ムスコクラス
    /// </summary>
    public class Musuko
    {
        /// <summary>
        /// ムスコの満足度
        /// </summary>
        private int _satisfaction;

        /// <summary>
        /// オットオブジェクト
        /// </summary>
        public Otto Otto { get; }

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="otto">オットオブジェクト</param>
        public Musuko(Otto otto)
        {
            Otto = otto;
        }

        /// <summary>
        /// 幸福値
        /// </summary>
        public string Happiness()
        {
            _satisfaction += 20;

            return $"ムスコの幸福値: {_satisfaction}";
        }
    }
}

実行するコードです。

"Program.cs"
using Object_Sample;

var musuko = new Musuko(new Otto(50));

while (musuko.Otto.Play())
{
    Console.WriteLine("まだ遊べる");
    Console.WriteLine($"{musuko.Happiness()}");
}

Console.WriteLine("オットは力尽きた");

上記のコードを実行してみると以下のようになります。

otto-ganbare-02

ムスコの幸福値は底なしです。満足することはありません。オットは力尽きるまで遊ぶしかありません。そう言う仕様です。

さて、話を戻しますが、遊ぶだけならオットではなくジィジでもできそうです。Grandfatherクラスを作成します。

"Grandfather.cs"
namespace Object_Sample
{
    /// <summary>
    /// ジィジクラス
    /// </summary>
    public class Grandfather
    {
        /// <summary>
        /// ジィジの体力
        /// </summary>
        private int _hp;

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="hp">ジィジの体力</param>
        public Grandfather(int hp)
        {
            _hp = hp;
        }

        /// <summary>
        /// 遊ぶ
        /// </summary>
        /// <returns>遊べるか否か</returns>
        public bool Play()
        {
            if (_hp < 0)
            {
                Console.WriteLine("ジィジはもう遊べない");

                return false;
            }

            _hp -= 20;

            return true;
        }
    }
}

早速遊び相手をジィジに変更しましょう。

"Program.cs"
using Object_Sample;

- var musuko = new Musuko(new Otto(50));
+ var musuko = new Musuko(new Grandfather(50));

while (musuko.Otto.Play())
{
    Console.WriteLine("まだ遊べる");
    Console.WriteLine($"{musuko.Happiness()}");
}

Console.WriteLine("ジィジは力尽きた");

しかしコンパイルエラーが発生してしまいました。

error-01

それもそのはず。Musuko クラスはOttoの具象クラスを受け取っているためnewしたGrandfatherを受け取ることは出来ません。Musuko クラスは Otto クラスに依存しています!

プログラムの世界でも現実世界と同じような依存が起こってしましました…。

※具象クラスとは、実際の処理が記載されたクラスのことでインスタンス化して使用するクラスです。

"Musuko.cs"
namespace Object_Sample
{
    /// <summary>
    /// ムスコクラス
    /// </summary>
    public class Musuko
    {
        /// <summary>
        /// ムスコの満足度
        /// </summary>
        private int _satisfaction;

        /// <summary>
        /// オットオブジェクト
        /// </summary>
        public Otto Otto { get; }

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="otto">オットオブジェクト</param>
        public Musuko(Otto otto) // <--★ Grandfatherを受け取ることは出来ない
        {
            Otto = otto;
        }

        // ~ 略 ~
    }
}

「遊ぶ人」に依存させたい

現在のコードではムスコはオットに依存していて、ジィジが遊んであげることは出来ません。

そこで、ムスコを「遊ぶ人」に依存するようにしたいと思います。「遊ぶ人」に依存させることで、オットかジィジのどちらかが遊ぶかは、その場にいる人によって変わります。これが依存関係の基本的な考え方です。

依存注入

ムスコは特撮ヒーローのレッドが大好きです。レッドであれば中身がオットでもジィジでも関係ありません。

image02

プログラムでこの関係を実現する方法として、依存関係注入(DI)があります。DI を使うと、子供が「遊ぶ人」に依存するように、プログラム中のクラスが特定の実装ではなくインターフェースに依存する用になります。

これにより、コードの疎結合が実現され、新しい実装を追加または変更する際に、既存のコードに影響を与えることなく行うことができます。

HeroRed インターフェースを作成する

HeroRed インターフェースを作成して、オットとジィジに実装します。

「インターフェースを実装する」という単語が使われますが、個人的にあるクラスにインターフェースを纏わせるという表現が一番しっくりきました。

ここで言うレッド(特撮ヒーロー)の衣装をオットとジィジが着るイメージです。

"IHeroRed.cs"
namespace Object_Sample
{
    public interface IHeroRed
    {
        bool Play();
    }
}

インターフェースに記述するのはこれだけです。実装先のクラスで使用するメソッドのみ定義します。

"Otto.cs"
namespace Object_Sample
{
    /// <summary>
    /// オットクラス
    /// </summary>
-   public class Otto
+   public class Otto : IHeroRed // インターフェースを実装
    {
        /// <summary>
        /// オットの体力
        /// </summary>
        private int _hp;

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="hp">オットの体力</param>
        public Otto(int hp)
        {
            _hp = hp;
        }

        /// <summary>
        /// 遊ぶ
        /// </summary>
        /// <returns>遊べるか否か</returns>
        public bool Play()
        {
            if (_hp < 0)
            {
                Console.WriteLine("オットはもう遊べない");

                return false;
            }

            _hp -= 20;

            return true;
        }
    }
}

Grandfatherクラスにも実装します。

"Grandfather.cs"
namespace Object_Sample
{
    /// <summary>
    /// ジィジクラス
    /// </summary>
-   public class Grandfather
+   public class Grandfather : IHeroRed
    {
        /// <summary>
        /// ジィジの体力
        /// </summary>
        private int _hp;

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="hp">ジィジの体力</param>
        public Grandfather(int hp)
        {
            _hp = hp;
        }

        /// <summary>
        /// 遊ぶ
        /// </summary>
        /// <returns>遊べるか否か</returns>
        public bool Play()
        {
            if (_hp < 0)
            {
                Console.WriteLine("ジィジはもう遊べない");

                return false;
            }

            _hp -= 20;

            return true;
        }
    }
}

Musukoクラスのコンストラクタに指定している具象クラスをインターフェースに変更します。

"Musuko.cs"
namespace Object_Sample
{
    /// <summary>
    /// ムスコクラス
    /// </summary>
    public class Musuko
    {
        /// <summary>
        /// ムスコの満足度
        /// </summary>
        private int _satisfaction;

        /// <summary>
        /// IHeroRedインターフェースが実装されたクラス
        /// </summary>
-       public Otto Otto { get; }
+       public IHeroRed HeroRed { get; }

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="heroRed">IHeroRedインターフェースが実装されたクラス</param>
-       public Musuko(Otto otto)
+       public Musuko(IHeroRed heroRed)
        {
-           Otto = otto;
+           HeroRed = heroRed;
        }

        /// <summary>
        /// 幸福値
        /// </summary>
        public string Happiness()
        {
            _satisfaction += 20;

            return $"ムスコの幸福値: {_satisfaction}";
        }
    }
}

Musukoクラスのコンストラクタで受け取る引数の型をIHeroRedと変更しました。

public Musuko(IHeroRed heroRed){}こうすることで、IHeroRedを実装した具象クラスであれば受け取る事ができます。

ここまでコードを変更した状態で、先程のコンパイルエラーが発生した箇所を見てみます。

error-02

Musukoクラスのコンストラクタに代入していたnew Grandfather(50)の箇所のコンパイルエラーが無くなっていることが分かります。

musuko.Otto.Play()のエラーは、プロパティに格納する型をIHeroRedに変更したのでこれもmusuko.HeroRed.Play()に変更します。

program

これで全て修正が完了しました。実行してみます。

complete-02

エラー無く実行できていることが分かります。var musuko = new Musuko(new Otto(50));も試してみます。

complete-03

こちらもエラー無く実行できました。IHeroRedインターフェースを実装したクラスであればTsumma(ツッマ)クラスを新しく作成し変更したとしてもMusukoクラスや呼び出し側のコードを変更しなくても切り替えが可能です。

program-01

complete-04

依存しないと何が良いのか?

依存関係を適切に管理することで、機能の追加や修正が行いやすいというメリットがありますが、その他にテストコードが書きやすかったり外部との接触(データベースや外部 API)を簡単に切り替えれる点が挙げられます。

例えば、データベースが用意できていない時に画面の実装を進めたい場合があったとします。

フェイクデータとしてcsvファイルを用意しておき、デバッグモード時はとりあえずデータベースの代わりにダミーデータを使用して開発を進めて、SQLServer の用意ができた時に外部との接触箇所をcsvから SQLServer に切り替える事ができます。

Factory Method でもっと簡単に

現状のコードでは、GrandfatherOttoTsummaクラスの切り替えを直接Musukoクラスのコンストラクタに代入していましたが、Factory Method パターンを使用することで簡単に切り換える事ができます。

Factory Method とは?

一言でいうと、インスタンスの生成をFactory Methodという特別なメソッドで行うことを言います。

メリットとして、切り換える具象クラスが増えてきた時にFactory Methodを修正するだけで生成するインスタンスの追加ができます。

また、インスタンスの生成に関するロジックが一箇所に集まるので、「このコードはどこに書けば良いんだっけ?」と迷うことが無く、保守も容易になります。

更に、呼び出し元のクライアントコードの機能に影響しない点も挙げられます。

Factory Method を作成する

Factories.csを作成し、以下のコードを記述します。

"Factories.cs"
namespace Object_Sample
{
    /// <summary>
    /// ファクトリークラス
    /// </summary>
    public static class Factories
    {
        /// <summary>
        /// 遊ぶ人区分
        /// </summary>
        public enum PlayKind
        {
            Otto,
            Grandfather,
            Tsumma
        }

        /// <summary>
        /// instance生成メソッド
        /// </summary>
        /// <param name="kind">遊ぶ人区分</param>
        /// <param name="hp">遊ぶ人の体力</param>
        /// <returns>IHeroRedを実装したクラス</returns>
        public static IHeroRed Create(PlayKind kind, int hp)
        {
            if (PlayKind.Otto == kind)
            {
                return new Otto(hp);
            }

            if (PlayKind.Grandfather == kind)
            {
                return new Grandfather(hp);
            }

            return new Tsumma(hp);
        }
    }
}

Factoriesクラスは、アプリケーションでただ一つのクラスで全体から呼び出せるようにしたいので、staticとしてください。

呼び出し元であるProgram.csを修正します。

"Program.cs"
using Object_Sample;

- var musuko = new Musuko(new Tsumma(50));
+ var musuko = new Musuko(Factories.Create(Factories.PlayKind.Tsumma, 50));

while (musuko.HeroRed.Play())
{
    Console.WriteLine("まだ遊べる");
    Console.WriteLine($"{musuko.Happiness()}");
}

Console.WriteLine($"{nameof(musuko.HeroRed)}は力尽きた");

Factory Method からインスタンスを生成できるようになりました。

今回の記事で使用したコードは以下のリポジトリに掲載しています。

最後に

育児とプログラミングは、一見無関係に思えるかもしれませんが、実は共通する考え方があります。

依存関係をうまく管理することで、コードの品質や保守性を向上させることができます。ぜひ、この記事を参考に、オブジェクト指向プログラミングにおける依存関係を理解し、実践してみてください。