Привет, Хабр! Применение switch-case в коде - давняя тема холиваров на форумах на предмет чистоты кода. Лично я склоняюсь к простому мнению: инструмент необходимо использовать по назначению.
Сегодня хотелось бы рассмотреть несколько простых кейсов, где switch-case является не лучшим выбором и предложить красивое и удобное решение проблемы.
Итак, непосредственно, кейс: от значения одной переменной зависит значение другой переменной.
Исходные данные: программа, имитирующая зоопарк. Она содержит несколько животных, представленных в виде перечисления Animal, и пару работников, описанных интерфейсом ZooWorker.
public enum Animal {
HIPPO,
PENGUIN,
MONKEY,
OWL;
}
public interface ZooWorker {
void feed(Animal animal);
}
Задача: научить работников кормить животных (по сути - реализовать интерфейс ZooWorker). Алгоритм действия очень прост - необходимо определить название корма, которым питается животное, и вывести в консоль сообщение о том, что животное покормлено и чем именно оно покормлено.
Первый вариант написан на java 11. Он является самым громоздким и выглядит следующим образом:
public class Java11Worker implements ZooWorker {
@Override
public void feed(Animal animal) {
String foodName;
switch (animal) {
case OWL:
foodName = "Mouse";
break;
case HIPPO:
foodName = "Grass";
break;
case PENGUIN:
foodName = "Fish";
break;
case MONKEY:
foodName = "Banana";
break;
default:
throw new IllegalArgumentException("Unknown animal!");
}
System.out.printf("%s eat: %s%n", animal, foodName);
}
}
Данное решение имеет ряд проблем:
В случае добавления животного в перечисление, требуется дописать и вышеуказанный код.
Разработчику никто не напомнит, что это нужно сделать. Т.е., если зоопарк разрастётся до сотен тысяч строк, вполне можно забыть, что при добавлении животного в перечисление необходимо еще и дописать код. В конце концов, это приведет к ошибке (и хорошо, если определено поведение default, в этом случае, по крайней мере, есть возможность быстро определить проблемное место).
При большом количестве животных switch-case сильно разрастётся.
Ну, и известная проблема switch-case в java 11 – бесконечный break, который легко пропустить.
Существует возможность немного отрефакторить вышеописанный пример и избавиться от проблемы №4 следующим образом:
public class Java11Worker implements ZooWorker {
@Override
public void feed(Animal animal) {
String foodName = getFoodName(animal);
System.out.printf("%s eat: %s%n", animal, foodName);
}
private String getFoodName(Animal animal) {
switch (animal) {
case OWL:
return "Mouse";
case HIPPO:
return "Grass";
case PENGUIN:
return "Fish";
case MONKEY:
return "Banana";
default:
throw new IllegalArgumentException("Unknown animal!");
}
}
}
Выглядит лучше, однако, другие проблемы остаются.
Начиная с java 14 и выше появилась возможность использовать более удобный формат switch-case. Представленное выше решение в новом формате будет выглядеть следующим образом:
public class Java17Worker implements ZooWorker {
@Override
public void feed(Animal animal, int animalCount) {
String foodName = switch (animal) {
case OWL -> "Mouse";
case HIPPO -> "Grass";
case PENGUIN -> "Fish";
case MONKEY -> "Banana";
};
System.out.printf("%s eat: %s%n", animal, foodName);
}
}
Помимо избавления от break, решилась проблема №2: если разработчик добавит в перечисление животное, но не определит необходимое поведение в switch-case, код просто не скомпилируется. Уже лучше, однако, третья и первая проблемы все ещё остаются.
В последнем решении перенесём зависимость переменных непосредственно в перечисление. Для этого немного изменим его:
public enum Animal {
HIPPO("Grass"),
PENGUIN("Fish"),
MONKEY("Banana"),
OWL("Mouse");
@Getter
private final String foodName;
Animal(String foodName) {
this.foodName = foodName;
}
}
Теперь можно реализовать работника всего в одну строку:
public class EasyWorker implements ZooWorker {
@Override
public void feed(Animal animal) {
System.out.printf("%s eat: %s%n", animal, animal.getFoodName());
}
}
В представленном решении, при добавлении элемента в перечисление, нет необходимости дописывать код, и разработчик точно не забудет нигде ничего дописать. К тому же, код не будет превращаться в лапшу при увеличении количества элементов в перечислении.
Попробуем немного расширить и усложнить наш кейс: теперь от значения одной переменной зависит не только значение другой, но и последующий алгоритм действий.
Задача остается той же - научить работников кормить животных. Однако, теперь для того, чтобы покормить животное, необходимо не просто определить требуемый корм, но и рассчитать его объем. Для этого изменим интерфейс ZooWorker:
public interface ZooWorker {
void feed(Animal animal, int animalCount);
}
Для большей наглядности представим, что расчёт корма не везде производится простым умножением на коэффициент, а используется некая формула:
Для бегемотов: [количество бегемотов^2]
Для филинов: [количество филинов * 3]
Для пингвинов: [(количество пингвинов ^ 3)/2]
Для обезьян: [количество обезьян * 10]
Ниже представлены решения по уже используемым шаблонам:
Решение switch-case на java 11
public class Java11Worker implements ZooWorker {
@Override
public void feed(Animal animal, int animalCount) {
String foodName;
int foodQuantity;
switch (animal) {
case OWL:
foodName = "Mouse";
foodQuantity = animalCount * 3;
break;
case HIPPO:
foodName = "Grass";
foodQuantity = (int) Math.pow(animalCount, 2);
break;
case MONKEY:
foodName = "Banana";
foodQuantity = animalCount * 10;
break;
case PENGUIN:
foodName = "Fish";
foodQuantity = (int) (Math.pow(animalCount, 3)/2);
break;
default:
throw new IllegalArgumentException("Unknown animal!");
}
System.out.printf("%s eat: %d %s", animal, foodQuantity, foodName);
}
}
Немного переработанный код будет выглядеть следующим образом:
public class Java11Worker implements ZooWorker {
@Override
public void feed(Animal animal, int animalCount) {
String foodName = getFoodName(animal);
int foodQuantity = getfoodQuantity(animal, animalCount);
System.out.printf("%s eat: %d %s", animal, foodQuantity, foodName);
}
private String getFoodName(Animal animal) {
switch (animal) {
case OWL:
return "Mouse";
case HIPPO:
return "Grass";
case MONKEY:
return "Banana";
case PENGUIN:
return "Fish";
default:
throw new IllegalArgumentException("Unknown animal!");
}
}
private int getfoodQuantity(Animal animal, int animalCount) {
switch (animal) {
case OWL:
return animalCount * 3;
case HIPPO:
return (int) Math.pow(animalCount, 2);
case MONKEY:
return animalCount * 10;
case PENGUIN:
return (int) (Math.pow(animalCount, 3)/2);
default:
throw new IllegalArgumentException("Unknown animal!");
}
}
}
Решение switch-case на java 17
public class Java17Worker implements ZooWorker {
@Override
public void feed(Animal animal, int animalCount) {
String foodName = switch (animal) {
case OWL -> "Mouse";
case HIPPO -> "Grass";
case PENGUIN -> "Fish";
case MONKEY -> "Banana";
};
int foodQuantity = switch (animal) {
case OWL -> animalCount * 3;
case HIPPO -> (int) Math.pow(animalCount, 2);
case PENGUIN -> (int) (Math.pow(animalCount, 3) / 2);
case MONKEY -> animalCount * 10;
};
System.out.printf("%s eat: %d %s", animal, foodQuantity, foodName);
}
}
Для решения через enum требуется доработать перечисление:
public enum Animal {
HIPPO("Grass", animalCount -> (int) Math.pow(animalCount, 2)),
PENGUIN("Fish", animalCount -> (int) (Math.pow(animalCount, 3) / 2)),
MONKEY("Banana", animalCount -> animalCount * 10),
OWL("Mouse", animalCount -> animalCount * 3);
@Getter
private final String foodName;
@Getter
private final IntFunction<Integer> foodCalculation;
Animal(String foodName, IntFunction<Integer> foodCalculation) {
this.foodName = foodName;
this.foodCalculation = foodCalculation;
}
}
И, собственно, сам работник:
public class EasyWorker implements ZooWorker {
@Override
public void feed(Animal animal, int animalCount) {
System.out.printf("%s eat: %d %s",
animal,
animal.getFoodCalculation().apply(animalCount),
animal.getFoodName()
);
}
}
Перейдем к заключительному примеру. Предположим, расчёт корма представляет собой сложную логику, которую не получится передать в качестве лямбды.
В этом случае для каждого животного создадим отдельного сотрудника, который будет кормить только его.
Реализованные работники
public class HippoWorker implements ZooWorker {
private final String foodName;
public HippoWorker(String foodName) {
this.foodName = foodName;
}
@Override
public void feed(int animalCount) {
//Сложная логика
int foodQuantity = (int) Math.pow(animalCount, 2);
System.out.printf("Hippo eat: %d %s", foodQuantity, foodName);
}
}
public class MonkeyWorker implements ZooWorker {
private final String foodName;
public MonkeyWorker(String foodName) {
this.foodName = foodName;
}
@Override
public void feed(int animalCount) {
//Сложная логика
int foodQuantity = animalCount * 10;
System.out.printf("Monkey eat: %d %s", foodQuantity, foodName);
}
}
public class OwlWorker implements ZooWorker {
private final String foodName;
public OwlWorker(String foodName) {
this.foodName = foodName;
}
@Override
public void feed(int animalCount) {
//Сложная логика
int foodQuantity = animalCount * 3;
System.out.printf("Owl eat: %d %s", foodQuantity, foodName);
}
}
public class PenguinWorker implements ZooWorker {
private final String foodName;
public PenguinWorker(String foodName) {
this.foodName = foodName;
}
@Override
public void feed(int animalCount) {
//Сложная логика
int foodQuantity = (int) (Math.pow(animalCount, 3) / 2);
System.out.printf("Penguin eat: %d %s", foodQuantity, foodName);
}
}
Представим, как решить задачу «покормить всех животных» «в лоб»: например, в вызывающем классе собираем список (множество) всех работников, и получаем возможность по очереди вызвать метод feed у элементов списка. Вышло бы что-то вроде этого:
public class Feeder {
private final List<ZooWorker> workerList;
public Feeder(List<ZooWorker> workerList) {
this.workerList = workerList;
}
public void feedAll(int animalCount) {
workerList.forEach(zooWorker -> zooWorker.feed(animalCount));
}
}
Выглядит неплохо, однако, что делать, если потребуется отдельный метод для каждого животного, например, public void feedHippo(int animalCount)
, или универсальный public void feedAnimal(Animal animal, int animalCount)
? На данном этапе возникнут проблемы. Решением может быть создание мапы,содержащей всех работников. Но тогда необходимо хранить ключи к ней (или хардкодить). Можно сделать ключом непосредственно значение перечисления, но все равно придется где-то собирать мапу. Другим решением может стать внедрение работников в качестве полей, но их [работников] может быть много, а feedAnimal
будет опять работать на громоздком switch-case. И все эти варианты нужно поддерживать, а при добавлении нового животного придется искать по коду, где отрабатывает логика кормления.
Однако, если изменим перечисление следующим образом:
public enum Animal {
HIPPO(new HippoWorker("Grass")),
PENGUIN(new PenguinWorker("Fish")),
MONKEY(new MonkeyWorker("Banana")),
OWL(new OwlWorker("Mouse"));
private final ZooWorker worker;
Animal(ZooWorker worker) {
this.worker = worker;
}
public void feed(int animalCount) {
worker.feed(animalCount);
}
}
Все становится настолько простым:
public class Feeder {
public void feedAll(int animalCount) {
Arrays.stream(Animal.values())
.forEach(animal -> animal.feed(animalCount));
}
public void feedHippo(int animalCount) {
Animal.HIPPO.feed(animalCount);
}
public void feedAnimal(Animal animal, int animalCount) {
animal.feed(animalCount);
}
}
Подведем итоги.
Мы рассмотрели 3 варианта с нарастающей сложностью, где можно красиво применить перечисление вместо switch-case. Предложенные решения задач просты в реализации и более поддерживаемы и расширяемы по сравнению с решением "в лоб" с использованием switch-case.
Комментарии (24)
whalemare
05.08.2022 05:08+1Полиморфизм это супер, тоже стараюсь использовать его, когда это не оверхед
Hardcoin
05.08.2022 06:10+10В этом случае для каждого животного создадим отдельного сотрудника, который будет кормить только его
Смешивание архитектуры и бизнес-логики. Зоопарк может не захотеть нанимать дополнительных работников только потому, что программисты так отрефакторили код. При этом классы, которые отвечают за реально существующих работников всё же могут появиться и будет полная каша из названий.
PapaKarlo787
05.08.2022 18:20Вот, да! А ещё не понятно почему все указанные параметры это не прерогатива животных их определять?
panzerfaust
05.08.2022 06:50+15Связывание типа животного с относящейся к нему бизнес-логикой в рамках енам-класса - это уже нарушение SRP и смешение уровней абстракции.
Кроме того, финальный вариант из статьи вовсе не финальный. Что если с каждым животным потребуется связать не только тип работника, но и, например, тип вольера, тип поставщика кормов, режим дня? А что если работник нужен не в одном экземпляре (с мышом справится один человек, а на бегемота нужны двое-трое)? Енам будет обрастать новыми private final полями и дополнительной бизнес-логикой, а проблема нарушения SRP будет только усугубляться.
Я б использовал абстрактную фабрику.
Hidden text
Как это часто бывает, GOF спешит на помощь даже спустя 30 лет
AnimalFactory factory = animalFactory.findFactory(animal) List<Workers> workers = factory.getWorkers() List<FoodItem> food = factory.getFood() ... AnimalFactory findFactory(Animal animal) { switch(animal) case HIPPO -> new HippoFactory() ... }
Такой подход максимально развязывает руки. Например, работники могут браться из пула, а не быть строго назначенными на каждый тип животного.
Короче, лучше вернутся к исчерпывающим свитчам из 14 джавы, чем накручивать енам левой логикой. Разрастающийся свитч будет локализован внутри фабрики. Ну а что потребуется дописывать код - тут уж извините, работа у нас такая. Плюс не забываем о TDD. Кейс добавления нового типа животного должен сразу появляться в тестах, что также минимизирует вероятность что-то забыть.
brutfooorcer Автор
05.08.2022 09:02Если животное обрастает зависимостями, как в вашем примере - можно связать его [животное] с конкретной фабрикой, и тогда и не придется нагружать енам логикой, и избавимся от всех перечисленных в статье проблем в методе
findFactory()
.Естественно, расширять енам кучей полей/логики - плохая идея. Но в тех случаях, когда это оправдано - почему нет? Приведенный зоопарк - лишь наглядный пример. Также, я не согласен, что примеры нарушают SRP: енам имеет единственную причину для изменений - добавление нового животного.
Ну и, на самом деле, свичи из 14 джавы действительно уже хороши и уже являются не плохой альтернативой - я не считаю, что нужно везде все срочно переделывать на перечисления. Проблема в том, что далеко не все проекты используют джава 14, и некоторые кейсы с перечислениями точно будут выглядеть лучше)
iamkisly
05.08.2022 10:08+1Совершенно верно, у рабоника может быть список пермишнов (орнитолог не может чесать пузико киту) поэтому этим должна заниматься фабрика.
Nialpe
05.08.2022 08:45+6По-моему термин Worker внутри термина Animal не совсем уместен и вносит путаницу. Перечисление Animal должно содержать информацию о животном в соответствии с так любимой буквой S из магического слова SOLID. И тогда вместо Worker в Animal можно поместить информацию о рационе животного, да и то достаточно условно т.к. можно еще накрутить, что детеныши и взрослые особи питаются по-разному и т.п.(погружаться можно до бесконечности).
А далее, если речь о зоопарке. Есть список работников-ухаживающих за животными, есть понимание кто из работников на рабочем месте (не заболел, не в отпуске), кто умеет кормить тот или иной животного и назначать дневную нагрузку на рабочего, а рабочий лезет в перечисление Animal, получает рацион, берет со склада корм и кормит животного.
В разработке о правильности и неправильности абстракций спорить можно бесконечно. И мое предложение вполне вероятно не идеал. Но мыслями поделиться хотелось с вами и с публикой.
vesper-bot
05.08.2022 10:20-1Что обидно, на низком уровне вы все равно никуда не денетесь от GOTO. Весь этот сахар нужен только чтобы набирать код было удобнее, а мест, где человек может ошибиться, меньше. Но и только.
datacompboy
05.08.2022 15:01+4Да, мы всё еще говорим в конечном счете о нагреве окружающей среды. С этой точки зрения более крутые языки лучше, так как греют не только в продакшене но и при компиляции!
Hardcoin
05.08.2022 19:36Не низком уровне не goto, а электроны. Избавиться от них не выйдет, но какая разница? Код пишут для людей, а не только для машин (если это не личный проект для самого себя).
Sergey_zx
06.08.2022 14:03Как уже выше писали, делаем список обьектов и к нему придефайненый енум таким образом, что бы его числовое значение было индексом соответствующего объекта в списке. И никаких if-case-switch-goto.
Ну а далее уже в зависимости от задачи. Или список-енум статически линкованный, или инициализируемый при старте, или динамически изменяемый.
VadimZ76
05.08.2022 12:23-1System.out.printf("Hippo eat: %d %s", , foodQuantity, foodName);
Две запятые? Я программировал на java, но не помню такого? Это опечатка?
Иначе хотелось бы понять, что за магия?
feoktant
05.08.2022 13:18+3По курсу Скалы, было такое эмперическое правило:
- Если собираетесь увеличивать число операцией над +/- стабильными сущностями - используйте ФП подход, switch/case
- Если собираетесь увеличивать количество сущностей, а операции +\- стабильны - используйте ООП подход
И свитч-кейс, и ООП вполне имеют право на существование. А с приходом рекордов в Джаве станет еще больше опций для выбора
ystal
05.08.2022 15:04import lombok.Getter; import java.util.List; import java.util.function.BiConsumer; import java.util.function.BiFunction; import java.util.function.Consumer; class Scratch { public enum Animal { HIPPO("Grass", BaseWorker::feedHippo), PENGUIN("Fish", BaseWorker::feedPenguin), MONKEY("Banana", BaseWorker::feedMonkey), OWL("Mouse", BaseWorker::feedOwl); @Getter private final String foodName; @Getter private final BiConsumer<BaseWorker, Integer> foodCalculation; Animal(String foodName, BiConsumer<BaseWorker, Integer> foodCalculation) { this.foodName = foodName; this.foodCalculation = foodCalculation; } } //@Component - воркер может быть под управлением DI контейнера static class BaseWorker { //@Autowired - с инжектом внешних зависимостей. MenuRepository menuRepository; public void feedHippo(int animalCount) { //Сложная логика int foodQuantity = (int) Math.pow(animalCount, 2); System.out.printf("Hippo eat: %d %s", foodQuantity, menuRepository.findMenuFor("Hippo")); } void feedPenguin(int animalCount) { //Сложная логика int foodQuantity = (int) (Math.pow(animalCount, 3) / 2); System.out.printf("Penguin eat: %d %s", foodQuantity, menuRepository.findMenuFor("Penguin")); } void feedMonkey(int animalCount) { //Сложная логика int foodQuantity = animalCount * 10; System.out.printf("Penguin eat: %d %s", foodQuantity, menuRepository.findMenuFor("Monkey")); } void feedOwl(int animalCount) { //Сложная логика int foodQuantity = animalCount * 3; System.out.printf("Penguin eat: %d %s", foodQuantity, menuRepository.findMenuFor("Owl")); } } }
Мне кажется, можно чутка дальше подход развить...
Вариант более лаконичный в плане создания классов, позволяет инжектить зависимости и менее хардкордный в плане воркеров.MyraJKee
07.08.2022 15:34Чето как-то не очень? А если кол-во наименований животных будет постоянно увеличиваться?
Throwable
05.08.2022 16:48+3Идея интересная, однако не стоит так делать повсеместно. Здесь вы инкапсулируете в класс животного свойства, которые относятся больше к компетенции ZooWorker-а, и не имеют большого смысла в отрыве от него. Например вот у нас в зоопарке открылся ветеринарный отдел, и теперь снова будем нагружать бедных животных новыми свойствами и всей логикой, относящейся к ветеринарной службе? В итоге когда предметная область вырастет, ваши животные превратятся в пухлых монстров.
fransua
06.08.2022 12:41Кажется, что тут уместнее абстрактный класс Animal и его имплементации с реализацией нужных методов / свойств.
А перечисления я привык использовать как удобную замену констант, которая исчезает при компиляции и не несет оверхеда.
souls_arch
07.08.2022 05:45-1Хороший велосипед ты изобрел. Перейдем к читабельности/ поддерживаемости кода в рамках не микросервиса(да даже в нем) и монолита.
Я только учус. Имхо, автор вообще не понимает, как работает jvm и что скрывается за реализацей енум. Спасибо за мнение, придете в команду, не забудьте навязать! Отпишитесь - похохочем!
sargon5000
07.08.2022 16:30Что красивого в том, чтобы захардкодить этот зоопарк? И при любом пополнении или смерти животного переписывать текст программы? Может, я кощунственные слова скажу, но я б загнал зоопарк – людей, зверей, пищу, зарплатные ведомости – в простенькую БД. И ни в одной строке программы не будут упомянуты "Monkey" или "Сторож дядя Петя".
Pavel_nobranch
В приведенном прмере идет чистый key - value. Такое-то животное, ест такой-то корм. Поэтому оптимально использовать Dictionary. Если нужна возможность расширить список кормов, то валуем нужно сделать лист<>. Куда добавлять корма. А если в валуй загнать обьект класса, то можно реализовать любую логику.
AlexZaharow
В C# есть кортежи. Нет ли в Java чего-то подобного?
isden
Records?
Еще есть третьесторонние либы, типа javatuples.
Pavel_nobranch
Пишу на C#. Про java знаю что он идентичен. Писал на java/fx давно чуть-чуть, перешел на c#/wpf. Поэтому не эксперт.