列挙型スイッチのアンチパターン

最近、私はコードに興味深いパターンをしばしば見ます。 オブジェクトの小さなセットを記述する列挙を作成し、コード内のさまざまな場所で列挙からの値をswitchステートメントを使用して処理します。



このテンプレートの実装はどのようなもので、どのような危険がありますか? それを理解しましょう。



タスクの説明



チームがテキストエディタを開発しており、その中にいくつかのプログラミング言語のサポートを実装するとします。 もちろん、これは十分ではありません。これには十分なリソースがなく、あまり意味がありません。



サポートされている言語のリストを格納する列挙が作成されます



列挙型言語
public enum Language { Java, CSharp, TSQL }
      
      







エディターがさまざまな場所で機能するには、特定の言語に依存するパラメーターを取得する必要があります。 このために、これらの関数が作成されます:



GetExtensions(言語ラング)
 List<string> GetExtensions(Language lang) { switch (lang) { case Language.Java: { List<string> result = new List<string>(); result.Add("java"); return result; } case Language.CSharp: { List<string> result = new List<string>(); result.Add("cs"); return result; } case Language.TSQL: { List<String> result = new List<string>(); result.Add("sql"); return result; } default: throw new InvalidOperationException(" " + lang + "  "); } }
      
      







IsCaseSensitive(言語ラング)
 bool IsCaseSensitive(Language lang) { switch (lang) { case Language.Java: case Language.CSharp: return true; case Language.TSQL: return false; default: throw new InvalidOperationException(" " + lang + "  "); } }
      
      







GetIconFile(言語lang)
 string GetIconFile(Language lang) { switch (lang) { case Language.Java: return "bean.svg"; case Language.CSharp: return "cs.svg"; case Language.TSQL: return "tsql.svg"; default: throw new InvalidOperationException(" " + lang + "  "); } }
      
      







コンパイラーは、最後に例外をスローするように強制します。これは、各関数の保証された結果を提供する必要があり、C#コンパイラーは、switchステートメントで複数の値のカバレッジの完全性を制御できません。 また、新しいプログラミング言語のデフォルトアイコンを事前に考え出すことができる場合、未知の言語が大文字と小文字を区別するかどうか、およびそのソースが持つ拡張子を事前に判断することは不可能であるため、例外がスローされます。



結果として、かなり単純な画像が最初に表示されます。 サポートされている言語はすべて1か所に集められています。 何かが言語に依存する場合、スイッチが挿入されます。 1人の開発者が2〜3言語のサポートを同時に実装するのは非常に簡単です。 しかし、後に、このテンプレートに基づいたプログラムのサポートと開発により、深刻な問題が発生します。



列挙型スイッチを使用することの欠点



事実は、このアプローチが神オブジェクト(神オブジェクト)を作成するということです。 Enum自体と各スイッチは、神オブジェクトの役割を果たします。 エディターでサポートされているプログラミング言語の1つに関連する変更を行うには、すべてのgodオブジェクトに変更を加える必要があります。 Javaサポートで作業する場合、C#またはTransactSQLに関連するコードを中断できます。



複数の開発者間で言語を配布することはできないため、誰もが単一の個別の言語に対するエディターのサポートを実装します。 開発者は同じファイルに変更を加える必要があり、改善点をマージすると作業中のコードが簡単に破損する可能性があります。



switchステートメントのサイズが大きくなり、それらを制御するのがますます難しくなるため、新しい言語のサポートを追加する複雑さは常に増大します。 そのようなプログラムは高品質とは言えません。なぜなら、優れたプログラムは、時間の経過とともに開発とサポートが改善され、容易になり、安価になるからです。



開発者は、enum-switchアプローチを使用して、実際にはハードリンクと実質的に接続されていないエンティティを結合します。 誰かが単一のテキストエディタでそれらを開きたい場合を除き、TransactSQLとJavaの間に共通点はないかもしれません。 しかし、コードでは、TransactSQLとJavaは同じタイプの列挙型であることが判明しました。



これは、アンチパターンの神オブジェクトの現れです。



ただし、このパターンには他のアンチパターンの出現も見られます。 テキストエディターの開発者は、プログラミング言語の開発に参加せず、自分のソフトウェア製品のロジックの実装にのみ従事します。 したがって、エディターの場合、言語の機能は外部データであり、処理できる必要があります。 ここで、このデータはコードの一部です。 つまり、一種のハードコーディングでした。 ソースファイルの拡張子がJであるJavaが出てきたら、エディターをやり直して、他の言語が壊れているかどうかを確認する必要があります。



