Несколько If-else или enum-какой из них предпочтительнее и почему?

вот исходный код:

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-->, компилятор не знал бы, что что-то не так. И так как я правильно написал, это не будет выделяться как ошибка, когда вы просматриваете код. Но если бы я ... --1-->, компилятор может сразу сказать вам, что я ошибся он.

вы также можете определить growAFruit как куча простых, удобных для чтения методов, а не большой блок if-then-elses. Это становится еще более очевидным, когда у вас есть несколько десятков фруктов или когда вы начинаете добавлять harvestAFruit(), packageAFruit(), sellAFruit(), etc. Без перечислений вы будете копировать свой большой блок if-else повсюду, и если вы забыли добавить случай, он упадет в случай по умолчанию или ничего не сделает, в то время как с перечислениями компилятор может сказать вам, что метод не имеет были реализованы.

еще больше проверки компилятора goodness: если у вас также есть growVegetable метод и связанные строковые константы, ничто не мешает вам вызывать growVegetable("pineapple") или growFruit("peas"). Если у вас есть "tomato" константа, единственный способ узнать, считаете ли вы это фруктом или овощем, - прочитать исходный код соответствующих методов. Еще раз, с перечислениями компилятор может сразу сказать вам, если вы сделали что-то неправильно.

другое преимущество заключается в том, что это группы связанных констант вместе и дает им правильный дом. Альтернативой является куча public static final поля, брошенные в какой-то класс, который их использует, или застрял интерфейс констант. Интерфейс полный константы не имеет смысла, потому что если это все, что вам нужно определить, что перечисление гораздо легче чем написание интерфейса. Кроме того, в классах или интерфейсах есть возможность случайно использовать одно и то же значение для нескольких постоянный.

они типа Iterable. Чтобы получить все значения перечисления, вы можете просто вызвать Fruit.values(), тогда как с константами вам придется создавать и заполнять свой собственный массив. Или если просто использовать литералы, как в вашем примере, есть нет авторского списка допустимых значений.

бонусный раунд: поддержка IDE

  • с перечислением вы можете использовать функцию автоматического завершения IDE и автоматические рефакторинги
  • вы можно использовать такие вещи, как" найти ссылки " в Eclipse со значениями перечисления, в то время как вам придется выполнять текстовый поиск, чтобы найти строковые литералы, которые обычно также возвращают много ложных срабатываний (событие, если вы используете статические конечные константы, кто-то мог использовать строковый литерал где-то)

главная причина не использовать перечисление было бы, если вы не знаете всех возможных значений во время компиляции (т. е. вам нужно добавить больше значений, пока программа бегущий.) В этом случае вы можете определить их как класс heirarchy. Кроме того, не бросайте кучу несвязанных констант в перечисление и не называйте это днем. Должен быть какой-то общий поток, соединяющий значения. Вы всегда можете сделать несколько перечислений, если это более целесообразно.


перечисления-это путь, но вы можете значительно улучшить свой код следующим образом:

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

кроме того, если все ваши методы перечисления возвращают значение, вы должны рефакторировать свой дизайн, чтобы инициализировать значения в конструкторе и возвращать их в методе, определенном в перечислении класс, а не отдельные элементы:

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

вы сделали только половину изменений, чтобы быть чище. Расти метод должен быть изменен следующим образом:

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

и Fruits следует переименовать в Fruit: яблоко-это плод, а не фрукты.

Если вам действительно нужно сохранить ваши строковые типы, то определите метод (в самом классе перечисления, например), возвращающий плод, связанный с каждым типом. Но большая часть кода должна использовать Fruit вместо String.


Я думаю, вы хотите Map<String, Fruit> (или <String, FruitGrower>).

эта карта может быть заполнена автоматически конструкторами перечисления или статическим инициализатором. (Он может даже отображать несколько имен на одной константе перечисления, если некоторые фрукты имеют псевдонимы.)

код grow метод тогда выглядит так:

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

конечно, вам действительно нужна строка здесь? Разве вы не должны всегда использовать объект enum?


Я не уверен, что перечисления здесь. Возможно, я что-то упускаю (?), но я думаю, что мое решение будет выглядеть примерно так, с отдельными классами для каждого типа фруктов, все на основе одного рода фруктов-класса:

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

Я думаю, что вы на правильном пути. Я бы пошел и добавил несколько дополнительных байтов для 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();

просто, нет необходимости в садовод, но пойти на это, если вы думаете, что это необходимо.


Я второй Шон Патрик Флойд на этом перечислении, но хотел бы добавить, что вы можете сократить свое событие кода более резко, используя такую схему:

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

   private final String gimme;

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

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

кроме того, метод" grow " подозрителен. Разве это не должно быть что-то вроде

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

вы также можете улучшить его с помощью переменной для хранения значения gimmeFruit и inititialize с конструктором.

(Я еще не собирал, поэтому могут быть некоторые синтаксические ошибки)

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 не является той же строкой, используйте карту для определения совпадений типа для перечисления и возврата поиска с карты.


