web-dev-qa-db-ja.com

コードの切り替えを排除する方法

コードでスイッチを使用しないようにする方法は何ですか?

168
Mariano

スイッチ文はそれ自体アンチパターンではありませんが、オブジェクト指向をコーディングしている場合、スイッチの使用がswitch文を使用する代わりに polymorphism で解決されるかどうかを考慮する必要があります。

多態性では、これ:

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();
}
261
mlarsen

Switch Statements Smell を参照してください。

通常、同様の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");
        }
    }

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

// 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();
    }
}

お役に立てれば。

223
toolkit

Switchは、switchステートメントを使用して実装されている場合でも、チェーン、ルックアップテーブル、oopポリモーフィズム、パターンマッチングなどのパターンである場合でも、パターンです。

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

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

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

Switchステートメントの代替案は次のとおりです。

40
Pop Catalin

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

37
Lars Westergren

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

コードは次のようになります(擬似)。

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

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

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

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

13
Bernard

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

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

  • 値のマップ->関数
  • 多型。 (オブジェクトのサブタイプは、特定のプロセスの処理方法を決定します)。
  • ファーストクラスの機能。
6
AJ.

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

 int state; 
 
 String getString(){
 switch(state){
 case 0://状態0の動作
 return "zero"; 
 case 1://状態1の振る舞い
 return "one"; 
} 
 throw new IllegalStateException(); 
} 
 
 double getDouble(){
 
 switch(this.state){
 case 0://状態0の動作
 return 0d; 
 case 1://状態1の振る舞い
 return 1d; 
} 
 throw new IllegalStateException(); 
} 

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

Javaでは、実行時に値がわかっているプリミティブ型を非常に限られた数だけ切り替えることができます。これはそれ自体で問題を提起します。状態は魔法の数字または文字として表されています。

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

他者が「ポリモーフィズム」として提案した解決策は、 State pattern のインスタンスです。

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

 IState状態; 
 
 String getString(){
 return state.getString(); 
} 
 
 double getDouble(){
 return state.getDouble(); 
} 

新しい状態を追加するたびに、IStateインターフェイスの新しい実装を追加する必要があります。 switchの世界では、各caseswitchを追加します。

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

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

5
jamesh

if-else

しかし、スイッチは本質的に悪いという前提に反論します。

4
Iain Holder

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

第二に、スイッチはいつでもif/else ifステートメントに置き換えることができます。

3
SCdF

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

3
Nick Johnson

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

プログラミングプロセスの各ポイントでは、通常、使用できる構造とパターンの範囲があります。スイッチ、if-else-ifシーケンス、仮想関数、ジャンプテーブル、関数ポインターを使用したマップなどです。経験を積むことで、プログラマーは特定の状況で使用する適切なツールを本能的に知ることができます。

コードを維持またはレビューする人は、少なくとも元の作者と同じくらい熟練している必要があるため、どの構成体も安全に使用できます。

3
Skizz

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

オブジェクト指向言語では、オブジェクト構造、特に多態性をより有効に活用する他の代替手段がほとんど常に利用できます。

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

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

明らかなポリモーフィズムが進行していない場合、 Strategy pattern を実装する価値があります。

しかし、もしあなたの代替が大きなIF ... THEN ... ELSEブロックなら、それを忘れてください。

1
Bill Michell

Switch文が組み込まれていない言語を使用します。 Perl 5が思い浮かびます。

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

1
innaM

C++の場合

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

1
epatel

関数ポインターは、巨大なチャンキー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の冗長性を削除する方法がわからないことに注意してください。おそらくそれについて質問する必要があります。 :)

1
Cheery

さまざまな種類のオブジェクトを区別するためにスイッチがある場合、おそらく、それらのオブジェクトを正確に記述するためのいくつかのクラス、またはいくつかの仮想メソッドが欠落しています...

1
Xavier Nodet

交換したい理由によります!

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

C/C++スイッチで見落としているのは、パスカルの「入力」と範囲です。また、文字列をオンにしたいです。しかし、これらはコンパイラにとっては取るに足らないものですが、構造体やイテレータなどを使用して行うと大変な作業になります。ですから、逆に、Cのswitch()だけがもっと柔軟だったら、スイッチに置き換えたいと思うものがたくさんあります!

0
Will

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

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);
}

礼儀

0
Premraj

スイッチは、オープンクローズプリンシパルに違反するため、適切な方法ではありません。これが私のやり方です。

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();
}

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

「Liskov Substitution Principle」も読んでください。

0
Sheraz

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

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

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

0
Remo.D

多くの場合、Switchステートメントは適切なOOデザインに置き換えることができます。

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

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

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

0
jason