15

这是原始代码:

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 是否有任何顾虑?

4

14 回答 14

19

关于改进枚举的使用,您已经得到了很好的答案。至于为什么它们比字符串常量更好:

我认为最大的好处是编译时错误检查。如果我调用growAFruit("watermelon"),编译器将不知道有什么问题。而且由于我拼写正确,所以当您查看代码时,它不会被视为错误。但如果我这样做WATERMELEN.growAFruit(),编译器会立即告诉你我拼错了。

您还可以定义growAFruit为一堆简单易读的方法,而不是一大块if- then- elses。harvestAFruit()当您有几十个水果时,或者当您开始添加, packageAFruit(),等时,这一点变得更加明显sellAFruit()。没有枚举,您将复制您的大 if-else 块,如果您忘记添加一个案例,它会下降通过默认情况或什么都不做,而使用枚举,编译器可以告诉您该方法尚未实现。

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

另一个好处是它将相关的常量组合在一起并为它们提供了一个适当的位置。public static final另一种方法是在某个恰好使用它们的类中抛出一堆字段,或者卡住一个常量接口。充满常量的接口甚至没有意义,因为如果这就是你所需要的,定义枚举比写出接口要容易得多。此外,在类或接口中,可能会意外地为多个常量使用相同的值。

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

奖金回合:IDE 支持

  • 通过枚举,您可以使用 IDE 的自动完成功能和自动重构
  • 您可以在 Eclipse 中使用带有枚举值的“查找引用”之类的东西,而您必须进行纯文本搜索才能找到字符串文字,这通常也会返回很多误报(如果您使用静态最终常量,则事件,有人可能在某处使用了字符串文字)

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

于 2011-06-18T06:20:35.143 回答
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; }
}
于 2011-05-25T14:44:19.870 回答
6

您只进行了一半的更改以更清洁。增长方法应该这样改变:

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

并且Fruits应该重命名为Fruit:一个苹果是一种水果,而不是一种水果。

如果您确实需要保留字符串类型,则定义一个方法(例如在枚举类本身中)返回与每种类型关联的 Fruit。但是大部分代码应该使用 Fruit 而不是 String。

于 2011-05-25T14:45:55.963 回答
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();
   }
}

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

于 2011-06-16T14:05:33.087 回答
3

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

// 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... 
    }
}
于 2011-06-20T19:11:43.337 回答
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,但如果你认为有必要就去吧。

于 2011-06-22T09:11:15.543 回答
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();
}
于 2011-05-25T14:50:45.143 回答
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;
        }

    }
}

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

于 2011-05-25T14:59:35.400 回答
2

我经常在枚举中实现一个方法来解析给定的字符串并返回相应的枚举常量。我总是把这个方法命名为parse(String)

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

它的实现总是一样的:遍历所有枚举值()并在你点击一个时返回。最后做一个返回作为 fallthrough - 通常是一个特定的枚举常量或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删除它。或者“其他水果”是如何生长的?oOnull将 then in方法作为 fallthrough 值返回,并在调用inparse(String)之前进行 null 检查。grow()growAFruit(String)

@CheckForNull然后向方法添加注释是一个好主意parse(String)

于 2011-06-20T16:37:49.013 回答
2

公共 API 完全相同。你仍然有相同的 if-else 块,它现在只是在枚举中。所以我觉得没有更好的。如果有的话,由于增加了复杂性,情况会更糟。

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

水果种植操作有多复杂?如果您担心 if-else 块的行长,我认为更好的主意是定义单独的水果种植方法(growWatermelon(), growOrange(), ...)或定义一个FruitGrowingProcedure接口,为每种水果类型实现子类并存储它们在地图或设置下FruitGrower

于 2011-06-21T00:03:14.243 回答
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);
}

让主类引用一个 FruitLocator 对象,为其设置一个设置器,然后调用 getFruit 命令:

private FruitLocator fruitLocator;

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

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

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

<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 逻辑更具可扩展性。

我知道这起初看起来很复杂,但我认为如果不提及控制反转,这个主题将是不完整的。

于 2011-06-21T08:13:40.170 回答
1

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

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

这是一个很好的例子:

// 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
    }
}

...

一旦定义了产品类别,我们就可以创建一个无需任何“验证”类型检查即可种植水果的类别。

public class FruitGrower {
    public void growAFruit(Fruit fruit) {
        fruit.grow();
    }
}
于 2011-06-20T22:45:55.873 回答
1

好吧,枚举代码肯定更长。我的建议:

  • 当逻辑简单、小、会使用 1 次时使用字符串。
  • 当您必须多次使用常量时使用枚举,添加或修改是可能的,或者当它们混合在复杂的代码中时,最好用枚举来阐明它们。
于 2011-06-22T14:47:26.710 回答
1

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

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

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

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

于 2011-06-22T18:15:49.577 回答