コードの切り替えを排除する方法[終了]


175

コードでスイッチの使用を排除する方法は何ですか?


18
適切に使用する場合、なぜスイッチを削除したいのですか?質問について詳しく教えてください。
RB。

みんなが同じことを言っているので、この質問に投票してコミュニティを編集可能にします。すべての意見をまとめるといいかもしれません;)
Josh

2
スイッチは他の指示ほど標準的ではありません。たとえば、C ++では「ブレーク」を忘れてしまい、予期しない結果が生じる可能性があります。その上、この「ブレイク」はGOTOに似すぎています。私はスイッチを排除しようとしたことがありませんが、確かにスイッチは私の好きな指示ではありません。;)

2
彼はC ++またはJavaのswitchステートメントではなく、switchの概念を参照していると思います。スイッチは、「if-else if」ブロックのチェーンにすることもできます。
アウトロープログラマ

2
面白いかもしれません:反IFキャンペーンに参加しているアジャイルコミュニティの著名なプログラマーが多数います:antiifcampaign.com/supporters.html
Mike A

回答:


267

スイッチステートメント自体はアンチパターンではありませんが、オブジェクト指向をコーディングしている場合は、スイッチステートメントを使用する代わりに、スイッチの使用がポリモーフィズムで解決されるかどうかを検討する必要があります。

ポリモーフィズムでは、これは:

foreach (var animal in zoo) {
    switch (typeof(animal)) {
        case "dog":
            echo animal.bark();
            break;

        case "cat":
            echo animal.meow();
            break;
    }
}

これになる:

foreach (var animal in zoo) {
    echo animal.speak();
}

2
私は、stackoverflow.com / questions / 374239 /… で同様の提案があったために打ちのめされています。非常に多くのpplは、ポリモーフィズムを信じていません。
ナズゴブ2008

30
-1:でswitchステートメントを使用したことtypeofがないので、この回答は、他の状況でswitchステートメントを回避する方法や理由を示唆していません。
ケビン

3
私は@Kevinに同意します-与えられた例は、ポリモーフィズムによってスイッチを排除する方法を実際には示していません。簡単な例:値を切り替えることによって列挙型の名前を取得するか、何らかのアルゴリズムで適切な値によってコードを実行します。
HotJard 2013年

特定の実装に依存する前に、つまり、工場でそれを排除する方法を考えていました。そして、私はここで素晴らしい例を発見したstackoverflow.com/a/3434505/711855
juanmf

1
非常に些細な例。
GuardianX

239

Switchステートメントのにおいをご覧ください。

通常、同様のswitchステートメントがプログラム全体に散在しています。1つのスイッチで句を追加または削除すると、多くの場合、他の句も見つけて修復する必要があります。

パターンへのリファクタリングリファクタリングの両方これを解決するためのアプローチがあります。

(疑似)コードが次のようになっている場合:

class RequestHandler {

    public void handleRequest(int action) {
        switch(action) {
            case LOGIN:
                doLogin();
                break;
            case LOGOUT:
                doLogout();
                break;
            case QUERY:
               doQuery();
               break;
        }
    }
}

このコードはOpen Closed Principleに違反しており、新しいタイプのアクションコードすべてに脆弱です。これを修正するには、「コマンド」オブジェクトを導入します。

interface Command {
    public void execute();
}

class LoginCommand implements Command {
    public void execute() {
        // do what doLogin() used to do
    }
}

class RequestHandler {
    private Map<Integer, Command> commandMap; // injected in, or obtained from a factory
    public void handleRequest(int action) {
        Command command = commandMap.get(action);
        command.execute();
    }
}

(疑似)コードが次のようになっている場合:

class House {
    private int state;

