列挙型はコードの匂いではありませんか?


17

ジレンマ

オブジェクト指向の実践に関するベストプラクティスの本をたくさん読んでいますが、読んだ本のほとんどすべてに、enumはコードの匂いだと言う部分がありました。列挙型が有効な場合に説明する部分を見逃したと思います。

そのように、私を探していますガイドラインおよび/またはユースケースの列挙型がありませ有効な構文コードのにおいと、実際に。

ソース:

「警告経験則として、列挙型はコードの匂いであり、多態性クラスにリファクタリングする必要があります。[8]」Seemann、Mark 、. 342

[8] Martin Fowler et al。、Refactoring:Improving the Design of Existing Code(New York:Addison-Wesley、1999)、82。

環境

私のジレンマの原因は取引APIです。このメソッドを介して送信することにより、Tickデータのストリームを提供します。

void TickPrice(TickType tickType, double value)

どこ enum TickType { BuyPrice, BuyQuantity, LastPrice, LastQuantity, ... }

変更を壊すことがこのAPIの生き方であるため、このAPIのラッパーを作成しようとしました。ラッパーで最後に受信した各ティックタイプの値を追跡したいので、ダニタイプのディクショナリを使用してそれを行いました。

Dictionary<TickType,double> LastValues

私には、キーとして使用される場合、これは列挙型の適切な使用のように思えました。しかし、私はこのコレクションに基づいて意思決定を行う場所があり、switchステートメントを排除する方法を考えることができないため、工場を使用できますが、その工場にはまだどこかにswitchステートメント。私はただ物事を動かしているように見えましたが、まだ匂いがします。

列挙型のDO N'Tを見つけるのは簡単ですが、DOはそれほど簡単ではありません。人々が自分の専門知識、長所と短所を共有できるなら、私はそれを感謝します。

考え直し

一部の決定とアクションはこれらに基づいておりTickType、enum / switchステートメントを削除する方法を考えることはできないようです。私が考えることができる最もクリーンなソリューションは、ファクトリを使用し、に基づいて実装を返すことTickTypeです。それでも、インターフェイスの実装を返すswitchステートメントがあります。

以下は、間違った列挙型を使用しているのではないかと疑っているサンプルクラスの1つです。

public class ExecutionSimulator
{
  Dictionary<TickType, double> LastReceived;
  void ProcessTick(TickType tickType, double value)
  {
    //Store Last Received TickType value
    LastReceived[tickType] = value;

    //Perform Order matching only on specific TickTypes
    switch(tickType)
    {
      case BidPrice:
      case BidSize:
        MatchSellOrders();
        break;
      case AskPrice:
      case AskSize:
        MatchBuyOrders();
        break;
    }       
  }
}

25
列挙型がコードの匂いだと聞いたことはありません。参照を含めてもらえますか?私は、限られた数の潜在的な価値に対して非常に理にかなっていると思います
リチャード・

23
列挙型はコードの匂いだと言っている本は何ですか?より良い本を入手してください。
ジャックB

3
したがって、質問に対する答えは「ほとんどの時間」になります。
gnasher729

5
意味を伝える素敵な、タイプセーフな値?適切なキーが辞書で使用されることを保証しますか?コードの匂いはいつですか?

7
文脈がなければ、私はより良い言葉遣いになると思うenums as switch statements might be a code smell ...
15年

回答:


31

列挙型は、変数が取り得るすべての可能な値を文字通り列挙したユースケースを対象としています。今まで。曜日や月、ハードウェアレジスタの構成値などのユースケースを考えます。安定性が高く、単純な値で表現できるもの。

腐敗防止レイヤーを作成している場合、ラッピングしているデザインのために、switchステートメントがどこかにあることを避けることはできませんが、それを正しく行う場合は、その1つの場所に制限し、他の場所でポリモーフィズムを使用します。


3
これが最も重要なポイントです-列挙に追加の値を追加することは、コード内の型のすべての使用を見つけることを意味し、潜在的に新しい値に新しいブランチを追加する必要があります。ポリモーフィック型と比較して、新しい可能性を追加するということは、単にインターフェースを実装する新しいクラスを作成することを意味します。変更は1か所に集中しているので、簡単に作成できます。新しいティックタイプが追加されないことが確実な場合、enumは問題ありません。そうでない場合は、ポリモーフィックインターフェイスでラップする必要があります。
ジュール

