web-dev-qa-db-ja.com

列挙型は壊れやすいインターフェイスを作成しますか?

以下の例を検討してください。 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 )仕様の強力なバインディングを設計しており、列挙型クラスを使用して、xs:enumerationで宣言されている仕様内の型を表現しています。次のバージョン(4.0)が出たときにどうなるか考えています。クラスライブラリは3.0モードと4.0モードで動作しますか?次のバージョンが100%下位互換性がある場合は、多分そうです。しかし、列挙値が仕様から削除された場合、私は水中で死んでしまいます。

18

Enumは、適切に使用すると、置き換えられる「マジックナンバー」よりもはるかに読みやすく、堅牢になります。私は通常それらがコードをよりもろくするのを見ていません。例えば:

  • setColor()は、valueが有効なカラー値であるかどうかを確認するために時間を浪費する必要はありません。コンパイラはすでにそれを行っています。
  • SetColor(0)の代わりにsetColor(Color :: Red)と書くことができます。 enum class現代のC++の機能により、人々に後者ではなく常に前者を書くことを強制することさえできます。
  • 通常は重要ではありませんが、ほとんどの列挙型は任意のサイズの整数型で実装できるため、コンパイラーはそのようなことを考えさせることなく、最も便利なサイズを選択できます。

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

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

編集:編集後、どのような拡張性を探しているのかはまだはっきりしていませんが、コマンドパターンは、「ポリモーフィック列挙型」に最も近いものだと思います。

25
Ixrec

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

いいえ、ありません。 2つのケースがあります。実装者は、

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

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

「球」、「長方形」、「ピラミッド」を「Shape」列挙型に入れ、対応するソリッドを描画するために記述したdrawSolid()関数にそのような列挙型を渡すと、朝、 "Ikositetrahedron"値を列挙型に追加することに決めた場合、drawSolid()関数が影響を受けないままになることは期待できません。 icositetrahedronsを描画するための実際のコードを最初に記述する必要なしに、何らかの方法でicositetrahedronsを描画することを期待していた場合、それは列挙型のせいではなく、あなたのせいです。そう:

列挙型はインターフェイスを脆弱にする傾向がありますか?

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

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


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

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

したがって、ソリッドを使用して例を実装する純粋にオブジェクト指向の方法は、もちろん多態性を使用することです。すべてを描画する方法を知っている単一の巨大な集中メソッドを使用する代わりに、どのソリッドを描画するかを通知する必要がある代わりに、「抽象(純粋仮想)draw()メソッドを使用したSolidクラス、次に「Sphere」、「Rectangle」、および「Pyramid」サブクラスを追加し、それぞれに独自のdraw()の実装があります。自分自身を描く方法を知っています。

このようにして、「Ikositetrahedron」サブクラスを導入するときは、そのためのdraw()関数を提供するだけでよく、そうでない場合は「Icositetrahedron」をインスタンス化させないことで、コンパイラーはそのことを通知します。

15
Mike Nakis

列挙型は脆弱なインターフェイスを作成しません。 列挙型の誤用はそうです。

列挙型とは何ですか?

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

  • 値が削除されることはありません。
  • (そして)新しい値が必要になることはほとんどありません。
  • (または)新しい値が必要になることを受け入れますが、それが原因で壊れるすべてのコードを修正するのに十分な場合はほとんどありません。

列挙型の適切な使用法:

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

列挙型の悪い使い方:

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

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

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

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

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

この場合に代わりにすべきことは、OO原則を使用して、このタイプのインターフェースを作成することです。このタイプを通信するためにenumを保持しますが、それを使用して何かを行う必要があるとすぐに、ファクトリを使用して、enumに関連付けられたオブジェクトを作成します。更新する場所:あなたのファクトリー、そして新しいクラスを追加することによって。

4
congusbongus

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

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

上記の最後のポイントでは、列挙型が架空の状況に適していませんが、一部の仕様が変更され、一部のコードを更新する必要がある実際の状況は、ライブラリの中央レジストリに非常によく適合しているようです。したがって、他の提案を心に留めるなら、列挙型は適切なはずです。

1
MvG