列挙型は脆弱なインターフェースを作成しますか?


17

以下の例を考えてください。ColorChoice列挙の変更は、すべてのIWindowColorサブクラスに影響します。

列挙型は脆弱なインターフェースを引き起こす傾向がありますか?多態的な柔軟性を可能にする列挙型よりも優れたものはありますか?

enum class ColorChoice
{
    Blue = 0,
    Red = 1
};

class IWindowColor
{
public:
    ColorChoice getColor() const=0;
    void setColor( const ColorChoice value )=0;
};

編集:私の例として色を使用して申し訳ありませんが、それは質問についてのものではありません。これは、ニシンを避け、柔軟性の意味についてより多くの情報を提供する別の例です。

enum class CharacterType
{
    orc = 0,
    elf = 1
};

class ISomethingThatNeedsToKnowWhatTypeOfCharacter
{
public:
    CharacterType getCharacterType() const;
    void setCharacterType( const CharacterType value );
};

さらに、適切なISomethingThatNeedsToKnowWhatTypeOfCharacterサブクラスへのハンドルがファクトリデザインパターンによって渡されることを想像してください。現在、許容される文字タイプが{human、dwarf}である別のアプリケーション用に将来拡張できないAPIがあります。

編集:ちょうど私が取り組んでいるものについてより具体的にするために。私はこの(MusicXML)仕様の強力なバインディングを設計しており、enumクラスを使用してxs:enumerationで宣言されている仕様内のそれらのタイプを表現しています。次のバージョン(4.0)がリリースされるとどうなるかを考えています。クラスライブラリは3.0モードと4.0モードで動作しますか?次のバージョンが100%下位互換性がある場合は、おそらく。しかし、列挙値が仕様から削除された場合、私は死んでしまいます。


2
「ポリモーフィックな柔軟性」と言うとき、どのような機能を念頭に置いていますか?
Ixrec


3
色に列挙型を使用すると、「列挙型を使用する」だけでなく、脆弱なインターフェイスが作成されます。
Doc Brown