Я часто реализую метод в перечислениях, анализирующих данную строку, и возвращает соответствующую константу перечисления. Я всегда называю этот метод parse(String).

иногда я перегружаю этот метод, чтобы проанализировать константу перечисления другим заданным типом ввода.

его реализация всегда одинакова: Повторите все значения перечисления () и вернитесь, когда вы нажмете один. Наконец, сделайте возврат как fallthrough-часто определенную константу перечисления или 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();
    }
}

Если вам это действительно не нужно удалить. Или как растет "другой плод"? ОО ... Возвращение null затем в parse(String) метод как значение fallthrough и сделать нулевую проверку перед вызовом grow() на growAFruit(String).

это хорошая идея, чтобы добавить @CheckForNull аннотация к parse(String) способ потом.


публичный API точно такой же. У вас все тот же блок if-else, он просто в перечислении. Так что, думаю, не лучше. Во всяком случае, это хуже, из-за дополнительной сложности.

что значит "заниматься выращиванием фруктов"? Вы говорите о том, что делает плодовод (возделывает почву, сажает семена и т. д.), Или о том, что делает сам плод (прорастает, прорастает, цветет и т. д.)? В исходном примере действия определяются параметром FruitGrower, но в вашем модификации они определяются Fruit. Это имеет большое значение, когда вы рассматриваете подклассы. Например, я мог бы определить MasterFruitGrower кто использует различные процессы, чтобы лучше росли плоды. Имея grow() операции Fruit делает это сложнее рассуждать.

насколько сложные плодоводства операции? Если вас беспокоит длина линии блока if-else, я думаю, что лучше определить отдельные методы выращивания фруктов (growWatermelon(), growOrange(), ...) или определить FruitGrowingProcedure интерфейс, реализующий подклассы для каждого типа фруктов и хранящий их на карте или в разделе FruitGrower.


ответ на ваш вопрос использует перечисления или, еще лучше, фабрики и полиморфизм, как упоминалось выше. Однако, если вы хотите полностью избавиться от коммутаторов (что и делает ваш оператор if-else), хороший способ, если не лучший, сделать это-использовать инверсию управления. Таким образом, я предлагаю использовать spring следующим образом:

public interface Fruit {
   public void grow();
}

public class Watermelon implements Fruit {

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

Теперь создайте интерфейс fruit locator следующим образом:

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 и фрукты:

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

единственное, что осталось сделать, это создать плоды.файл свойств в пути к классам и добавьте сопоставление типа-bean следующим образом:

wtrmln=waterMelon        

теперь вы можете добавить столько фруктов, сколько хотите, вам нужно только создать новый класс фруктов, определить его как Боб и добавить сопоставление с файлом свойств. Гораздо более масштабируемый, чем охота на логику if-else в коде.

Я знаю, что сначала это кажется сложным, но я думаю, что тема будет неполной без упоминания инверсии контроля.


чтобы ответить на ваш вопрос, я бы сказал, что ни 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
    }
}

...

после определения классов продуктов мы можем создать класс, который выращивает фрукты без проверки типа "ify".

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

Ну, код перечисления, безусловно, больше. Что я предлагаю:

  • используйте строки, когда логика проста, мала и будет использоваться 1 раз.
  • используйте перечисления, когда вам нужно использовать константы много раз, добавление или изменения возможны, или когда они смешиваются в сложном коде, поэтому лучше уточнить их с перечислением.

Если у разных фруктов разное поведение роста, я бы не использовал if-else или enum, а вместо этого использовал объектно-ориентированный дизайн. Проблема со стилем if-else/enum (я считаю их эквивалентными в вашем примере) заключается в том, что они собирают поведение разных типов объектов в одном месте. Если вы добавляете новый фрукт, вы должны редактировать свой if-else/enum каждый раз. Это нарушает принцип open-closed.

рассмотрим, если ваши 4 фрукты имели 3 поведения (например, расти, созревать, гнить). У вас будет if-else/enum для каждого поведения, Каждый из которых содержит 4 ссылки на фрукты. Теперь подумайте о добавлении 5-го фрукта, вы должны отредактировать 3 разных блока if-else/enum. Поведение арбуза не имеет ничего общего с яблоком, но они находятся в одном и том же фрагменте кода. Теперь подумайте о добавлении нового поведения фруктов (например, аромата) - вы должны создать новый if-else/enum, который имеет те же проблемы.

Я считаю, что правильным решением в большинстве случаев является использование классов (например, плоды интерфейс / базовый класс с одним классом реализации на плод) и поместите поведение в каждый класс вместо того, чтобы собирать все это в одном месте. Поэтому, когда вы добавляете новый фрукт, единственным изменяемым кодом является запись нового класса фруктов.

существует известный рефакторинг, который вы можете использовать, чтобы взять существующий код и перенести его на то, о чем я говорю. Это называется заменить условное на полиморфизм.