消除代码切换的方法

时间:2008-09-24 10:34:14

标签: design-patterns

有什么方法可以消除代码中交换机的使用?

23 个答案:

答案 0 :(得分:254)

Switch语句本身不是反模式,但是如果你是面向对象的编码,你应该考虑使用polymorphism而不是使用switch语句来更好地解决开关的使用。

使用多态,这个:

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

答案 1 :(得分:217)

请参阅Switch Statements Smell

  

通常,类似的switch语句分散在整个程序中。如果在一个开关中添加或删除子句,通常也必须查找并修复其他子句。

RefactoringRefactoring to Patterns都有解决此问题的方法。

如果您的(伪)代码如下:

class RequestHandler {

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

此代码违反了Open Closed Principle,并且对于出现的每种新类型的操作代码都很脆弱。 要解决这个问题,你可以引入一个'Command'对象:

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

希望这有帮助。

答案 2 :(得分:39)

switch是一种模式,无论是用switch语句实现的,if else链,查找表,oop多态,模式匹配还是别的。

是否要取消使用“切换声明”或“切换模式”?第一个可以被删除,第二个可以被删除,只有在可以使用另一个模式/算法时,并且大部分时间是不可能的,或者这不是更好的方法。

如果你想从代码中消除 switch语句,首先要问的问题是消除switch语句并使用其他技术是有意义的。不幸的是,这个问题的答案是特定领域的。

请记住,编译器可以进行各种优化来切换语句。因此,例如,如果您想要有效地进行消息处理,那么切换语句几乎是可行的方法。但另一方面,基于switch语句运行业务规则可能不是最好的方法,应该重新构建应用程序。

以下是switch语句的一些替代方法:

答案 3 :(得分:37)

切换本身并不是那么糟糕,但是如果你的方法中的对象上有很多“switch”或“if / else”,那么这可能表明你的设计有点“程序化”并且你的对象只是价值桶。将逻辑移动到对象,调用对象上的方法,让他们决定如何响应。

答案 4 :(得分:21)

我认为最好的方法是使用好的地图。使用字典,您几乎可以将任何输入映射到其他值/对象/函数。

你的代码看起来像这样(psuedo):

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

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

答案 5 :(得分:13)

每个人都喜欢巨大的if else块。这么容易阅读!我很好奇你为什么要删除switch语句。如果需要switch语句,则可能需要switch语句。说真的,我会说这取决于代码的作用。如果所有开关都在调用函数(比如说),你可以传递函数指针。无论是更好的解决方案都值得商榷。

我认为语言也是一个重要因素。

答案 6 :(得分:6)

我认为您正在寻找的是战略模式。

这可以通过多种方式实施,在这个问题的其他答案中已经提到过,例如:

  • 价值图 - &gt;功能
  • 多态性。 (对象的子类型将决定它如何处理特定进程)。
  • 一流的功能。

答案 7 :(得分:5)

如果您发现自己在语句中添加新状态或新行为,那么

switch语句可以替换:

int state;

String getString() {
   switch (state) {
     case 0 : // behaviour for state 0
           return "zero";
     case 1 : // behaviour for state 1
           return "one";
   }
   throw new IllegalStateException();
}

double getDouble() {

   switch (this.state) {
     case 0 : // behaviour for state 0
           return 0d;
     case 1 : // behaviour for state 1
           return 1d;
   }
   throw new IllegalStateException();
}

添加新行为需要复制switch,添加新状态意味着在每个 case语句中添加另一个switch

在Java中,您只能切换在运行时知道其值的非常有限数量的基本类型。这本身就存在一个问题:状态被表示为幻数或字符。

Pattern matching,可以使用多个if - else块,但在添加新行为和新状态时确实存在相同的问题。

其他人建议为“多态”的解决方案是State pattern的一个实例:

用自己的类替换每个状态。每个行为在类上都有自己的方法:

IState state;

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

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

每次添加新状态时,都必须添加IState接口的新实现。在switch世界中,您要为每个case添加switch

每次添加新行为时,都需要向IState接口和每个实现添加新方法。这与以前的负担相同,但现在编译器将检查您是否在每个预先存在的状态上实现了新行为。

其他人已经说过,这可能太重了,所以当然有一点可以达到你从一个移动到另一个的地方。就个人而言,我第二次写一个开关就是我重构的地方。

答案 8 :(得分:4)

的if-else

我反驳说切换本质上很糟糕的前提。

答案 9 :(得分:3)

你为什么这么想?在一个好的编译器的手中,switch语句可以比if / else块更有效(并且更容易阅读),并且如果它们被任何排序替换,只有最大的开关可能加速间接查找数据结构。

答案 10 :(得分:3)

'switch'只是一种语言结构,所有语言结构都可以被视为完成工作的工具。与真实工具一样,某些工具更适合于一项任务而不是另一项任务(您不会使用大锤来放置图片钩子)。重要的是如何定义“完成工作”。它是否需要可维护,是否需要快速,是否需要扩展,是否需要可扩展等等。

在编程过程的每个点上,通常都有一系列可以使用的构造和模式:一个开关,一个if-else-if序列,虚函数,跳转表,带有函数指针的映射等等。根据经验,程序员会本能地知道在特定情况下使用的正确工具。

必须假设维护或审查代码的人至少与原作者一样熟练,以便可以安全地使用任何构造。

答案 11 :(得分:3)

嗯,首先,我不知道使用开关是一种反模式。

其次,切换总是可以用if / else if语句替换。

答案 12 :(得分:1)

如果开关是为了区分各种对象,你可能会遗漏一些类来精确描述这些对象,或者某些虚拟方法......

答案 13 :(得分:1)

函数指针是替换巨大的chunky switch语句的一种方法,它们特别适用于可以通过名称捕获函数并使用它们创建内容的语言。

当然,你不应该强迫你的代码中的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的冗余。也许我应该对它提出一个问题。 :)