3
新しい列挙型バリアントを追加すると、その列挙型を使用したコードが破損します。一方で、列挙型に新しい操作を追加することは、完全に自己完結型です。処理する必要があるすべてのケースがすぐそこにあるためです(これをインターフェイスとスーパークラスと比較してください。それは本当に必要な変更の種類に依存します。

1
MusicXMLについて:各スキーマが使用しているスキーマのバージョンをXMLファイルから簡単に確認する方法がない場合、それは仕様の重大な設計上の欠陥として私を襲います。どうにかして回避する必要がある場合、4.0で何を壊すかを正確に把握し、それが引き起こす特定の問題について尋ねることができるまで、おそらく私たちを支援する方法はありません。
Ixrec

回答:


25

適切に使用すると、列挙型は、置き換える「マジックナンバー」よりもはるかに読みやすく、堅牢になります。通常、コードがより脆弱になることはありません。例えば:

  • setColor()valueは、有効な色の値であるかどうかをチェックする時間を無駄にする必要はありません。コンパイラはすでにそれを行っています。
  • setColor(0)の代わりにsetColor(Color :: Red)を書くことができます。私は信じているenum class近代的なCでの機能が++も、あなたはいつもの代わりに、後者の元を書くために人々を強制することができます。
  • 通常は重要ではありませんが、ほとんどの列挙型は任意のサイズの整数型で実装できるため、コンパイラはそのようなことを考えずに、最も便利なサイズを選択できます。

ただし、多くの(ほとんどの?)状況でユーザーをこのような小さな色のセットに制限する理由はないため、色に列挙型を使用するのは疑問です。任意のRGB値を渡すこともできます。私が取り組んでいるプロジェクトでは、このような色の小さなリストは、具体的な色の薄い抽象化として機能するはずの一連の「テーマ」または「スタイル」の一部としてしか登場しません。

あなたの「ポリモーフィックな柔軟性」の質問がどうなっているかはわかりません。列挙型には実行可能なコードがないため、ポリモーフィックにするものは何もありません。おそらく、コマンドパターンを探していますか?

編集:編集後、どのような拡張性を探しているのかはまだわかりませんが、コマンドパターンは「多態性列挙型」に最も近いものだと思います。


0がenumとして渡されないようにすることについての詳細はどこで見つけることができますか?
TankorSmash

5
@TankorSmash C ++ 11は、「スコープ付き列挙」とも呼ばれる「enumクラス」を導入しました。これは、暗黙的に基礎となる数値型に変換できません。また、古いCスタイルの「enum」型がしたように、名前空間を汚染することも避けます。
マシュージェームスブリッグス

2
通常、列挙型は整数によってサポートされます。シリアル化/逆シリアル化、または整数と列挙型間のキャストで発生する可能性のある奇妙なことがたくさんあります。列挙型が常に有効な値を持っていると想定することは常に安全とは限りません。
エリック

1
あなたは正しいエリックです(これは私が何度も遭遇した問題です)。ただし、逆シリアル化の段階で無効な値を心配するだけです。列挙型を使用する他のすべての時間では、値が有効であると想定できます(少なくともScalaのような言語の列挙型では、一部の言語には列挙型の非常に強力な型チェックがありません)。
キャット

1
@Ixrec「多くの(ほとんど?)状況で、ユーザーをこのような小さな色のセットに制限する理由がないため」正当なケースがあります。.Netコンソールは、16色(CGA標準の16色)msdn.microsoft.com/en-us/library/…の
Pharap

15

ColorChoice列挙の変更は、すべてのIWindowColorサブクラスに影響します。

いいえ、そうではありません。次の2つの場合があります。

  • 列挙値を保存、返送、転送し、それらを操作することはありません。その場合、列挙値の変更による影響を受けません。

  • 個々の列挙値を操作します。その場合、当然、列挙の変更は当然、必然的に、実装者のロジックの対応する変更に対応する必要があります。

「Sphere」、「Rectangle」、「Pyramid」を「Shape」列挙型に配置し、そのような列挙型をdrawSolid()対応するソリッドを描画するために作成した関数に渡すと、ある朝、「 「Ikositetrahedron」値を列挙型に追加すると、drawSolid()関数が影響を受けないままになることは期待できません。icositetrahedronsを描画するために実際のコードを最初に記述する必要なく、どういうわけかicositetrahedronsを描画すると予想した場合、それは列挙型の誤りではなく、あなたの誤りです。そう:

列挙型は脆弱なインターフェースを引き起こす傾向がありますか?

いいえ、そうではありません。脆弱なインターフェイスの原因は、十分な警告を有効にせずにコードをコンパイルしようとするプログラマーを自分自身を忍者と考えていることです。次に、コンパイラは、新しく追加された「Ikositetrahedron」列挙値の句が欠落しdrawSolid()ているswitchステートメントが関数に含まれていることを警告しませんcase

動作するはずの方法は、新しい純粋仮想メソッドを基本クラスに追加することに似ています。すべての継承者にこのメソッドを実装する必要があります。そうしないと、プロジェクトはビルドしません。


正直なところ、列挙型はオブジェクト指向の構造ではありません。それらは、オブジェクト指向のパラダイムと構造化プログラミングのパラダイムの間の実際的な妥協です。

物事を行う純粋なオブジェクト指向の方法は、列挙型をまったく持たず、代わりにオブジェクトをすべて持つことです。

したがって、ソリッドを使用して例を実装するための純粋にオブジェクト指向の方法は、もちろんポリモーフィズムを使用することです。抽象(純粋な仮想)draw()メソッドを備えたSolid」クラスを追加し、次に「Sphere」、「Rectangle」、および「Pyramid」サブクラスを追加しdraw()ます。

このように、「Ikositetrahedron」サブクラスを導入する場合、draw()関数を提供する必要があります。そうしないと、コンパイラーは「Icositetrahedron」をインスタンス化させないことで、それを行うように通知します。


これらのスイッチに対してコンパイル時の警告をスローするヒントはありますか?通常、ランタイム例外をスローします。しかし、コンパイル時間は素晴らしいかもしれません!それほど素晴らしいものではありません。しかし、単体テストが思い浮かびます。
ヴォーンヒルツ

1
前回C ++を使用してからしばらく経ちましたので、確信はありませんが、コンパイラに-Wallパラメーターを指定し、default:句を省略することでそれが行われるはずです。主題をすばやく検索しても、明確な結果は得られなかったため、これは別のprogrammers SE質問の主題として適している可能性があります。
マイクナキス

C ++では、有効にした場合、コンパイル時にチェックすることができます。C#では、実行時にチェックを行う必要があります。フラグ以外の列挙型をパラメーターとして使用する場合は、必ずEnum.IsDefinedを使用して検証しますが、それは新しい値を追加するときに列挙型のすべての使用を手動で更新する必要があることを意味します。参照:stackoverflow.com/questions/12531132/...
マイクはモニカサポート

1
オブジェクト指向プログラマーが、Pyramid実際にdraw()ピラミッドの使い方を知っているように、データ転送オブジェクトを作成しないことを願っています。せいぜいメソッドから派生しSolidGetTriangles()メソッドを持っているだけで、SolidDrawerサービスに渡すことができます。OOPのオブジェクトの例として、物理オブジェクトの例から逃げようとしていると思いました。
スコットホイットロック

1
それは大丈夫です、古き良きオブジェクト指向プログラミングを叩く人は誰も好きではありませんでした。:)特に私たちのほとんどは、厳密にOOPよりも関数型プログラミングとOOPを組み合わせているためです。
スコットホイットロック

