多个If-else或枚举 - 哪一个更好,为什么?

时间:2011-05-25 14:40:54

标签: java enums if-statement code-design

以下是原始代码:

public class FruitGrower {
    public void growAFruit(String type) {
        if ("wtrmln".equals(type)) {
            //do watermelon growing stuff
        } else if ("ppl".equals(type)) {
            //do apple growing stuff
        } else if ("pnppl".equals(type)) {
            //do pineapple growing stuff
        } else if ("rng".equals(type)) {
            //do orange growing stuff
        } else {
            // do other fruit growing stuff
        }
    }
}

这就是我改变它的方式:

public class FruitGrower {
    enum Fruits {
        WATERMELON {
            @Override
            void growAFruit() {
                //do watermelon growing stuff
            }
        },

        APPLE {
            @Override
            void growAFruit() {
                //do apple growing stuff
            }
        },

        PINEAPPLE {
            @Override
            void growAFruit() {
                //do pineapple growing stuff
            }
        },

        ORANGE {
            @Override
            void growAFruit() {
                //do orange growing stuff
            }
        },

        OTHER {
            @Override
            void growAFruit() {
                // do other fruit growing stuff
            }
        };
        static void grow(String type) {
            if ("wtrmln".equals(type)) {
                WATERMELON.growAFruit();
            } else if ("ppl".equals(type)) {
                APPLE.growAFruit();
            } else if ("pnppl".equals(type)) {
                PINEAPPLE.growAFruit();
            } else if ("rng".equals(type)) {
                ORANGE.growAFruit();
            } else {
                OTHER.growAFruit();
            }
        };
        abstract void growAFruit();
    }


    public void growAFruit(String type) {
        Fruits.grow(type);
    }
}

我发现enums代码较长,可能不如if-else代码那么清晰,但我相信它更好,有人可以告诉我,为什么我错了(或者我可能是不)?

UPD - 将源代码更改为更具体问题。我将重新解释一下这个问题:是否有关于使用枚举而不是if-else的担忧?

14 个答案:

答案 0 :(得分:18)

您已经获得了有关改进Enums使用的良好答案。至于为什么它们比字符串常量更好:

我认为最大的好处是编译时错误检查。如果我打电话给growAFruit("watermelon"),编译器就不知道出了什么问题。因为我拼写正确,所以当你查看代码时,它不会被证明是一个错误。但是如果我去WATERMELEN.growAFruit(),编译器可以立即告诉你我拼错了它。

您还可以将growAFruit定义为一组简单易读的方法,而不是大块if - then - else s 。当你有几十个成果,或者当你开始添加harvestAFruit()packageAFruit()sellAFruit()等时,这变得更加明显。如果没有枚举,你将复制你的大if-else全部阻止,如果你忘了添加一个案例,它将落到默认情况下或什么也不做,而使用枚举编译器可以告诉你该方法尚未实现。

更好的编译器检查优点:如果你还有一个growVegetable方法和相关的字符串常量,那么没有什么可以阻止你调用growVegetable("pineapple")growFruit("peas")。如果你有一个"tomato"常数,那么知道你认为它是水果还是蔬菜的唯一方法就是阅读相关方法的源代码。再次,使用枚举编译器可以立即告诉您是否做错了。

另一个好处是将相关常数组合在一起并为他们提供合适的家庭。替代方案是在一些类中抛出的一堆public static final字段碰巧使用它们,或者插入常量接口。充满常量的界面甚至没有意义,因为如果你需要定义枚举,那么比编写界面更容易 。此外,在类或接口中,可能会意外地将相同的值用于多个常量。

他们也可迭代。要获取枚举的所有值,您只需调用Fruit.values(),而使用常量,您必须创建并填充自己的数组。或者,如果只是在示例中使用文字,则没有授权列表有效值。

奖金回合:IDE支持

  • 使用枚举,您可以使用IDE的自动完成功能和自动重构
  • 您可以在Eclipse中使用枚举值中的“查找引用”之类的内容,而您必须进行纯文本搜索以查找字符串文字,这通常也会返回大量误报(如果您使用,则会发生错误)静态最终常量,有人可以在某处使用字符串文字)

使用枚举的主要原因是,如果您在编译时不知道所有可能的值(即,您需要在程序运行时添加更多值)。在这种情况下,您可能希望将它们定义为类层次结构。另外,不要将一堆不相关的常量抛入枚举中并将其称为一天。应该有某种连接值的共同线程。如果更合适,您可以随时制作多个枚举。