答案 14 :(得分:1)

使用未附带内置开关语句的语言。想到Perl 5.

说真的,你为什么要避免它呢?如果你有充分的理由避免它,为什么不简单地避免呢?

答案 15 :(得分:1)

对于C ++

如果你指的是一个AbstractFactory,我认为 registerCreatorFunc(..)方法通常比要求为每个所需的“新”语句添加一个案例更好。然后让所有类创建并注册 creatorFunction(..),这可以通过宏轻松实现(如果我敢提的话)。我相信这是许多框架的常用方法。我第一次在ET ++中看到它,我认为许多需要DECL和IMPL宏的框架都使用它。

答案 16 :(得分:1)

在程序语言中,如C,那么切换将比任何替代方案更好。

在面向对象语言中,几乎总有其他替代方案可以更好地利用对象结构,特别是多态性。

当应用程序中的多个位置出现几个非常相似的切换块时,会出现switch语句的问题,并且需要添加对新值的支持。对于开发人员来说,忘记将新值的支持添加到分散在应用程序周围的其中一个交换机块中是很常见的。

使用多态,然后新类替换新值,并在添加新类时添加新行为。然后,这些切换点处的行为要么从超类继承,要么被覆盖以提供新行为,要么实现为在super方法是抽象时避免编译器错误。

如果没有明显的多态性,那么值得实施Strategy pattern

但是,如果你的替代品是一个很大的IF ...那么... ELSE阻止,那就算了。

答案 17 :(得分: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);
}

Courtesy

答案 18 :(得分:0)

切换不是一个好方法,因为它打破了Open Close Principal。我就是这样做的。

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替代原则”。

答案 19 :(得分:0)

取决于你想要替换它的原因!

许多解释器使用'computed gotos'而不是switch语句来执行操作码。

我想念的C / C ++开关是Pascal'in'和范围。我也希望我可以打开字符串。但是,当使用结构和迭代器和事物完成时,这些对于编译器来说是微不足道的。所以,相反,如果只有C的开关()更灵活,我希望可以用开关取代很多东西!

答案 20 :(得分:0)

切换语句通常可以用良好的OO设计代替。

例如,您有一个Account类,并且正在使用switch语句根据帐户类型执行不同的计算。

我建议将其替换为多个帐户类,代表不同类型的帐户,以及所有帐户界面。

然后切换变得不必要,因为您可以对所有类型的帐户进行相同处理,并且由于多态性,将为帐户类型运行相应的计算。

答案 21 :(得分:0)

最明显的,语言独立的答案是使用一系列'if'。

如果您使用的语言具有函数指针(C)或具有第一类值(Lua)的函数,则可以使用(指向)函数的数组(或列表)获得类似于“switch”的结果

如果你想要更好的答案,你应该对语言更具体。

答案 22 :(得分:-10)

if / else的另一次投票。我不是case或switch语句的忠实粉丝,因为有些人不使用它们。如果使用大小写或交换机,代码可读性较差。也许你的可读性不是那么低,而是那些从未需要使用命令的人。

对象工厂也是如此。

if / else blocks是一个简单的构造,每个人都可以获得。您可以采取一些措施来确保不会导致问题。

首先 - 不要尝试多次缩进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 

优化被诅咒。它对代码的速度没有太大的影响。

其次,只要在特定代码块中分散了足够的中断语句以使其显而易见,我就不反对违反If Block

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上以及为什么我认为很难理解:

这是一个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语言的常规控制结构已被彻底打破。一般规则是,如果要在控制结构中放置多行代码,可以使用大括号或开始/结束语句。

e.g。

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

对我来说(如果我错了你可以纠正我),Case语句将这条规则抛出窗口。有条件执行的代码块不会放在开始/结束结构中。因此,我认为Case在概念上有所不同,不能使用。

希望能回答你的问题。