LoginSignup
1
4

More than 5 years have passed since last update.

[C#]TypeSafeEnumパターンでenumに対するswitch-case, if-elseを駆逐する => 基底クラスの実装

Last updated at Posted at 2019-02-03

enumとswitch-caseの例:四則演算

こんな四則演算のenumがあったとする


        private enum CalcType
        {
            /// <summary>足し算</summary>
            Add,

            /// <summary>引き算</summary>
            Subtract,

            /// <summary>掛け算</summary>
            Multiple,

            /// <summary>割り算</summary>
            Divide,
        }

で、計算メソッドはこんな感じ


        private decimal Calc(decimal valueL, decimal valueR, CalcType type)
        {
            switch (type)
            {
                case CalcType.Add:
                    return valueL + valueR;
                case CalcType.Subtract:
                    return valueL - valueR;
                case CalcType.Multiple:
                    return valueL * valueR;
                case CalcType.Divide:
                    return valueL / valueR;
                default:
                    throw new ArgumentOutOfRangeException(nameof(type), type, null);
            }
        }

使用方法(テストコード)はこんな感じ。(※要Chaining Assertion:Nugetから取得)


        [TestMethod]
        public void CalcTest()
        {
            // 足し算
            Calc(4, 5, CalcType.Add).Is(9);
            // 引き算
            Calc(4, 5, CalcType.Subtract).Is(-1);
            // 掛け算
            Calc(4, 5, CalcType.Multiple).Is(20);
            // 割り算
            Calc(4, 5, CalcType.Divide).Is((decimal) 0.8);
        }

・・・switch-caseは美しくない。例えばGUIアプリだと計算そのもののほかにも"+", "-", "*", "/"といった記号を表示しないといけないかもしれない。その時にまたswitch-caseで分岐するのか?とかいろいろ不安。

そこでTypeSafeEnumパターン

ステップ1:基本

TypeSafeEnumとは何か?それはジョシュア・ブロックが「Effective Java」で~とかなんとか能書きはおいといて、コードやコード。Talk is cheapや。
先ほどの四則演算enumだったら下記のように実装する。


        /// <summary>四則計算(TypeSafeEnum)</summary>
        public class CalcTypeAsSimpleTypeSafeEnum
        {
            public static readonly CalcTypeAsSimpleTypeSafeEnum Add =
                new CalcTypeAsSimpleTypeSafeEnum("足し算", "+", (v1, v2) => v1 + v2);
            public static readonly CalcTypeAsSimpleTypeSafeEnum Subtract =
                new CalcTypeAsSimpleTypeSafeEnum("引き算", "-", (v1, v2) => v1 - v2);
            public static readonly CalcTypeAsSimpleTypeSafeEnum Multiple =
                new CalcTypeAsSimpleTypeSafeEnum("掛け算", "*", (v1, v2) => v1 * v2);
            public static readonly CalcTypeAsSimpleTypeSafeEnum Divide =
                new CalcTypeAsSimpleTypeSafeEnum("割り算", "/", (v1, v2) => v1 / v2);

            private CalcTypeAsSimpleTypeSafeEnum(string name, string symbol, Func<decimal, decimal, decimal> calcFunc)
            {
                _calcFunc = calcFunc ?? throw new ArgumentNullException(nameof(calcFunc));
                Name = name;
                Symbol = symbol;
            }

            /// <summary>計算関数</summary>
            private readonly Func<decimal, decimal, decimal> _calcFunc;

            /// <summary>名称</summary>
            public string Name { get; }

            /// <summary>記号</summary>
            public string Symbol { get; }

            /// <summary>計算を実施</summary>
            /// <param name="v1">値1</param>
            /// <param name="v2">値2</param>
            /// <returns>計算結果</returns>
            public decimal Calc(decimal v1, decimal v2) => _calcFunc(v1, v2);

            public override string ToString() { return $"{Name}[{Symbol}]"; }
        }

ポイント

  • コンストラクタはprivateにし、インスタンスはpublic static readonlyで定義する
    • (初心者には違和感があるかもしれない。。当時プログラマ3年生位だった自分は最初変な感じだと思った。でもすぐに慣れた。)
  • 単なるクラスなのでプロパティやメンバ変数を定義できる
    • 普通のenumでも属性を使ったら文字列持たせられるとかいうけど、個人的にめんどいと思うので、あまりやらない。(既存コードとの兼ね合いでやることもある。。)
  • クラスのインスタンス=定数でないのでswitch-caseは逆に使えなくなる

これにより先ほどのCalcメソッドはこのように簡略化される。


        private decimal Calc(decimal v1, decimal v2, CalcTypeAsSimpleTypeSafeEnum type)
        {
            if (type == null)
            {
                throw new ArgumentNullException(nameof(type));
            }

            return type.Calc(v1, v2);
        }

かくしてswitch-caseの駆逐に成功!

ステップ2:一般的な機能追加

さて、switch-caseの駆逐に成功したあとは使い勝手の向上について考える。

実際のenumの使い方のシナリオでは例えば

  • DBにenumのキーを保存し、また逆にDBからenumのキーを読みだして復元する

とか、

  • enumの値を列挙する

とかいうことがある。そのようなシナリオに対応するために以下のようにステップ1のコードに手を加える。

        /// <summary>四則計算(TypeSafeEnum)</summary>
        public class CalcTypeAsTypeSafeEnum
        {
            public static readonly CalcTypeAsTypeSafeEnum Add =
                new CalcTypeAsTypeSafeEnum(0,"足し算", "+", (v1, v2) => v1 + v2);
            public static readonly CalcTypeAsTypeSafeEnum Subtract =
                new CalcTypeAsTypeSafeEnum(1,"引き算", "-", (v1, v2) => v1 - v2);
            public static readonly CalcTypeAsTypeSafeEnum Multiple =
                new CalcTypeAsTypeSafeEnum(2,"掛け算", "*", (v1, v2) => v1 * v2);
            public static readonly CalcTypeAsTypeSafeEnum Divide =
                new CalcTypeAsTypeSafeEnum(3,"割り算", "/", (v1, v2) => v1 / v2);

            private CalcTypeAsTypeSafeEnum(int key, string name, string symbol, Func<decimal, decimal, decimal> calcFunc)
            {
                _calcFunc = calcFunc ?? throw new ArgumentNullException(nameof(calcFunc));
                Key = key;
                Name = name;
                Symbol = symbol;
                KeyValueDictionary.Add(key, this);
            }

            private static readonly Dictionary<int, CalcTypeAsTypeSafeEnum> KeyValueDictionary =
                new Dictionary<int, CalcTypeAsTypeSafeEnum>();

            /// <summary>計算関数</summary>
            private readonly Func<decimal, decimal, decimal> _calcFunc;

            /// <summary>キー</summary>
            public int Key { get; }

            /// <summary>名称</summary>
            public string Name { get; }

            /// <summary>記号</summary>
            public string Symbol { get; }

            /// <summary>全値を取得</summary>
            public static IReadOnlyCollection<CalcTypeAsTypeSafeEnum> Values => KeyValueDictionary.Values;

            /// <summary>キーからオブジェクトを取得</summary>
            /// <param name="key">The key.</param>
            /// <param name="defaultValue">取得できなかった時のデフォルト値</param>
            /// <returns>オブジェクト</returns>
            /// <exception cref="System.ArgumentNullException">defaultValue</exception>
            public static CalcTypeAsTypeSafeEnum FromKey(int key, CalcTypeAsTypeSafeEnum defaultValue = null)
                          => KeyValueDictionary.TryGetValue(key, out var ret) ? ret : defaultValue;


            /// <summary>計算を実施</summary>
            /// <param name="v1">値1</param>
            /// <param name="v2">値2</param>
            /// <returns>計算結果</returns>
            public decimal Calc(decimal v1, decimal v2) => _calcFunc(v1, v2);

            public override string ToString() { return $"{Name}[{Symbol}]"; }
        }

ポイント

  • メンバ変数にkeyを追加(enumの値に相当)
  • static なKeyValueDictionaryを定義し、コンストラクタでkey, 値(this=自分自身のオブジェクト)を登録する
    • これによりValuesで全値を列挙できる
    • FromKeyメソッドでキーから値を取得できる

ステップ3:一般的な機能を基底クラス(TypeSafeEnumBase<>)に持たせる

ステップ2の一般的な機能は毎回実装するのは面倒。ということで共通の基底クラスを作っておく。
いろいろ実装方法はあるが、自分は以下のような感じのものを使っている。

(※これを職場が変わるたびに作り直しているのが面倒だったのでWeb上に残そうと思ったのがこのエントリのモチベーション)


    /// <summary>TypeSafeEnumBase</summary>
    public class TypeSafeEnumBase<TSelf, TKey> where TSelf : TypeSafeEnumBase<TSelf, TKey>
    {
        /// <summary>Dictionary{Key, Value}</summary>
        private static readonly Dictionary<TKey, TSelf> KeyValueDictionary = new Dictionary<TKey, TSelf>();

        static TypeSafeEnumBase()
        {
            // touch myself to initialize values
            // without this, Count might be 0 on first time
            var value =(typeof(TSelf).GetFields(BindingFlags.GetField | BindingFlags.Public | BindingFlags.Static |
                                                BindingFlags.DeclaredOnly)
                .Where(f => !f.IsLiteral)).FirstOrDefault()?.GetValue(null);
        }

        /// <summary>Initializes a new instance of the <see cref="TypeSafeEnumBase{TSelf, TKey}" /> class.</summary>
        /// <param name="key">The key.</param>
        protected TypeSafeEnumBase(TKey key)
         : this(key, key.ToString())
        {
        }

        /// <summary>Initializes a new instance of the <see cref="TypeSafeEnumBase{TSelf, TKey}"/> class.</summary>
        /// <param name="key">The key.</param>
        /// <param name="name">The name.</param>
        protected TypeSafeEnumBase(TKey key, string name)
        {
            if (KeyValueDictionary.ContainsKey(key))
            {
                throw new ArgumentException($"The key [{key}] has been already registered.");
            }

            Key = key;
            Name = name;

            // Add {key, self} to dictionary
            KeyValueDictionary.Add(key, this as TSelf);
        }

        /// <summary>Key</summary>
        public TKey Key { get; }

        /// <summary>Name</summary>
        public string Name { get; }

        /// <summary>AllValues</summary>
        public static IReadOnlyCollection<TSelf> Values => KeyValueDictionary.Values;

        /// <summary>Count</summary>
        public static int Count => KeyValueDictionary.Count;

        /// <summary>Get value by key</summary>
        /// <param name="key">The key.</param>
        /// <param name="defaultValue">The default value.</param>
        /// <returns>value</returns>
        public static TSelf FromKey(TKey key, TSelf defaultValue = null) 
                      => KeyValueDictionary.TryGetValue(key, out var ret) ? ret : defaultValue;

        /// <summary>Converts to string.</summary>
        /// <returns>A <see cref="System.String" /> that represents this instance.</returns>
        public override string ToString() => $" {Name}";
    }

ポイント

  • Keyのほかによく使うNameを標準で持つ。(指定しない場合はNameはkey.ToString()となる)
  • ジェネリックの指定型はTypeSafeEnumBaseを継承した自身の型とkeyの型。
  • staticコンストラクタで自分自身にアクセスしている。これをやらないと初回ValuesやCount(辞書)にアクセスしたときにまだ定義(Add, Subtract...)が生成されておらず、辞書が空っぽになっているので。。

で、先ほどのCalcTypeで実装するとこんな感じ


        /// <summary>四則計算(TypeSafeEnum)</summary>
        public class CalcTypeUsesTypeSafeEnumBase : TypeSafeEnumBase<CalcTypeUsesTypeSafeEnumBase, int>
        {
            public static readonly CalcTypeUsesTypeSafeEnumBase Add =
                new CalcTypeUsesTypeSafeEnumBase(0,"足し算", "+", (v1, v2) => v1 + v2);
            public static readonly CalcTypeUsesTypeSafeEnumBase Subtract =
                new CalcTypeUsesTypeSafeEnumBase(1,"引き算", "-", (v1, v2) => v1 - v2);
            public static readonly CalcTypeUsesTypeSafeEnumBase Multiple =
                new CalcTypeUsesTypeSafeEnumBase(2,"掛け算", "*", (v1, v2) => v1 * v2);
            public static readonly CalcTypeUsesTypeSafeEnumBase Divide =
                new CalcTypeUsesTypeSafeEnumBase(3,"割り算", "/", (v1, v2) => v1 / v2);

            private CalcTypeUsesTypeSafeEnumBase(int key, string name, string symbol, Func<decimal, decimal, decimal> calcFunc) 
                : base(key, name)
            {
                _calcFunc = calcFunc ?? throw new ArgumentNullException(nameof(calcFunc));
                Symbol = symbol;
            }
            /// <summary>計算関数</summary>
            private readonly Func<decimal, decimal, decimal> _calcFunc;

            /// <summary>記号</summary>
            public string Symbol { get; }

            /// <summary>計算を実施</summary>
            /// <param name="v1">値1</param>
            /// <param name="v2">値2</param>
            /// <returns>計算結果</returns>
            public decimal Calc(decimal v1, decimal v2) => _calcFunc(v1, v2);
        }

これでステップ2よりはだいぶスッキリ。

TypeSafeEnumBaseの他の使用例

もう少し実践的にわかりやすい例を示すために(嘘)、自分の好きなプログレバンドを実装してみよう。
(Hadi Hariri先生もKotlinの説明の時にプログレバンドPink Floyedを使ってたしぃ)
https://hhariri.wordpress.com/2013/11/18/refactoring-to-functional-reducing-and-flattening-lists/
↓引用

data class Track(val title: String, val durationInSeconds: Int)

val pinkFloyd = listOf(
        Album("The Dark Side of the Moon", 1973, 2, 1,
                listOf(Track("Speak to Me", 90),
                        Track("Breathe", 163),
                        Track("On he Run", 216),
                        Track("Time", 421),
                        Track("The Great Gig in the Sky", 276),
                        Track("Money", 382),
                        Track("Us and Them", 462),
                        Track("Any Color You Like", 205),
                        Track("Brain Damage", 228),
                        Track("Eclipse", 123)
                )
        ))
        // the rest omitted for brevity

TypeSafeEnumBaseの例:プログレバンド名と国籍


        /// <summary>ProgRockBands for TypeSafeEnumBase Demo</summary>
        private class ProgRockBands : TypeSafeEnumBase<ProgRockBands, int>
        {
            public static readonly ProgRockBands MoonSafari
                = new ProgRockBands(1, "Moon Safari", Country.Sweden);

            public static readonly ProgRockBands GentleGiant
                = new ProgRockBands(2, "Gentle Giant", Country.UK);

            public static readonly ProgRockBands Genesis
                = new ProgRockBands(3, "Genesis", Country.UK);

            public static readonly ProgRockBands CameliasGarden
                = new ProgRockBands(4, "Camelias Garden", Country.Italy);

            public static readonly ProgRockBands IQ
                = new ProgRockBands(5, "IQ", Country.UK);

            public static readonly ProgRockBands PFM
                = new ProgRockBands(6, "PFM", Country.Italy);

            public static readonly ProgRockBands YoninBayashi
                = new ProgRockBands(7, "四人囃子(Yonin Bayashi)", Country.Japan);

            public static readonly ProgRockBands TheFlowerKings
                = new ProgRockBands(8, "The Flower Kings", Country.Sweden);

            /// <summary>Initializes a new instance of the <see cref="ProgRockBands" /> class.</summary>
            /// <param name="key">The key.</param>
            /// <param name="name">The name.</param>
            /// <param name="country">The country.</param>
            private ProgRockBands(int key, string name, Country country)
                : base(key, name)
            {
                Country = country;
            }

            /// <summary>Country</summary>
            public Country Country { get; }
        }

        private enum Country
        {
            UK,
            Italy,
            Sweden,
            Japan
        }

上記クラスのテストコードは以下のような感じ(※要Chaining Assertion:Nugetから取得)

    <summary>ProgRockBandsTest</summary>
    [TestClass]
    public class ProgRockBandsTest
    {
        /// <summary>CountTest</summary>
        [TestMethod]
        public void CountTest()
        {
            ProgRockBands.Count.Is(8);
        }

        /// <summary>Valueses the test.</summary>
        [TestMethod]
        public void ValuesTest()
        {
            // All
            ProgRockBands.Values.Is(
                ProgRockBands.MoonSafari,
                ProgRockBands.GentleGiant,
                ProgRockBands.Genesis,
                ProgRockBands.CameliasGarden,
                ProgRockBands.IQ,
                ProgRockBands.PFM,
                ProgRockBands.YoninBayashi,
                ProgRockBands.TheFlowerKings);

            // Filter by country
            ProgRockBands.Values.Where(v => v.Country == Country.Sweden)
                .Is(ProgRockBands.MoonSafari, ProgRockBands.TheFlowerKings);
            ProgRockBands.Values.Where(v => v.Country == Country.Italy)
                .Is(ProgRockBands.CameliasGarden, ProgRockBands.PFM);
            ProgRockBands.Values.Where(v => v.Country == Country.UK)
                .Is(ProgRockBands.GentleGiant, ProgRockBands.Genesis, ProgRockBands.IQ);
            ProgRockBands.Values.Where(v => v.Country == Country.Japan).Is(ProgRockBands.YoninBayashi);
        }

        /// <summary>FromKeyTest</summary>
        [TestMethod]
        public void FromKeyTest()
        {
            ProgRockBands.FromKey(1).Is(ProgRockBands.MoonSafari);
            ProgRockBands.FromKey(8).Is(ProgRockBands.TheFlowerKings);

            // no match value => default
            ProgRockBands.FromKey(9).IsNull();
            ProgRockBands.FromKey(10, ProgRockBands.YoninBayashi).Is(ProgRockBands.YoninBayashi);
        }
   }

それでは良いプログラミングライフをノシ

1
4
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
1
4