14

列挙型は、脆弱なインターフェースを作成しません。列挙型の誤用はありません。

列挙型の目的は何ですか?

列挙型は、意味のある名前の定数のセットとして使用されるように設計されています。次の場合に使用されます。

  • 値が削除されないことを知っています。
  • (そして)あなたは、新しい価値が必要になる可能性が非常に低いことを知っています。
  • (または)新しい値が必要になることを受け入れますが、そのために壊れるすべてのコードの修正を保証するのに十分なことはめったにありません。

列挙型の優れた用途:

  • 曜日:(. NetのSystem.DayOfWeekとおり)信じられないほど不明瞭なカレンダーを扱っていない限り、週の7日間しかありません。
  • 非拡張色:(. NetのとおりSystem.ConsoleColor)これに反対する人もいるかもしれませんが、.Netは理由によりこれを選択しました。.Netのコンソールシステムでは、コンソールで使用できる色は16色のみです。これらの16色は、CGAまたは「カラーグラフィックアダプター」と呼ばれる従来のカラーパレットに対応しています。新しい値は追加されません。つまり、これは実際には列挙型の合理的なアプリケーションであることを意味します。
  • 固定状態を表す列挙:(JavaのとおりThread.State)Javaの 設計者は、Javaスレッドモデルではaの状態の固定セットのみが存在することを決定した Threadため、問題を簡素化するために、これらの異なる状態は列挙として表されます。つまり、多くの状態ベースのチェックは、実際には整数値を操作する単純なifとスイッチであり、プログラマーが実際に値が何であるかを心配する必要はありません。
  • 非相互に排他的なオプションを表すビットフラグ:(.NETの通りSystem.Text.RegularExpressions.RegexOptionsビットフラグは列挙型の非常に一般的に使用されています)。実際、よくあることですが、.Netではすべての列挙型にHasFlag(Enum flag)メソッドが組み込まれています。ビットごとの演算子もサポートしており、ビットFlagsAttributeフラグのセットとして使用するために列挙型をマークすることができます。フラグのセットとして列挙型を使用することにより、単一の値でブール値のグループを表すことができます。また、便宜上、フラグに明確な名前を付けることができます。これは、エミュレータでステータスレジスタのフラグを表す場合、またはファイルのアクセス許可(読み取り、書き込み、実行)を表す場合、または関連オプションのセットが相互に排他的ではない場合に非常に有益です。

列挙型の不適切な使用:

  • ゲームのキャラクタークラス/タイプ:ゲームが再び使用される可能性が低いワンショットデモでない限り、enumはキャラクタークラスに使用しないでください。多くのクラスを追加する可能性があるためです。キャラクターを表す単一のクラスを持ち、別の方法で表すゲーム内キャラクターの「タイプ」を持つことをお勧めします。これを処理する1つの方法はTypeObjectパターンです。他のソリューションには、文字タイプを辞書/タイプレジストリ、または2つの混合で登録することが含まれます。
  • 拡張可能な色:後で追加される可能性のある色に列挙型を使用している場合、これを列挙型で表現するのは得策ではありません。これは上記の問題に似ているため、同様のソリューションを使用する必要があります(TypeObjectのバリエーション)。
  • 拡張可能な状態:ステートマシンを使用して、さらに多くの状態を導入する可能性がある場合は、これらの状態を列挙で表すことはお勧めできません。推奨される方法は、マシンの状態のインターフェースを定義し、インターフェースをラップしてそのメソッド呼び出しを委任する実装またはクラスを提供し(Strategyパターンと同様 )、その後、現在提供されている実装を変更して状態を変更することです。
  • 相互に排他的なオプションを表すビットフラグ列挙型を使用してフラグを表し、それらのフラグのうち2つが一緒に発生することはない場合は、自分で足を踏み入れています。フラグの1つに反応するようにプログラムされているものは、最初に応答するようにプログラムされているフラグに突然反応します。さらに悪いことに、両方に応答する可能性があります。この種の状況は、単にトラブルを求めているだけです。最善のアプローチは、可能であれば、フラグの欠如を代替条件として扱うことです(つまり、Trueフラグの欠如はを意味しますFalse)。この動作は、特殊な機能(IsTrue(flags)およびIsFalse(flags))を使用してさらにサポートできます。

