关于设计模式:消除代码切换的方法

Ways to eliminate switch in code

在代码中消除开关的方法是什么?


switch语句本身不是一个反模式,但是如果您是面向对象的编码,那么应该考虑使用多态性而不是switch语句是否更好地解决了switch的使用问题。

对于多态性,这是:

1
2
3
4
5
6
7
8
9
10
11
foreach (var animal in zoo) {
    switch (typeof(animal)) {
        case"dog":
            echo animal.bark();
            break;

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

变成这样:

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


参见开关语句气味:

Typically, similar switch statements are scattered throughout a program. If you add or remove a clause in one switch, you often have to find and repair the others too.

重构和模式重构都有解决这一问题的方法。

如果(伪)代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class RequestHandler {

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

这段代码违反了开放-关闭原则,对于每一种新的动作代码都是脆弱的。要解决此问题,可以引入"command"对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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();
    }
}

如果(伪)代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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");
        }
    }

然后你可以引入一个"状态"对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 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();
    }
}

希望这有帮助。


开关是一种模式,无论是用switch语句、if else链、查找表、oop多态性、模式匹配还是其他什么实现。

是否要取消使用"switch语句"或"switch模式"?第一种方法可以消除,第二种方法只能在使用另一种模式/算法的情况下消除,而且大多数情况下这是不可能的,或者不是更好的方法。

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

记住编译器可以对switch语句进行各种优化。因此,例如,如果您希望高效地进行消息处理,那么switch语句就相当不错了。但另一方面,基于switch语句运行业务规则可能不是最好的方法,应用程序应该重新构造。

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

  • 查找表
  • 多态性
  • 模式匹配(特别是在函数编程,C++模板中使用)


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


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

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

1
2
3
4
5
6
7
8
void InitMap(){
    Map[key1] = Object/Action;
    Map[key2] = Object/Action;
}

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


每个人都喜欢巨大的if else街区。很容易阅读!不过,我很好奇为什么要删除switch语句。如果需要switch语句,则可能需要switch语句。不过,说真的,这取决于代码在做什么。如果所有的开关都在调用函数(比如说),那么可以传递函数指针。这是否是一个更好的解决方案还存在争议。

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


我认为你想要的是战略模式。

这可以通过许多方法来实现,这些方法在这个问题的其他答案中已经提到,例如:

  • 值的映射->函数
  • 多态性。(对象的子类型将决定它如何处理特定进程)。
  • 一流的功能。

如果你发现自己在声明中添加了新的状态或新的行为,那么switch声明将是很好的替代:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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,添加新状态意味着在每个switch语句中添加另一个case

在爪哇中,您只能切换在运行时知道的值非常有限的原始类型。这本身就存在一个问题:状态被表示为神奇的数字或字符。

模式匹配和多个if - else块可以使用,尽管在添加新行为和新状态时确实存在相同的问题。

其他人提出的"多态性"解决方案是状态模式的一个实例:

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

1
2
3
4
5
6
7
8
9
IState state;

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

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

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

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

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


如果其他

不过,我驳斥了这样一个前提,即转换本身就很糟糕。


"switch"只是一个语言构造,所有的语言构造都可以看作是完成工作的工具。与真正的工具一样,有些工具更适合于一项任务而不是另一项任务(您不会使用大锤挂起一个图片挂钩)。重要的是如何定义"完成工作"。它是否需要维护,是否需要快速,是否需要扩展,是否需要可扩展等等。

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

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


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

其次,switch总是可以替换为if/else if语句。


你为什么想要?在一个好的编译器手中,switch语句可能比if/else块更高效(而且更容易读取),并且只有最大的开关在被任何类型的间接查找数据结构替换时才可能加快速度。


开关不是一个好的方式,因为它打破了开合原则。我就是这样做的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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");
   }
}

下面是如何使用它(使用您的代码):

1
2
3
4
foreach (var animal in zoo)
{
    echo animal.speak();
}