それは私が探していた答えの一つです。ラップしているメソッドが列挙型を使用していて、これらの列挙型を1か所に集中させることが目的の場合、それを避けることはできません。APIがこれらの列挙型を変更した場合、ラッパーを変更するだけで、ラッパーのコンシューマーは変更しないように、ラッパーを作成する必要があります。
ストロム

@Jules-「新しいティックタイプが追加されないことが確実な場合...」そのステートメントは多くのレベルで間違っています。各タイプの動作がまったく同じであるにもかかわらず、すべてのタイプのクラスは、「合理的な」アプローチとはほど遠いものです。さまざまな動作があり、線を描く必要がある「switch / if-then」ステートメントが必要になるまではそうではありません。確かに、将来さらに列挙値を追加するかどうかに基づいているわけではありません。
ダンク

@dunkあなたは私のコメントを誤解したと思います。まったく同じ振る舞いを持つ複数のタイプを作成することを提案したことはありません。
ジュール

1
「考えられるすべての値を列挙した」としても、型ごとにインスタンスごとに追加の機能が追加されることはありません。月のようなものでさえ、日数のような他の有用なプロパティを持つことができます。列挙型を使用すると、モデリングしている概念がすぐに拡張されなくなります。基本的には、OOP防止メカニズム/パターンです。
デイブCousineau

17

まず、コード臭は何かが間違っているという意味ではありません。それは何かが間違っているかもしれないことを意味します。enum頻繁に乱用されているためにおいがしますが、それはあなたがそれらを避けなければならないという意味ではありません。入力するだけでenum、停止し、より良い解決策を確認できます。

最も頻繁に発生する特定のケースは、さまざまな列挙型が、さまざまな動作で同じインターフェースを持つさまざまなタイプに対応する場合です。たとえば、さまざまなバックエンドとの対話、さまざまなページのレンダリングなど。これらは、ポリモーフィッククラスを使用してより自然に実装されます。

あなたの場合、TickTypeは異なる動作に対応していません。これらは、さまざまなタイプのイベントまたは現在の状態のさまざまなプロパティです。だから、これは列挙型にとって理想的な場所だと思います。


列挙型に名詞がエントリ(色など)として含まれている場合、おそらく正しく使用されていると言っていますか?そして、それらが動詞(Connected、Rendering)である場合、それは誤用されている兆候ですか?
ストロム

2
@stromms、文法はソフトウェアアーキテクチャの恐ろしいガイドです。TickTypeが列挙型ではなくインターフェイスである場合、メソッドはどうなりますか?私には何も見えません、それが私にとって列挙型のケースのようです。ステータス、色、バックエンドなどのその他のケースでは、メソッドを考え出すことができます。それが、それらがインターフェースである理由です。
ウィンストンイーバート

@WinstonEwertと編集により、それら異なる動作であるように見えます。

ああ だから列挙型のメソッドを考えることができれば、それはインターフェースの候補かもしれませんか?それは非常に良い点かもしれません。
ストロム

1
@stromms、スイッチのより多くの例を共有できますか?それで、TickTypeの使用方法についてより良いアイデアを得ることができますか?
ウィンストンイーバート

8

データ列挙を送信するとき、コードの匂いはありません

私見、データを送信するときにenums、フィールドが制限された(めったに変更されない)値のセットから値を持つことができることを示すのは良いことです。

私は、任意の送信することが望ましい考えますstringsints。文字列は、スペルや大文字の違いによって問題を引き起こす可能性があります。Intsは範囲外の値の送信を許可し、セマンティクスはほとんどありません(たとえば3、取引サービスから受け取った場合、どういう意味ですかLastPrice?)LastQuantity?他に何か?

オブジェクトの送信とクラス階層の使用は常に可能とは限りません。たとえば、では、受信側がどのクラスが送信されたかを区別できません。

私のプロジェクトでは、サービスは操作の効果のためにクラス階層を使用し、クラス階層DataContractからオブジェクトを介して送信する直前に、タイプを示すためにunionを含む-のようなオブジェクトにコピーされますenum。クライアントは、値をDataContract使用して階層内のクラスのオブジェクトを受け取り、作成し、正しいタイプのオブジェクトを作成します。enumswitch

