web-dev-qa-db-ja.com

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

ジレンマ

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

したがって、私はガイドラインおよび/またはユースケースを探しています。列挙型は[〜#〜]ではありません[〜#〜]コードの匂いと実際には有効な構成。

出典:

「警告経験則として、列挙型はコードのにおいであり、ポリモーフィッククラスにリファクタリングする必要があります。[8]」Seemann、Mark、Dependency Injection in .Net、2011、p。 342

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

コンテキスト

私のジレンマの原因は取引APIです。彼らは私にこのメソッドを介して送信することによってティックデータのストリームを与えます:

void TickPrice(TickType tickType, double value)

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

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

Dictionary<TickType,double> LastValues

私には、キーとして使用される場合、これは列挙型の適切な使用のように見えました。しかし、私はこのコレクションに基づいて決定を下す場所があり、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;
    }       
  }
}
17
stromms

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

破損防止レイヤーを作成している場合は、ラッピングしているデザインのために、switchステートメントどこかを避けることはできませんが、正しく実行すると、それをその1つの場所に制限し、他の場所で多態性を使用します。

32
Karl Bielefeldt

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

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

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

17
Winston Ewert

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

IMHO、enumsを使用してデータを送信する場合、フィールドが制限された(ほとんど変更されない)値のセットからの値を持つことができることを示すのは良いことです。

任意のstringsまたはintsを送信するよりも望ましいと思います。文字列は、スペルや大文字の違いによって問題を引き起こす可能性があります。 Intは範囲外の値を送信でき、セマンティクスはほとんどありません(例:3あなたの取引サービスから、それはどういう意味ですか? LastPriceLastQuantity?他に何か?

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

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

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

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

IMHO、switchに応じて異なるコンストラクターを呼び出すsingleenum- statementは、コードの匂いではありません。タイプ名に基づくリフレクションなど、他の方法よりも必ずしも良いまたは悪いわけではありません。これは実際の状況に依存します。

すべての場所でenumのスイッチをオンにすることはコードのにおいであり、 oop は多くの場合より良い代替手段を提供します。

  • メソッドをオーバーライドした階層の異なるクラスのオブジェクトを使用します。つまり、ポリモーフィズム。
  • 階層内のクラスがほとんど変更されない場合、および(多くの)操作を階層内のクラスに疎結合にする必要がある場合は、ビジターパターンを使用します。
8

より複雑なタイプを使用した場合はどうなりますか?

    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を保持していることです。

2
Chris

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があることを確実に認識し、より複雑なデッドエンド_getErrorCode(403)の代わりにその_entity.ErrorKey_を参照できるようになりました。

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

利益。

1
ruffin

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

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

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

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

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

上記の2つのオプションのいずれかで問題を分類できない場合は、より洗練された解決策を見ることができます(たとえば、上記で引用した Wikipediaのページ を参照して、簡単な説明と参考資料を参照してください)。

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

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

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

0
Giorgio