答案 1 :(得分:17)

枚举是可行的方法,但您可以大大改善您的代码:

public static String grow(String type) {
    return Fruits.valueOf(type.toUpperCase()).gimmeFruit();
};

哦,你需要一个默认情况,这会让它变得更加困难。当然你可以这样做:

public static String grow(String type) {
    try{
        return Fruits.valueOf(type.toUpperCase()).gimmeFruit();
    }catch(IllegalArgumentException e){
        return Fruits.OTHER.gimmeFruit();
    }
};

但那很难看。我想我会这样:

public static String grow(String type) {
    Fruits /*btw enums should be singular */ fruit = Fruits.OTHER;
    for(Fruits candidate : Fruits.values()){
        if(candidate.name().equalsIgnoreCase(type)){
            fruit = candidate;
            break;
        }
    }
    return fruit.gimmeFruit();
};

此外,如果您的所有枚举方法都返回一个值,您应该重构您的设计,以便在构造函数中初始化值并在Enum类中定义的方法中返回它们,而不是单个项:

public enum Fruit{
    WATERMELON("watermelon fruit"),
    APPLE("apple fruit")
    // etc.

    ;
    private final String innerName;
    private Fruit(String innerName){ this.innerName = innerName; }
    public String getInnerName(){ return this.innerName; }
}

答案 2 :(得分:6)

您只有一半的更改更清洁。 grow方法应该像这样改变:

static String grow(Fruits type) {
    return type.gimmeFruit();
}

Fruits应该重命名为Fruit:苹果是水果,不是水果。

如果你真的需要保留你的字符串类型,那么定义一个方法(例如在枚举类本身中)返回与每种类型相关的Fruit。但是大多数代码应该使用Fruit而不是String。

答案 3 :(得分:4)

我想你想要一个Map<String, Fruit>(或<String, FruitGrower>)。

此映射可以由枚举构造函数自动填充,也可以由静态初始化程序填充。 (如果某些水果具有别名,它甚至可以在同一个枚举常量上映射多个名称。)

您的grow方法如下所示:

static void grow(String type) {
   Fruit f = map.get(type);
   if (f == null) {
       OTHER.growFruit();
   }
   else {
       f.growFruit();
   }
}

当然,你真的需要这里的字符串吗?你不应该总是使用枚举对象吗?

答案 4 :(得分:3)

我不确定我会在这里使用Enums。我可能在这里遗漏了一些东西(?),但我认为我的解决方案看起来像这样,每种类型的水果都有单独的类,都是基于一类水果类:

// Note: Added in response to comment below
public enum FruitType {
    WATERMELON,
    WHATEVERYOUWANT,
    ....
}


public class FruitFactory {

    public Fruit getFruitToGrow(FruitType type) {

        Fruit fruitToGrow = null;

        switch(type){
            case WATERMELON:
                fruitToGrow = new Watermelon();
                break;
            case WHATEVERYOUWANT:
                ...
            default:
                fruitToGrow = new Fruit();
        }

        return fruitToGrow;
    }
}


public class Fruit(){
    public void grow() {
        // Do other fruit growing stuff
    }
}



// Add a separate class for each specific fruit:
public class Watermelon extends Fruit(){
    @override
    public void grow(){
        // Do more specific stuff... 
    }
}

答案 5 :(得分:3)

我认为你走在正确的轨道上。我会为HashMap抛出一些额外的字节来摆脱字符串切换块。这样可以为您提供更清晰的外观,更少的代码以及更多可能的额外性能。

public enum Fruit {

    APPLE("ppl") {
        public void grow() {
            // TODO
        }
    },
    WATERMELON("wtrmln") {
        public void grow() {
            // TODO
        }
    },

    // SNIP extra vitamins go here

    OTHER(null) {
        public void grow() {
            // TODO
        }
    };

    private static Map<String, Fruit> CODE_LOOKUP;
    static {
        // populate code lookup map with all fruits but other
        Map<String, Fruit> map = new HashMap<String, Fruit>();
        for (Fruit v : values()) {
            if (v != OTHER) {
                map.put(v.getCode(), v);
            }
        }
        CODE_LOOKUP = Collections.unmodifiableMap(map);
    }

    public static Fruit getByCode(String code) {
        Fruit f = CODE_LOOKUP.get(code);
        return f == null ? OTHER : f;
    }

    private final String _code;

    private Fruit(String code) {
        _code = code;
    }