    public void enter() {
        switch (state) {
            case INSIDE:
                throw new Exception("Cannot enter. Already inside");
            case OUTSIDE:
                 state = INSIDE;
                 ...
                 break;
         }
    }
    public void exit() {
        switch (state) {
            case INSIDE:
                state = OUTSIDE;
                ...
                break;
            case OUTSIDE:
                throw new Exception("Cannot leave. Already outside");
        }
    }

次に、「状態」オブジェクトを導入できます。

// Throw exceptions unless the behavior is overriden by subclasses
abstract class HouseState {
    public HouseState enter() {
        throw new Exception("Cannot enter");
    }
    public HouseState leave() {
        throw new Exception("Cannot leave");
    }
}

class Inside extends HouseState {
    public HouseState leave() {
        return new Outside();
    }
}

class Outside extends HouseState {
    public HouseState enter() {
        return new Inside();
    }
}

class House {
    private HouseState state;
    public void enter() {
        this.state = this.state.enter();
    }
    public void leave() {
        this.state = this.state.leave();
    }
}

お役に立てれば。


7
コードをリファクタリングする方法の素晴らしい例をありがとう。私は最初に言うかもしれませんが、それを完全に理解するためにいくつかのファイルを切り替える必要があるため、それを読むのは少し難しいです
rshimoda '29

8
スイッチに対する引数は、ポリモーフィックソリューションがコードの単純さを犠牲にすることを理解している限り有効です。さらに、スイッチのケースを常に列挙型に格納すると、一部のコンパイラは状態がスイッチにないことを警告します。
Harvey、

1
これは、完全/不完全な操作と、もちろんコードをOOPに再構築することについての良い例です。トンありがとう。OOP /デザインパターンの支持者がOOPの概念を概念ではなく演算子のように扱うことを提案するなら、それは有用だと思います。「拡張」、「工場」、「実装」などが、ファイル、クラス、ブランチ全体で頻繁に使用されることを意味します。これらは、「+」、「-」、「+ =」、「?:」、「==」、「->」などの演算子のように単純である必要があります。プログラマが頭の中でそれらを演算子としてだけ使用する場合、その後、プログラムの状態と(不完全な)操作についてクラスライブラリ全体を検討できます。
名前空間

13
SWITCHはこれに比べてはるかに理解しやすく論理的だと思い始めています。そして、私は通常OOPがとても好きですが、この解決策は私にはあまりにも抽象的なようです
JK

1
Commandオブジェクトを使用すると、生成するコードにMap<Integer, Command>スイッチは必要ありませんか?
ataulm

41

switchは、switchステートメントで実装されているかどうかに関係なく、チェーン、ルックアップテーブル、oopポリモーフィズム、パターンマッチングなどのパターンです。

switchステートメント」または「switchパターン」の使用を削除しますか?1つ目は削除でき、2つ目は別のパターン/アルゴリズムを使用できる場合にのみ削除できます。ほとんどの場合、それは不可能であるか、そうするのが適切な方法ではありません。

コードからswitchステートメントを削除したい場合、最初に尋ねる質問は、switchステートメントを削除して他の手法を使用することが理にかなっている場所です。残念ながら、この質問への答えはドメイン固有です。

また、コンパイラはステートメントを切り替えるためにさまざまな最適化を実行できることを覚えておいてください。したがって、たとえばメッセージ処理を効率的に実行したい場合は、switchステートメントがほとんどの方法です。ただし、その一方で、switchステートメントに基づいてビジネスルールを実行することはおそらく最善の方法ではなく、アプリケーションを再設計する必要があります。

switchステートメントのいくつかの代替方法を次に示します。


1
誰かがスイッチと他の代替を使用したメッセージ処理の比較を与えることができますか?
マイクA

37

スイッチ自体はそれほど悪いことではありませんが、メソッド内のオブジェクトに多くの「スイッチ」または「if / else」がある場合、デザインが少し「手続き型」であり、オブジェクトが単なる値であることを示している可能性がありますバケツ。ロジックをオブジェクトに移動し、オブジェクトのメソッドを呼び出し、代わりに応答方法を決定させます。


もちろん、彼がCで書いていないと仮定します。:)
Bernard

1
C言語では、彼は(ab?)関数ポインタと構造体を使用して、オブジェクトのような何かを構築できます;)
Tetha

FORT ^ H ^ H ^ H ^ H Javaは任意の言語で記述できます。; p
バーナード

完全に同意します-スイッチはコード行を削減するための素晴らしい方法ですが、それをいじってはいけません。
HotJard 2013年

21