クラスのオブジェクトを送信したくないもう1つの理由は、送信されたオブジェクト(などLastPrice)に対して、クライアントとはまったく異なる動作がサービスで必要になる場合があることです。その場合、クラスとそのメソッドを送信することは望ましくありません。

switchステートメントは悪いですか?

私見、に応じて異なるコンストラクタを呼び出す単一の switchステートメントenumは、コードの匂いではありません。型名に基づいたリフレクションなど、他のメソッドよりも必ずしも良いまたは悪いわけではありません。これは実際の状況に依存します。

enumいたるところにスイッチをはコードのにおいであり、はしばしばより良い代替手段を提供します

  • オーバーライドされたメソッドを持つ階層のさまざまなクラスのオブジェクトを使用します。すなわち多型。
  • 階層内のクラスがほとんど変更されない場合、および(多くの)操作を階層内のクラスに疎結合する必要がある場合は、訪問者パターンを使用します。

実際、TickTypeはとしてワイヤを介して送信されていintます。私のラッパーはTickType、受信しintたからキャストされたを使用するラッパーです。いくつかのイベントは、さまざまなリクエストへの応答であるワイルドに変化するシグネチャでこのティックタイプを使用します。intさまざまな機能に継続的に使用することは一般的な習慣ですか?たとえばTickPrice(int type, double value)、2、4、および5 typeTickSize(int type, double value使用している間、1,3、および6 を使用しますか?これらを2つのイベントに分けることも理にかなっていますか?
ストロム

2

より複雑なタイプを使用した場合:

    abstract class TickType
    {
      public abstract string Name {get;}
      public abstract double TickValue {get;}
    }

    class BuyPrice : TickType
    {
      public override string Name { get { return "Buy Price"; } }
      public override double TickValue { get { return 2.35d; } }
    }

    class BuyQuantity : TickType
    {
      public override string Name { get { return "Buy Quantity"; } }
      public override double TickValue { get { return 4.55d; } }
    }
//etcetera

その後、リフレクションからタイプをロードするか、自分でビルドすることができますが、ここで最初に行うことは、SOLIDのOpen Close Principleを保持することです


1

TL; DR

質問に答えるために、私は今、列挙型がそうでない時間を考えるのに苦労していますあるレベルではコードの匂いでしています。効率的に宣言するという明確な意図がありますが(この値には明確に制限された、限られた数の可能性があります)、本質的に閉じた性質により、それらはアーキテクチャ的に劣ります。

レガシーコードを大量にリファクタリングするので、すみません。/ため息; ^ D


しかし、なぜ?

これがLosTechiesのケースである理由はかなり良いレビューです

// calculate the service fee
public double CalculateServiceFeeUsingEnum(Account acct)
{
    double totalFee = 0;
    foreach (var service in acct.ServiceEnums) { 

        switch (service)
        {
            case ServiceTypeEnum.ServiceA:
                totalFee += acct.NumOfUsers * 5;
                break;
            case ServiceTypeEnum.ServiceB:
                totalFee += 10;
                break;
        }
    }
    return totalFee;
} 

これには、上記のコードと同じ問題がすべてあります。アプリケーションが大きくなると、同様の分岐ステートメントが含まれる可能性が高くなります。また、より多くのプレミアムサービスを展開すると、このコードを継続的に変更する必要があり、これはオープンクローズドプリンシプルに違反します。ここにも他の問題があります。サービス料金を計算する機能は、各サービスの実際の金額を知る必要はありません。それはカプセル化する必要がある情報です。

ちょっとしたことですが、列挙型は非常に限られたデータ構造です。ラベル付き整数である列挙型を実際に使用していない場合は、抽象化を正しくモデル化するためのクラスが必要です。Jimmyの素晴らしいEnumerationクラスを使用して、クラスをラベルとして使用することもできます。

これをリファクタリングして、多態的な動作を使用しましょう。必要なのは、サービスの料金を計算するために必要な動作を含めることができる抽象化です。

public interface ICalculateServiceFee
{
    double CalculateServiceFee(Account acct);
}

...

これで、インターフェイスの具体的な実装を作成し、アカウントに添付できます。

public class Account{
    public int NumOfUsers{get;set;}
    public ICalculateServiceFee[] Services { get; set; }
} 

public class ServiceA : ICalculateServiceFee
{
    double feePerUser = 5; 

    public double CalculateServiceFee(Account acct)
    {
        return acct.NumOfUsers * feePerUser;
    }
} 

public class ServiceB : ICalculateServiceFee
{
    double serviceFee = 10;
    public double CalculateServiceFee(Account acct)
    {
        return serviceFee;
    }
} 

別の実装事例...

一番下の行は、列挙値に依存する動作がある場合、代わりに値が存在することを保証する同様のインターフェースまたは親クラスの異なる実装を持たないのはなぜですか?私の場合、RESTステータスコードに基づいてさまざまなエラーメッセージを見ています。の代わりに...

private static string _getErrorCKey(int statusCode)
{
    string ret;

    switch (statusCode)
    {
        case StatusCodes.Status403Forbidden:
            ret = "BRANCH_UNAUTHORIZED";
            break;

        case StatusCodes.Status422UnprocessableEntity:
            ret = "BRANCH_NOT_FOUND";
            break;

        default:
            ret = "BRANCH_GENERIC_ERROR";
            break;
    }

    return ret;
}

...おそらく、ステータスコードをクラスでラップする必要があります。

public interface IAmStatusResult
{
    int StatusResult { get; }    // Pretend an int's okay for now.
    string ErrorKey { get; }
}

その後、新しいタイプのIAmStatusResultが必要になるたびに、コーディングします...

public class UnauthorizedBranchStatusResult : IAmStatusResult
{
    public int StatusResult => 403;
    public string ErrorKey => "BRANCH_UNAUTHORIZED";
}

...そして、以前のコードIAmStatusResultがスコープ内にあることを認識しentity.ErrorKey、より複雑なデッドエンドではなく、その参照を参照できるようになりました_getErrorCode(403)

さらに重要なことに、新しいタイプの戻り値を追加するたびに、それを処理するために他のコードを追加する必要はありません。Whaddyaが知っている、そしてenumそれswitchはおそらくコードのにおいでした。

利益。


どのように例えば、私は、事件について持っている多型のクラスをし、私は、コマンドラインパラメータを経由して使用しています1設定したいですか?コマンドライン->列挙->スイッチ/マップ->実装は、かなり正当なユースケースのようです。重要なことは、enumは条件付きロジックの実装としてではなく、オーケストレーションに使用されることだと思います。
アントP

@AntPこの場合、なぜStatusResult値を使用しないのですか?列挙型は、そのユースケースで人間が記憶できるショートカットとして有用であると主張することができますが、閉じたコレクションを必要としない優れた代替手段があるので、おそらくコード臭と呼ぶでしょう。
ルフィン

どのような選択肢がありますか?文字列?これは、「enumをポリモーフィズムに置き換える」という観点からのarbitrary意的な違いです。どちらのアプローチでも、値を型にマッピングする必要があります。この場合、列挙型を回避しても何も達成されません。
Ant P

@AntPここでワイヤが交差している場所がわかりません。インターフェイスのプロパティを参照する場合、そのインターフェイスの必要な場所にそのインターフェイスを配置し、そのインターフェイスの新しい実装を作成する(または既存の実装を削除する)ときにそのコードを更新することはできません。あなたが持っている場合enum、あなたはない更新コードに値を追加または削除し、あなたのコードを構造化する方法に応じて場所の潜在的に多く、すべての時間を持っています。enumの代わりにポリモーフィズムを使用することは、コードがBoyce-Codd Normal Formであることを保証するようなものです。
ルフィン

はい。ここで、N個のこのようなポリモーフィックな実装があるとします。Los Techiesの記事では、彼らは単にそれらすべてを繰り返し処理しています。しかし、たとえばコマンドラインパラメーターに基づいて1つの実装のみを条件付きで適用したい場合はどうすればよいですか?次に、構成値から注入可能な実装タイプへのマッピングを定義する必要があります。この場合の列挙型は、他の構成タイプと同様に適切です。これは、「ダーティ列挙」をポリモーフィズムに置き換える機会がこれ以上ない状況の例です。抽象化による分岐は、これまでのところあなただけを連れて行くことができます。
アントP

0

列挙型の使用がコードの匂いであるかどうかは、コンテキストに依存します。表現の問題を考えれば、質問に答えるためのいくつかのアイデアを得ることができると思います。したがって、さまざまなタイプのコレクションとそれらに対する操作のコレクションがあり、コードを整理する必要があります。2つの簡単なオプションがあります。

  • 操作に従ってコードを編成します。この場合、列挙を使用してさまざまな型にタグを付け、タグ付きデータを使用する各プロシージャにswitchステートメントを含めることができます。
  • データ型に応じてコードを整理します。この場合、列挙をインターフェイスで置き換え、列挙の各要素にクラスを使用できます。次に、各操作を各クラスのメソッドとして実装します。

どのソリューションが優れていますか?

Karl Bielefeldtが指摘したように、タイプが修正され、主にこれらのタイプに新しい操作を追加することでシステムが成長することを期待している場合、enumを使用してswitchステートメントを持つことはより良いソリューションです:新しい操作が必要になるたびにクラスを使用する場合は、各クラスにメソッドを追加する必要がありますが、新しいプロシージャを実装するだけです。

一方、かなり安定した操作を期待しているが、時間の経過とともにさらにデータ型を追加する必要があると思われる場合は、オブジェクト指向ソリューションを使用する方が便利です。新しいデータ型を実装する必要があるため、同じインターフェイスを実装する新しいクラスを追加し続けますが、enumを使用している場合は、enumを使用するすべてのプロシージャのすべてのswitchステートメントを更新する必要があります。

上記の2つのオプションのいずれかで問題を分類できない場合は、より洗練されたソリューションを見ることができます(例えば、Wikipediaページをもう一度参照し てください)短い引用については、上記。

そのため、アプリケーションがどの方向に進化するかを理解し、適切なソリューションを選択する必要があります。

あなたが参照する本はオブジェクト指向のパラダイムを扱っているので、それらが列挙型の使用に偏っているのは驚くことではありません。ただし、オブジェクト指向ソリューションが常に最適なオプションとは限りません。

結論:列挙型は必ずしもコードの匂いではありません。


この質問は、OOP言語であるC#のコンテキストで行われたことに注意してください。また、操作またはタイプによるグループ化が同じコインの2つの側面であるというのは興味深いですが、説明したように、一方が他方より「優れている」とは思いません。結果として生じるコードの量は同じであり、OOP言語はすでにタイプごとの整理を容易にします。また、大幅に直感的な(読みやすい)コードを生成します。
デイブCousineau

@DaveCousineau:「あなたが説明しているように、一方が他方よりも「優れている」とは思わない」:一方が常に他方より優れているとは言わなかった。文脈に応じて、一方が他方より優れていると言いました。例えば、操作は、多かれ少なかれ固定されている場合、あなたは新しいタイプを追加することを計画して(例えばA GUI固定してpaint()show()close() resize()オペレーションおよびカスタムウィジェット)、オブジェクト指向のアプローチは、それがあまりにも影響を与えることなく、新しいタイプを追加することを可能にするという意味で優れています多くの既存のコード(基本的にローカル変更である新しいクラスを実装します)。
ジョルジオ

あなたが説明したように、私はどちらが「より良い」とは考えていません。つまり、状況に依存することはわかりません。記述しなければならないコードの量は、両方のシナリオでまったく同じです。唯一の違いは、1つの方法がOOP(enums)に相反することと、そうでないことです。
デイブCousineau

@DaveCousineau:書かなければならないコードの量はおそらく同じです。ローカリティ(変更して再コンパイルする必要があるソースコードの場所)は大幅に変わります。たとえば、OOPでは、インターフェイスに新しいメソッドを追加する場合、そのインターフェイスを実装するクラスごとに実装を追加する必要があります。
ジョルジオ

それは単なる幅VS深さの問題ではありませんか?1つのメソッドを10個のファイルに追加するか、10個のメソッドを1つのファイルに追加します。
デイブCousineau
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.