    public String getCode() {
        return _code;
    }

    public abstract void grow();
}

这就是你如何使用它:

Fruit.getByCode("wtrmln").grow();

简单,不需要FruitGrower,但如果您认为有必要,请继续使用。

答案 6 :(得分:2)

我的第二个肖恩·帕特里克·弗洛伊德(Sean Patrick Floyd)就可以了,但我想补充一点,你可以通过这样的方案更加大幅缩短你的代码事件:

enum Fruits {
   WATERMELON("watermelon fruit"),
   APPLE("apple fruit"); //...

   private final String gimme;

   private Fruits(String gimme) {
      this.gimme = gimme;
   }

   String gimmeFruit() { return this.gimme; }       
}

此外,“成长”方法是可疑的。不应该像

那样
public static String grow(Fruits f) {
   return f.gimmeFruit();
}

答案 7 :(得分:2)

您还可以通过使用变量存储gimmeFruit值并使用构造函数初始化来改进它。

(我实际上没有编译它,因此可能存在一些语法错误)

public class FruitGrower {
    enum Fruits {
        WATERMELON("watermelon fruit"),
        APPLE("apple fruit"),
        PINEAPPLE("pineapple fruit"),
        ORANGE("orange fruit"),
        OTHER("other fruit")

        private String gimmeStr;

        private Fruits(String gimmeText) {
            gimmeStr = gimmeText;
        }

        public static String grow(String type) {
            return Fruits.valueOf(type.toUpperCase()).gimmeFruit();
        }

        public String gimmeFruit(String type) {
            return gimmeStr;
        }

    }
}

编辑: 如果grow方法的类型不是相同的字符串,则使用Map将类型匹配定义为Enum并从地图返回查找。

答案 8 :(得分:2)

我经常在枚举中实现一个解析给定String的方法,并返回相应的枚举常量。我总是将此方法命名为parse(String)

有时我会重载此方法,以便通过另一个给定的输入类型解析枚举常量。

它的实现总是一样的: 迭代所有枚举值()并在击中时返回。最后做一个回归 - 通常是一个特定的枚举常量或null。在大多数情况下,我更喜欢null。

public class FruitGrower {
    enum Fruit {
        WATERMELON("wtrmln") {
            @Override
            void grow() {
                //do watermelon growing stuff
            }
        },

        APPLE("ppl") {
            @Override
            void grow() {
                //do apple growing stuff
            }
        },

        PINEAPPLE("pnppl") {
            @Override
            void grow() {
                //do pineapple growing stuff
            }
        },

        ORANGE("rng") {
            @Override
            void grow() {
                //do orange growing stuff
            }
        },

        OTHER("") {
            @Override
            void grow() {
                // do other fruit growing stuff
            }
        };

        private String name;

        private Fruit(String name) {
            this.name = name;
        }

        abstract void grow();

        public static Fruit parse(String name) {
            for(Fruit f : values()) {
                if(f.name.equals(name)){
                    return f;
                }
            }

            return OTHER; //fallthrough value (null or specific enum constant like here.)
        }
    }


    public void growAFruit(String name) {
        Fruit.parse(name).grow();
    }
}

如果您确实不需要此Fruit.OTHER,请将其删除。或者“其他水果”如何成长? OO 返回null然后在parse(String)方法中作为fallthrough值,并在grow()中调用growAFruit(String)之前进行空值检查。

最好在@CheckForNull方法中添加parse(String)注释。

答案 9 :(得分:2)

公共API完全相同。你仍然有相同的if-else块,它现在就在枚举中。所以我认为这不是更好。如果情况更糟,由于增加了复杂性。

'做水果种植'是什么意思?你是在谈论水果种植者所做的事情(直到土壤,植物种子等),还是水果本身所做的事情(发芽,发芽,开花等)?在原始示例中,操作由FruitGrower定义,但在您的修改中,它们由Fruit定义。当您考虑子类化时,这会产生很大的不同。例如,我可能想要定义一个MasterFruitGrower谁使用不同的流程来更好地种植水果。在grow()中定义Fruit操作会使其更难以推理。

水果种植业务有多复杂?如果您担心if-else块的行长,我认为更好的想法是定义单独的水果种植方法(growWatermelon()growOrange(),...)或定义{{ 1}} interface,为每个水果类型实现子类,并将它们存储在地图中或设置在FruitGrowingProcedure下。

答案 10 :(得分:2)