そのため、プログラムに記述されているセットの個々のインスタンスのパラメーターは、プログラムの動作を実装するコードから最大限に分離する必要があるデータです。



switchステートメントは、多くの場合、異なるセットのエンティティ間の関係を定義します。 この例では、これはプログラミング言語とアイコンの間の接続です。 ただし、エンティティ間の関係もエンティティであり、他のすべてのデータと同様に処理する必要があります。たとえば、テーブル列に格納されます。 外部ストレージを使用できない場合は、少なくとも辞書へのリンクを作成します。



辞書
 Dictionary<Language, string> icons = new Dictionary<Language, string>(); icons[Language.Java] = "bean.svg"; icons[Language.CSharp] = "cs.svg"; icons[Language.TSQL] = "tsql.svg";
      
      







ただし、switchステートメントには別の不快な副作用があります。 オブジェクト間の接続を定義するだけでなく、それ自体が接続です。 私たちが話していることを明確にするために、例を考えてみましょう:



 switch (lang) { case Language.TSQL: case Language.PLSQL: return "sql.svg"; ... }
      
      





SQL sql.svgアイコンは、2つのSQL方言にマップされます。 現在、言語にはアイコンだけでなく、暗黙的なプロパティもあります。つまり、TransactSQL言語とPL-SQL言語には同じアイコンが必要です。 PL-SQLのアイコンを変更する開発者は、TransactSQLのアイコンを変更するかどうかを決定します。 ほとんどの場合、これは望ましくありません。



最後に、アンチパターン列挙型スイッチは、「この値は列挙型から提供されません」などのエラーの出現に寄与します。これは、列挙型に新しい値を追加するときにすべてのスイッチステートメントの完全なカバレッジを制御することが難しいためです。



抜け道がある



このテンプレートを使用しないようにするにはどうすればよいですか?



理解できない状況では、インターフェースを開始します。 列挙型を使用しない場合は、インターフェイスを取得します。 インターフェイスは、記述されたセットからオブジェクトのプロパティに関する情報を返す必要があります。 このインターフェイスに、enumの定数に格納されていた名前を追加します。



インターフェース
 interface Language { string GetName(); bool IsCaseSensitive(); string GetIconFile(); List<string> GetExtensions(); }
      
      







このインターフェイスを実装する特定のオブジェクトを別のプロバイダークラスに割り当てます。



プロバイダー
 class LanguageProvider { List<Language> GetSupportedLanguages() { ... } Language DetectLanguageByFile(string fileName) { ... } Language GetDefaultLanguage() { .... } }
      
      







オブジェクトの説明を保存するには、任意のフレームワークを使用できます。 パラメーターをハードコーディングしたり、データベースから値を取得したり、構成ファイルから値を取得したり、外部リソースからダウンロードしたり、オブジェクトのデフォルト構成を破ったりすることができます。 プロバイダーの実装は、プロバイダーによって作成されたオブジェクトを使用するクラスの操作には影響しません。



次に、スイッチを含むすべての関数を削除します(ある場合)。 コードは特定のオブジェクトではなく、プロパティを処理するため、これらは不要になります。



上記の例では、テキストエディターが10〜15の異なるプログラミング言語をサポートした後、別の言語を追加すると、以前に実装された言語のリストから設定がリストされます。 実際、多くのプログラミング言語がありますが、ソースコードの編集に影響を与えるニュアンスのほとんどは一般的です。



列挙型が必要な理由



それでは、なぜ、ほとんどのプログラミング言語にenumなどの型があるのですか?



少し注意してこれを行うと便利です。 まず、enumはオブジェクトの数が少ない場合に適用できます。 各開発者は、自分の裁量で許容限度を決定します。 enumで20個を超える定数を結合しません。



記述されたセットはオブジェクトで構成され、その違いはパラメータ化できます。 たとえば、曜日はシリアル番号によってのみ互いに​​異なるため、enumを使用して十分に説明します。 しかし、気象現象には共通点がほとんどないため、列挙に気象現象をリストすることはおそらく価値がありません。



列挙オブジェクトのセットは、新しい値が表示されないように固定するか、1つのプログラム内でのみ完全に定義および使用される内部にする必要があります。



enumを使用する典型的な例:






All Articles