最良の方法は良い地図を使うことだと思います。辞書を使用すると、ほとんどすべての入力を他の値/オブジェクト/関数にマッピングできます。

あなたのコードは次のようになります(疑似):

void InitMap(){
    Map[key1] = Object/Action;
    Map[key2] = Object/Action;
}

Object/Action DoStuff(Object key){
    return Map[key];
}

4
言語によって異なります。スイッチよりもはるかに読みにくくなる可能性があります
Vinko Vrsalovic '24

これは、適切な状況での優れたソリューションです。最近、このマッピングキーコードを実行しましたが、その目的で問題なく読み取れたようです。
バーナード

これは本当です、私はおそらくこれを単純なものには使用しませんが、switchステートメントの構成に関してある程度の柔軟性を提供します。スイッチは常にハードコーディングされていますが、その場で辞書を作成できます。
ジョシュ

状況によってはよりクリーンになることがあります。関数呼び出しを必要とするため、速度も遅くなります。
ニックジョンソン

1
それはあなたの鍵に依存します。コンパイラーは、switchステートメントを単純な検索または非常に高速な静的バイナリー検索にコンパイルできます。どちらの場合も、関数呼び出しは必要ありません。
ニックジョンソン

12

誰もが巨大なif elseブロックを愛しています。とても読みやすい!しかし、なぜswitchステートメントを削除したいのか、私は興味があります。switchステートメントが必要な場合は、おそらくswitchステートメントが必要です。しかし真剣に、それはコードが何をしているかに依存すると思います。すべてのスイッチが関数を呼び出すことである場合(たとえば)、関数ポインターを渡すことができます。それがより良い解決策であるかどうかは議論の余地があります。

ここでも言語は重要な要素だと思います。


13
それが皮肉だと仮定してイム:)
クレイグ・デイ

6

あなたが探しているのは戦略パターンだと思います。

これは、次のようなこの質問の他の回答で言及されているいくつかの方法で実装できます。

  • 値のマップ->関数
  • ポリモーフィズム。(オブジェクトのサブタイプによって、特定のプロセスの処理方法が決まります)。
  • ファーストクラスの機能。

5

switch ステートメントに新しい状態または新しい動作を追加する場合は、ステートメントを置き換えることをお勧めします。

int状態。

String getString(){
   スイッチ(状態){
     ケース0://状態0の動作
           「ゼロ」を返します。
     ケース1://状態1の動作
           「1」を返す;
   }
   新しいIllegalStateException();をスローします。
}

double getDouble(){

   スイッチ(this.state){
     ケース0://状態0の動作
           0dを返します。
     ケース1://状態1の動作
           1dを返す;
   }
   新しいIllegalStateException();をスローします。
}

新しい動作を追加するにはswitch、をコピーする必要があります。新しい状態を追加することはcaseすべての switchステートメントに別の状態を追加することを意味します。

Javaでは、実行時に値がわかっている非常に限られた数のプリミティブ型のみを切り替えることができます。これはそれ自体に問題があります。状態はマジックナンバーまたは文字として表現されています。

パターンマッチング、および複数のif - elseブロックを使用できますが、新しい動作や新しい状態を追加するときに実際に同じ問題が発生します。

他の人が「ポリモーフィズム」として提案したソリューションは、Stateパターンのインスタンスです。

各状態を独自のクラスに置き換えます。各動作には、クラスに独自のメソッドがあります。

IStateの状態。

String getString(){
   return state.getString();
}

double getDouble(){
   return state.getDouble();
}

新しい状態を追加するたびに、IStateインターフェースの新しい実装を追加する必要があります。ではswitch世界、あなたは追加することと思いますcaseそれぞれにしますswitch

新しい動作を追加するたびに、新しいメソッドをIStateインターフェースと各実装に追加する必要があります。これは以前と同じ負担ですが、コンパイラは、既存の各状態で新しい動作の実装があることを確認します。

他の人はすでにこれは重すぎるかもしれないと言っているので、もちろん、あなたが別の場所に移動するところに到達するポイントがあります。個人的には、2回目にスイッチを書くときがリファクタリングのポイントです。



3

1つには、スイッチの使用がアンチパターンであることを知りませんでした。