你的问题的答案是使用枚举,或者更好的是工厂和多态,如上所述。但是,如果你想完全摆脱开关(这就是你的if-else语句真正做的那样),一个好的方法,如果不是最好的话,那就是使用控制反转。因此,我建议使用spring,如下:

public interface Fruit {
   public void grow();
}

public class Watermelon implements Fruit {

   @Override
   public void grow()
   {
       //Add Business Logic Here
   }
}

现在,创建水果定位器界面,如下所示:

public interface FruitLocator {

Fruit getFruit(String type);
}

让main类引用FruitLocator对象,为它设置setter,然后调用getFruit命令:

private FruitLocator fruitLocator;

public void setFruitLocator (FruitLocator fruitLocator)
{
    this.fruitLocator = fruitLocator;
}

public void growAFruit(String type) {
    fruitLocator.getFruit(type).grow();
}

现在是棘手的部分。将FruitGrower类定义为spring bean,以及FruitLocator和Fruits:

<bean id="fruitGrower" class="yourpackage.FruitGrower">     
    <property name="fruitLocator" ref="fruitLocator" />
</bean>

<bean id="fruitLocator"
    class="org.springframework.beans.factory.config.ServiceLocatorFactoryBean">
    <property name="serviceLocatorInterface"
        value="yourpackage.FruitLocator" />
    <property name="serviceMappings" ref="locatorProperties" />
</bean>
<bean id="locatorProperties"
    class="org.springframework.beans.factory.config.PropertiesFactoryBean">
    <property name="location" value="classpath:fruits.properties" />
</bean>

    <bean id="waterMelon" class="yourpackage.WaterMelon">       
</bean>

唯一要做的就是在类路径中创建fruits.properties文件并添加type-bean映射,如下所示:

wtrmln=waterMelon        

现在,您可以根据需要添加任意数量的水果,您只需要创建一个新的水果类,将其定义为bean并将属性文件添加到您的属性文件中。比在代码中寻找if-else逻辑要灵活得多。

我知道这一开始看起来很复杂,但我认为如果不提及控制反转,这个话题就不完整了。

答案 11 :(得分:1)

要回答你的问题,我会说if-else或enums都不比你的具体问题更好。解决这个问题的一个好方法是使用封装和抽象,让继承为你处理“类型”。

每种水果之间种植水果的任务差别很大。明智的做法是让每个水果成为自己的类,以便在以后可能需要更多功能时提供更大的灵活性。

这是一个很好的例子:

// Abstract Base Class (Everything common across all fruits)
public abstract class Fruit {
    public abstract void grow();
}

// Concrete class, for each "type" of fruit
public class Apple extends Fruit {
    @override
    public void grow() {
        // Grow an apple
    }
}

public class Orange extends Fruit {
    @override
    public void grow() {
        // Grow an orange
    }
}

...

一旦定义了产品类,我们就可以创建一个可以在不进行任何“ify”类型检查的情况下生成水果的类。

public class FruitGrower {
    public void growAFruit(Fruit fruit) {
        fruit.grow();
    }
}

答案 12 :(得分:1)

嗯,枚举代码肯定会更长。我的建议是什么:

  • 当逻辑简单,小,并且将使用一次时使用字符串。
  • 如果必须多次使用常量,可以添加或修改,或者在复杂的代码中混合常量时使用枚举,那么最好使用枚举来澄清它们。

答案 13 :(得分:1)

如果不同的水果有不同的成长行为,我不会使用if-else或enum,而是使用面向对象的设计。 if-else / enum样式的问题(我认为它们在您的示例中是等效的)是它们将不同对象类型的行为收集到一个位置。如果添加新水果,则必须每次都编辑if-else / enum。这违反了open-closed principle

考虑你的4种果实是否有3种行为(例如成长,成熟,腐烂)。每个行为都有一个if-else / enum,每个行为包含4个水果参考。现在考虑添加第5个水果,你必须编辑3个不同的if-else / enum块。西瓜的行为与苹果没有任何关系,但它们在同一段代码中。现在考虑添加一个新的水果行为(例如香气) - 你必须创建一个新的if-else / enum,它有相同的问题。

我认为在大多数情况下,正确的解决方案是使用类(例如,每个水果具有一个实现类的Fruit接口/基类)并将行为放在每个类中,而不是在一个地方收集它们。因此,当您添加新水果时,唯一要更改的代码是编写新的水果类。

您可以使用众所周知的重构来获取现有代码并将其迁移到我所说的内容。它被称为Replace Conditional with Polymorphism