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

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

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 - измененный исходный код, чтобы быть более проблемным. Я буду перефразировать вопрос: есть ли проблемы с использованием enum вместо if-else?

Ответы

Ответ 1

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

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

Ответ 2

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

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 возвращают значение, вы должны реорганизовать свой проект так, чтобы вы инициализировали значения в конструкторе и возвращали их в методе, определенном в классе 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; }
}

Ответ 3

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

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

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

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

Ответ 4

Я думаю, вам нужен Map<String, Fruit> (или <String, FruitGrower>).

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

Затем ваш метод grow выглядит следующим образом:

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

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

Ответ 5

Я не уверен, что буду использовать 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... 
    }
}

Ответ 6

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

Ответ 7

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

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

Ответ 8

Вы также можете улучшить его, используя переменную для сохранения значения 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;
        }

    }
}

EDIT: Если тип метода роста не является одной и той же строкой, то используйте карту для определения совпадений типа с Enum и возврата поиска с карты.

Ответ 9

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

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

Реализация всегда одна и та же: Итерации по всем значениям перечисления() и возврату, когда вы нажмете один. Наконец, сделайте возврат как провал - часто конкретную константу перечисления или 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, то удалите его. Или как растет "Другой плод"? оо Верните null, затем в метод parse(String) в качестве падающего значения и выполните нуль-проверку перед вызовом grow() в growAFruit(String).

Это хорошая идея добавить аннотацию @CheckForNull к методу parse(String).

Ответ 10

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

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

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

Ответ 11

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

Осталось только создать файл fruit.properties в вашем пути к классам и добавить сопоставление типа bean следующим образом:

wtrmln=waterMelon        

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

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

Ответ 12

Чтобы ответить на ваш вопрос, я бы сказал, что нет, если-else или перечисления предпочтительнее вашей конкретной проблемы. Хорошим способом приблизиться к этой проблеме будет использование инкапсуляции и абстракции, а также наследование с помощью типа "type" для вас.

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

Вот хороший пример:

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

Ответ 13

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

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

Ответ 14

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

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

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

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