次に、switchは常にif / else ifステートメントで置き換えることができます。


正確に言えば、switchはif / elsifの束の単なる構文メタドンです。
マイクA

3

なぜあなたはしたいですか?優れたコンパイラーの手元では、switchステートメントはif / elseブロックよりもはるかに効率的で(読みやすく)、また、並べ替えによって置き換えられた場合、最大のスイッチのみが高速化される可能性があります。間接参照データ構造。


2
その時点で、コンパイラーを2番目に推測し、コンパイラーの内部に基づいて設計に変更を加えます。設計は、コンパイラーの性質ではなく、問題の性質に従う必要があります。
マイクA

3

「スイッチ」は単なる言語構成要素であり、すべての言語構成要素は仕事を成し遂げるためのツールと考えることができます。実際のツールと同様に、一部のツールは別のタスクよりも1つのタスクにより適しています(スレッジハンマーを使用して画像フックを付けることはありません)。重要な部分は、「仕事を終わらせる」ことをどのように定義するかです。それは、保守可能である必要があるか、高速である必要があるか、拡張する必要があるか、拡張可能である必要があるかなどです。

プログラミングプロセスの各ポイントには、通常、使用できるさまざまな構造とパターンがあります。スイッチ、if-else-ifシーケンス、仮想関数、ジャンプテーブル、関数ポインターを含むマップなどです。経験があれば、プログラマーは、与えられた状況で使用する適切なツールを本能的に知っています。

コードを保守またはレビューする人は、構成を安全に使用できるように、少なくとも元の作者と同じくらいのスキルがあると想定する必要があります。


OK、でもなぜ同じことをするために5つの異なる冗長な方法が必要なのですか-条件付き実行?
マイクA

@ mike.amy:それぞれの方法にはさまざまなメリットとコストがあり、コストを最小限に抑えて最大のメリットを得ることがすべてです。
Skizz、2009年

1

さまざまな種類のオブジェクトを区別するためのスイッチがある場合は、それらのオブジェクトを正確に説明するためのクラスや、仮想メソッドが不足している可能性があります...


1

C ++の場合

つまり、AbstractFactoryを参照している場合は、通常、registerCreatorFunc(..)メソッドの方が、必要なすべての「新しい」ステートメントごとにケースを追加する必要があるよりも優れていると思います。次に、すべてのクラスにcreateFunction(..)を作成および登録させます。これは、マクロを使用して簡単に実装できます(言及する場合)。これは多くのフレームワークが行う一般的なアプローチだと思います。私は最初にそれをET ++で見ましたが、DECLおよびIMPLマクロを必要とする多くのフレームワークがそれを使用していると思います。


1

Cのような手続き型言語では、switchは他のどの方法よりも優れています。

オブジェクト指向言語では、ほとんどの場合、オブジェクト構造、特にポリモーフィズムをより有効に利用できる他の代替手段があります。

switchステートメントの問題は、いくつかの非常に類似したswitchブロックがアプリケーションの複数の場所で発生し、新しい値のサポートを追加する必要がある場合に発生します。開発者がアプリケーションに散在するスイッチブロックの1つに新しい値のサポートを追加するのを忘れることはよくあることです。

ポリモーフィズムを使用すると、新しいクラスが新しい値を置き換え、新しいクラスの追加の一部として新しい動作が追加されます。これらのスイッチポイントでの動作は、スーパークラスから継承されるか、オーバーライドされて新しい動作を提供するか、スーパーメソッドが抽象の場合にコンパイラエラーを回避するために実装されます。

明確なポリモーフィズムが進行していない場合、戦略パターンを実装する価値があります。

しかし、代替案が大きなIF ... THEN ... ELSEブロックの場合は、忘れてください。


1

組み込みのswitchステートメントが付属していない言語を使用します。Perl 5が思い浮かびます。

真剣に、なぜあなたはそれを避けたいのですか?そして、あなたがそれを回避する正当な理由があるなら、なぜそれを単に回避しないのですか?


1

関数ポインタは、巨大な分厚いswitchステートメントを置き換える1つの方法です。それらは、名前で関数をキャプチャし、関数を作成できる言語で特に優れています。