基本上,我们所做的是将责任委托给儿童班,而不是让家长决定如何对待儿童。

你可能还想读一读"里斯科夫替代原理"。


如果开关是用来区分各种对象的,那么您可能会缺少一些类来精确地描述这些对象,或者一些虚拟方法…


使用不附带内置switch语句的语言。我想到了Perl5。

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


在程序语言(如C)中,那么switch将比任何可选语言都好。

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

当应用程序中的多个位置出现几个非常相似的开关块,并且需要添加对新值的支持时,就会出现switch语句的问题。对于开发人员来说,忘记向散布在应用程序周围的某个开关块添加对新值的支持是很常见的。

使用多态性,然后一个新的类替换新的值,新的行为作为添加新类的一部分被添加。然后,这些切换点的行为要么继承自超类,要么重写以提供新的行为,要么在超级方法是抽象的时实现以避免编译器错误。

如果不存在明显的多态性,那么很有必要实现策略模式。

但如果你的选择是一个大的如果…然后…否则封锁,然后忘记它。


对于C++

如果您指的是抽象工厂,我认为RegisterCreatorFunc(..)方法通常比要求为所需的每个"新"语句添加一个事例要好。然后让所有类创建并注册一个creatorfunction(..),它可以很容易地用宏实现(如果我敢提的话)。我相信这是许多框架都采用的一种通用方法。我第一次在ET++中看到它,我认为许多需要decl和impl宏的框架都使用它。


函数指针是替换一个巨大的块switch语句的一种方法,它们在语言中特别好,在语言中,您可以通过函数的名称捕获函数并用它们生成内容。

当然,您不应该强制将switch语句从代码中删除,而且您总是有可能做的都是错误的,这会导致愚蠢的冗余代码块。(有时这是不可避免的,但是一种好的语言应该允许您在保持整洁的同时消除冗余。)

这是一个很好的分而治之的例子:

假设你有某种类型的翻译。

1
2
3
4
5
6
7
8
9
10
11
12
13
switch(*IP) {
    case OPCODE_ADD:
        ...
        break;
    case OPCODE_NOT_ZERO:
        ...
        break;
    case OPCODE_JUMP:
        ...
        break;
    default:
        fixme(*IP);
}

相反,您可以使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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中操作码表的冗余。也许我应该对此提出一个问题。:)


这取决于你为什么要更换它!

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

我对C/C++交换机怀念的是Pascal的"in"和范围。我也希望我能打开弦。但是,这些对于编译器来说是微不足道的,但在使用结构、迭代器和其他东西时却很难做到。所以,恰恰相反,如果C的switch()更灵活,我希望我可以用一个switch替换很多东西!


switch语句通常可以替换为良好的OO设计。

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

我建议用一些表示不同类型帐户的帐户类替换它,所有这些类都实现一个帐户接口。

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


最明显的、独立于语言的答案是使用一系列"if"。

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

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


在使用关联数组的javascript中:这是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;
    }
}

变成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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);
}

礼貌


对if/else的另一个投票。我不太喜欢case或switch语句,因为有些人不使用它们。如果使用case或switch,代码的可读性会降低。也许对你来说不那么可读,但是对于那些从不需要使用命令的人来说。

对象工厂也是如此。

if/else块是每个人都能得到的简单构造。你可以做一些事情来确保你不会引起问题。

首先,不要尝试多次缩进if语句。如果你发现自己缩进了,那你就错了。

1
2
3
4
5
6
7
8
9
10
 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

真的很糟糕-改为这样做。

1
2
3
4
5
6
7
8
9
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块中分离出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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语句的示例…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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语句。

例如

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

对于我(如果我错了,你可以纠正我),案例陈述把这个规则抛到了窗外。有条件执行的代码块不放置在开始/结束结构中。正因为如此,我相信案例在概念上是完全不同的,无法使用。

希望能回答你的问题。