誰かがビットフラグとして使用されている列挙型の実際のまたはよく知られた例を見つけることができるなら、ビットフラグの例を追加します。それらが存在することは承知していますが、残念ながら現時点では思い出せません。
ファラプ


@BLSully素晴らしい例。私がそれらを使用したことがあるとは言えませんが、それらには理由があります。
ファラプ

4

列挙型は、多くの機能が関連付けられていない値の閉じたセットのマジック識別番号よりも大幅に改善されています。通常、実際に列挙型に関連付けられている数値は気にしません。この場合、最後に新しいエントリを追加することで簡単に拡張でき、脆弱性は発生しません。

問題は、列挙に関連する重要な機能がある場合です。つまり、次のようなコードがあります。

switch (my_enum) {
case orc: growl(); break;
case elf: sing(); break;
...
}

これらの1つまたは2つは問題ありませんが、この種の意味のある列挙型をswitch取得するとすぐに、これらのステートメントはトリブルのように増加する傾向があります。これで、列挙型を拡張するたびに、関連するすべてのswitchesを追い詰めて、すべてをカバーしていることを確認する必要があります。これはもろいです。Clean Codeなどのソースでは、switch列挙ごとに最大で1 つが必要であることが示唆されます。

この場合、代わりに行うべきことは、オブジェクト指向の原則を使用して、このタイプのインターフェイスを作成することです。このタイプを伝えるために列挙を保持することはできますが、何かを行う必要があるとすぐに、おそらくファクトリを使用して、列挙に関連付けられたオブジェクトを作成します。更新する場所を1つ探すだけでよいため、メンテナンスがはるかに簡単になります。それは、ファクトリであり、新しいクラスを追加することです。


1

それらの使用方法に注意を払っていれば、列挙型が有害であるとは思わないでしょう。ただし、単一のアプリケーションではなく、いくつかのライブラリコードでそれらを使用する場合は、考慮すべき点がいくつかあります。

  1. 値を削除したり並べ替えたりしないでください。ある時点でリストに列挙値がある場合、その値はすべての永遠にその名前に関連付けられている必要があります。必要に応じて、ある時点で値の名前を変更できますが、deprecated_orc削除しないことで、古い列挙セットに対してコンパイルされた古いコードとの互換性をより簡単に維持できます。一部の新しいコードが古い列挙定数を処理できない場合は、そこで適切なエラーを生成するか、そのような値がそのコードに到達しないことを確認してください。

  2. それらに対して算術を行わないでください。特に、順序の比較は行わないでください。どうして?そのため、既存の値を保持することができず、同時に正常な順序を維持できない状況が発生する可能性があるためです。コンパスの方向の列挙型を例にとります:N = 0、NE = 1、E = 2、SE = 3、…更新後に既存の方向の間にNNEなどを含める場合、最後に追加することができます。リストの順序を壊すか、既存のキーでインターリーブし、レガシーコードで使用される確立されたマッピングを壊します。または、古いキーをすべて廃止し、完全に新しいキーのセットと、レガシーコードのために古いキーと新しいキーを変換する互換性コードを用意します。

  3. 十分なサイズを選択してください。デフォルトでは、コンパイラはすべての列挙値を含むことができる最小の整数を使用します。つまり、何らかの更新で、使用可能な列挙型のセットが254から259に拡張した場合、1つの列挙値ではなく、突然2バイトが必要になります。これにより、構造やクラスのレイアウトが至る所で破損する可能性があるため、最初の設計で十分なサイズを使用してこれを回避してください。C ++ 11はここで多くの制御を提供しますが、それ以外の場合はエントリLAST_SPECIES_VALUE=65535を指定することも役立ちます。

  4. 中央レジストリを持っています。名前と値の間のマッピングを修正したいので、コードのサードパーティユーザーに新しい定数を追加させるのは悪いことです。コードを使用するプロジェクトは、新しいヘッダーを追加するためにそのヘッダーを変更できません。代わりにバグを追加して追加する必要があります。これは、あなたが言及した人間とドワーフの例では、列挙型は実際には適合性が低いことを意味します。そこでは、コードのユーザーが文字列を挿入して一意の番号を取得し、不透明な型で適切にラップできる、実行時に何らかのレジストリを用意する方がよいでしょう。「番号」は、問題の文字列へのポインタである場合もありますが、問題ではありません。

上記の私の最後の点が列挙型をあなたの仮想的な状況に適合させないという事実にもかかわらず、いくつかの仕様が変更され、いくつかのコードを更新しなければならない実際の状況は、ライブラリの中央レジストリに非常にうまく収まるようです。したがって、他の提案を心に留めておく場合は、列挙型が適切なはずです。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.