もちろん、switchステートメントを強制的にコードから削除するべきではありません。また、すべてを間違って実行している可能性が常にあります。(これは避けられない場合もありますが、適切な言語を使用すると、クリーンな状態で冗長性を削除できます。)

これは素晴らしい分割統治の例です:

なんらかの通訳がいるとします。

switch(*IP) {
    case OPCODE_ADD:
        ...
        break;
    case OPCODE_NOT_ZERO:
        ...
        break;
    case OPCODE_JUMP:
        ...
        break;
    default:
        fixme(*IP);
}

代わりに、これを使用できます。

opcode_table[*IP](*IP, vm);

... // in somewhere else:
void opcode_add(byte_opcode op, Vm* vm) { ... };
void opcode_not_zero(byte_opcode op, Vm* vm) { ... };
void opcode_jump(byte_opcode op, Vm* vm) { ... };
void opcode_default(byte_opcode op, Vm* vm) { /* fixme */ };

OpcodeFuncPtr opcode_table[256] = {
    ...
    opcode_add,
    opcode_not_zero,
    opcode_jump,
    opcode_default,
    opcode_default,
    ... // etc.
};

Cでopcode_tableの冗長性を削除する方法がわからないことに注意してください。おそらくそれについて質問する必要があります。:)


0

言語に依存しない最も明白な答えは、一連の「if」を使用することです。

使用している言語に関数ポインター(C)または第1クラス値(Lua)の関数がある場合、(ポインターへの)関数の配列(またはリスト)を使用して「スイッチ」と同様の結果を得ることができます。

より適切な回答が必要な場合は、言語についてより具体的にする必要があります。


0

多くの場合、switchステートメントは適切なOO設計で置き換えることができます。

たとえば、Accountクラスがあり、switchステートメントを使用して、アカウントのタイプに基づいて異なる計算を実行しています。

これは、さまざまなタイプのアカウントを表す多数のアカウントクラスに置き換え、すべてがアカウントインターフェースを実装することをお勧めします。

その後、切り替えは不要になります。すべてのタイプのアカウントを同じように扱うことができ、ポリモーフィズムのおかげで、アカウントタイプに対して適切な計算が実行されます。


0

あなたがそれを交換したい理由に依存します!

多くのインタープリターは、オペコードの実行にswitchステートメントの代わりに「計算されたgotos」を使用します。

C / C ++スイッチについて見逃しているのは、Pascalの「in」と範囲です。また、弦をオンにしたいです。しかし、これらは、コンパイラが食べるのは簡単ですが、構造体やイテレータなどを使用して行うと大変な作業です。したがって、逆に、Cのswitch()だけがより柔軟であったとしても、スイッチに置き換えられることがたくさんあります。


0

スイッチはOpen Closeプリンシパルを壊すので、良い方法ではありません。これが私のやり方です。

public class Animal
{
       public abstract void Speak();
}


public class Dog : Animal
{
   public virtual void Speak()
   {
       Console.WriteLine("Hao Hao");
   }
}

public class Cat : Animal
{
   public virtual void Speak()
   {
       Console.WriteLine("Meauuuu");
   }
}

そして、ここにそれを使用する方法があります(あなたのコードを取ります):

foreach (var animal in zoo) 
{
    echo animal.speak();
}

基本的に、私たちがしていることは、親に子供をどうするかを決めるのではなく、子クラスに責任を委任することです。

また、「リスコフの代替原則」について読むこともできます。


0

連想配列を使用するJavaScriptの場合:
これ:

function getItemPricing(customer, item) {
    switch (customer.type) {
        // VIPs are awesome. Give them 50% off.
        case 'VIP':
            return item.price * item.quantity * 0.50;

            // Preferred customers are no VIPs, but they still get 25% off.
        case 'Preferred':
            return item.price * item.quantity * 0.75;

            // No discount for other customers.
        case 'Regular':
        case
        default:
            return item.price * item.quantity;
    }
}

これになる:

function getItemPricing(customer, item) {
var pricing = {
    'VIP': function(item) {
        return item.price * item.quantity * 0.50;
    },
    'Preferred': function(item) {
        if (item.price <= 100.0)
            return item.price * item.quantity * 0.75;

        // Else
        return item.price * item.quantity;
    },
    'Regular': function(item) {
        return item.price * item.quantity;
    }
};

    if (pricing[customer.type])
        return pricing[customer.type](item);
    else
        return pricing.Regular(item);
}

礼儀


-12

if / elseへの別の投票。それらを使用しない人もいるので、私はcase文やswitch文の大ファンではありません。ケースやスイッチを使用すると、コードが読みにくくなります。読みやすさは劣らないかもしれませんが、コマンドを使用する必要がなかった人には読みやすいでしょう。

オブジェクトファクトリについても同様です。

if / elseブロックは、誰もが簡単に取得できる構造です。問題を引き起こさないようにするためにできることがいくつかあります。

まず、ifステートメントを数回以上インデントしないでください。自分がインデントしていることに気づいたら、それは間違っています。

 if a = 1 then 
     do something else 
     if a = 2 then 
         do something else
     else 
         if a = 3 then 
             do the last thing
         endif
     endif 
  endif

本当に悪いです-代わりにこれをしてください。

if a = 1 then 
   do something
endif 
if a = 2 then 
   do something else
endif 
if a = 3 then 
   do something more
endif 

最適化はおろそかにされる。コードの速度はそれほど変わりません。

次に、明確にするために特定のコードブロックに十分なbreakステートメントが散在している限り、Ifブロックから抜け出すことを嫌いません。

procedure processA(a:int)
    if a = 1 then 
       do something
       procedure_return
    endif 
    if a = 2 then 
       do something else
       procedure_return
    endif 
    if a = 3 then 
       do something more
       procedure_return
    endif 
end_procedure

編集:スイッチで、なぜ私はそれを理解するのが難しいと思います:

これがswitchステートメントの例です...

private void doLog(LogLevel logLevel, String msg) {
   String prefix;
   switch (logLevel) {
     case INFO:
       prefix = "INFO";
       break;
     case WARN:
       prefix = "WARN";
       break;
     case ERROR:
       prefix = "ERROR";
       break;
     default:
       throw new RuntimeException("Oops, forgot to add stuff on new enum constant");
   }
   System.out.println(String.format("%s: %s", prefix, msg));
 }

私にとってここでの問題は、Cのような言語で適用される通常の制御構造が完全に壊れていることです。制御構造内に複数行のコードを配置する場合は、中かっこまたはbegin / endステートメントを使用するという一般的な規則があります。

例えば

for i from 1 to 1000 {statement1; statement2}
if something=false then {statement1; statement2}
while isOKtoLoop {statement1; statement2}

私にとっては(そして、私が間違っていれば、あなたは私を訂正することができます)、Caseステートメントはこのルールをウィンドウの外にスローします。条件付きで実行されるコードブロックは、開始/終了構造内に配置されません。このため、ケースは概念的には十分に使用されないほど異なると思います。

それがあなたの質問に答えることを願っています。


うわー-明らかに争いの答え。私は何がそんなに間違っているのか知りたいです。
seanyboy 2008

えっと、複雑すぎるので切り替えますか?わかりません...使用できる言語機能が多く残っているとは思えません。:)また、あなたの中心的な例では、(a == 1 || a == 2 || a == 3)が何かをするなら賢くはないでしょうか?
Lars Westergren、2008

また、最後の例では、「break」はほとんどの言語で何もしません。最も近いブロック(通常はループ)から抜け出します。これは、次の行(endif)で発生します。ブレークが「リターン」である言語を使用する場合、多くのリターンも
不快になり

4
「最適化は酷いことです。コードの速度にそれほど大きな違いはありません。」私のコードはモバイルプラットフォームで実行されます。最適化は重要です。さらに、スイッチを正しく使用すると、コードを(if..elseif..elseif..elseif ...と比較して)非常にクリーンに見せることができます。それらを見たことがない?それらを学ぶ。
Swati

スイッチはまったく複雑ではありませんが、理解の摩擦を減らすために、コードで使用される構成要素の数を最小限に抑えるように常に努めてきました。
seanyboy 2008
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.