2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Unity】仕様書に強い共通の変数を宣言する

Last updated at Posted at 2024-03-22

仕様書を作っていると
色んなクラスで使える共通の変数を
作りたいと思う場面があるかもしれません。

そういった場合は
シングルトンを利用することが
多いのではないでしょうか?

しかしながら共通の変数として使用するシングルトンは
全てのクラスから実行できる仕組みなので
仕様書の段階で使えるクラスを絞る事ができません。

仕様書の段階で使うクラスを絞れないという事は
バグの原因にもなりえると思います。

そこで、仕様書の段階で
使えるクラスを絞る事ができるシングルトンを
考えてみました。

仕様書の段階で
ClassA は CommonDataA シングルトンを
使用する事ができない
という仕組みを実現します。

この方法でシングルトンを作成すると
共同開発でバグが発生する可能性が減るかもしれませんので
いつもの方法とは違う新鮮な感覚を感じながらでも
流し読みして頂けると幸いです。

また、自己流のやり方のハズですので
批評もどんどん頂けると幸いです。
「〇〇という理由でこの方法は良くない!」
といったコメントはとても参考になるので
批評だからと臆せずに
ガンガンコメントを頂けると助かります。
批評が集まりすぎたら
正しい情報を発信できていないということで
記事を削除するかもしれません


まずは基本のコードを載せます。

基本のコード
namespace CompanyName.CommonData
{
    /// <summary>
    /// 共通変数。
    /// ICommonData を継承したクラスでのみ使用可能。
    /// 継承するクラスを指定するタイプの仕様書に強い仕組み。
    /// こちらは data が protected なパターンで肥大化しないため使いやすい。
    /// </summary>
    public interface ICommonData
    {
        protected static readonly CommonData data = new(); //static フィールドを極力減らすためこの記述を行う。 ここは protected も指定可能。こちらの方が肥大化しない。

        public class CommonData
        {
            //経過ターン数
            public int NowTurnCount { get; set; } = 0;

            //特定の処理を実行しないためのフラグ1
            public bool IsNotExecuteSpecificProcess1 { get; set; } = false;

            //特定の処理を実行しないためのフラグ2
            public bool IsNotExecuteSpecificProcess2 { get; set; } = false;

            //特定の処理を実行しないためのフラグ3
            public bool IsNotExecuteSpecificProcess3 { get; set; } = false;
        }

        //継承せずとも使用したいプロパティやメソッドは public static を指定する。(非推奨)
        public static int GetTurnCount()
        {
            return data.NowTurnCount;
        }
    }
}

namespace CompanyName
{
    class ClassA : CommonData.ICommonData
    {
        static void ClassAFunction()
        {
            CommonData.ICommonData.data.NowTurnCount = 3; //OK
            CommonData.ICommonData.GetTurnCount(); //OK
        }
    }

    class ClassB
    {
        static void ClassBFunction()
        {
            //CommonData.ICommonData.data.NowTurnCount = 3; //コンパイルエラー
            CommonData.ICommonData.GetTurnCount(); //OK
        }
    }
}

という訳で解説していきます。

シングルトンっぽくないですが
これはシングルトンであるという認識でお願いします。

このシングルトンはインターフェイスで作成することで
継承したクラスでしか使えないようにする仕組みです。
インターフェイスなので、多重継承もできます。

仕様書に継承するクラス・インターフェイスを
羅列する事が前提条件にはなりますが
この仕組みならばシングルトンを使うクラスを
仕様書作成の段階で指定する事ができます。

コードを見てみると

ClassACommonData.ICommonData を継承しています。なので
CommonData.ICommonData.data
にアクセスすることができます。

しかしながら ClassBCommonData.ICommonData を継承していません。なので
CommonData.ICommonData.data
にアクセスすることができません。

仕様書に書かれていないインターフェイスを
継承する事は原則ありませんので
仕様書の段階で ClassB は
CommonData.ICommonData.data
にアクセスできない事になります。

シングルトンの種類が増えれば増えるほど
継承する必要があるインターフェイスが
増える問題がありますが
使えるクラスを限定できる分
バグが発生する可能性が減るかもしれないので
シングルトンの種類も
ガンガン増やして頂けると良いと思います。


次に応用のコードを載せます。

応用のコード
namespace CompanyName.CommonData
{
    /// <summary>
    /// プレイヤーの共通変数。
    /// IPlayerData を継承したクラスでのみ使用可能。
    /// 継承するクラスを指定するタイプの仕様書に強い仕組み。
    /// こちらは data が private なパターンで肥大化してしまうがより安全な方法。
    /// </summary>
    public interface IPlayerData
    {
        private static readonly PlayerData data = new(); //static フィールドを極力減らすためこの記述を行う。 private はより強力に制限をかける記述。

        public class PlayerData
        {
            public int hp { get; set; } = 32;
            public int maxHP { get; set; } = 32;
        }

        // data が private なので、アクセス可能にするためにプロパティを実装する
        protected static int HP
        {
            get
            {
                return data.hp;
            }
            set
            {
                data.hp = System.Math.Clamp(value, 0, data.maxHP);
                //ShowLogIPlayerData(); //デバック用
            }
        }

        protected static int MaxHP
        {
            get
            {
                return data.maxHP;
            }
            set
            {
                if (0 < value)
                {
                    data.maxHP = value;
                    if (data.maxHP < data.hp)
                    {
                        data.hp = data.maxHP;
                    }
                }
            }
        }

        //便利なメソッドも公開範囲を指定して作成できる。
        protected static void CauseDamage(int damage)
        {
            HP -= damage;
        }

        //継承せずとも使用したいプロパティやメソッドは public static を指定する。(非推奨)
        public static void ShowLog()
        {
            try
            {
                throw new System.Exception();
            }
            catch (System.Exception e)
            {
                UnityEngine.Debug.Log($"HP : {data.hp}, MaxHP : {data.maxHP}, Hierarchy is \n{e.StackTrace}");
            }
        }
    }
}

namespace CompanyName
{
    class ClassA : CommonData.IPlayerData
    {
        static void ClassAFunction()
        {
            CommonData.IPlayerData.HP = 100; //OK
            CommonData.IPlayerData.CauseDamage(5); //OK
            CommonData.IPlayerData.ShowLog(); //OK
        }
    }

    class ClassB
    {
        static void ClassBFunction()
        {
            //CommonData.IPlayerData.HP = 100; //コンパイルエラー
            //CommonData.IPlayerData.CauseDamage(5); //コンパイルエラー
            CommonData.IPlayerData.ShowLog(); //OK
        }
    }
}

応用のコードは記述量が肥大化しています。
しかしながら
CommonData.IPlayerData.data
にアクセスする方法が
ゲッターとセッターに限定されています。
肥大化する反面
安全性は高いと言って良いと感じます。

ログを出すときに
Exception を throw して
StackTrace を表示しているのは
どこから実行されたかを知るためです。
この記述をすると、たまに便利になるため
個人的に愛用している手法です。


以上が自己流かもしれませんが
仕様書に強い共通の変数を宣言する方法になります。

使って見れば分かりますが経験則上
仕様書を書くならば
共通の変数の扱いに神経を使う機会が減ります。

よろしければ皆さんも
試して頂けると幸いです。
閲覧ありがとうございました。

2
2
3

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
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?