Как красиво избавиться от switch-case посредством перечисления
Привет, Хабр! Применение switch-case в коде — давняя тема холиваров на форумах на предмет чистоты кода. Лично я склоняюсь к простому мнению: инструмент необходимо использовать по назначению.
Сегодня хотелось бы рассмотреть несколько простых кейсов, где switch-case является не лучшим выбором и предложить красивое и удобное решение проблемы.
Итак, непосредственно, кейс: от значения одной переменной зависит значение другой переменной.
Исходные данные: программа, имитирующая зоопарк. Она содержит несколько животных, представленных в виде перечисления Animal, и пару работников, описанных интерфейсом ZooWorker.
Задача: научить работников кормить животных (по сути — реализовать интерфейс ZooWorker). Алгоритм действия очень прост — необходимо определить название корма, которым питается животное, и вывести в консоль сообщение о том, что животное покормлено и чем именно оно покормлено.
Первый вариант написан на java 11. Он является самым громоздким и выглядит следующим образом:
Данное решение имеет ряд проблем:
В случае добавления животного в перечисление, требуется дописать и вышеуказанный код.
Разработчику никто не напомнит, что это нужно сделать. Т.е., если зоопарк разрастётся до сотен тысяч строк, вполне можно забыть, что при добавлении животного в перечисление необходимо еще и дописать код. В конце концов, это приведет к ошибке (и хорошо, если определено поведение default, в этом случае, по крайней мере, есть возможность быстро определить проблемное место).
При большом количестве животных switch-case сильно разрастётся.
Ну, и известная проблема switch-case в java 11 – бесконечный break, который легко пропустить.
Существует возможность немного отрефакторить вышеописанный пример и избавиться от проблемы №4 следующим образом:
Выглядит лучше, однако, другие проблемы остаются.
Начиная с java 14 и выше появилась возможность использовать более удобный формат switch-case. Представленное выше решение в новом формате будет выглядеть следующим образом:
Помимо избавления от break, решилась проблема №2: если разработчик добавит в перечисление животное, но не определит необходимое поведение в switch-case, код просто не скомпилируется. Уже лучше, однако, третья и первая проблемы все ещё остаются.
В последнем решении перенесём зависимость переменных непосредственно в перечисление. Для этого немного изменим его:
Теперь можно реализовать работника всего в одну строку:
В представленном решении, при добавлении элемента в перечисление, нет необходимости дописывать код, и разработчик точно не забудет нигде ничего дописать. К тому же, код не будет превращаться в лапшу при увеличении количества элементов в перечислении.
Попробуем немного расширить и усложнить наш кейс: теперь от значения одной переменной зависит не только значение другой, но и последующий алгоритм действий.
Задача остается той же — научить работников кормить животных. Однако, теперь для того, чтобы покормить животное, необходимо не просто определить требуемый корм, но и рассчитать его объем. Для этого изменим интерфейс ZooWorker:
Для большей наглядности представим, что расчёт корма не везде производится простым умножением на коэффициент, а используется некая формула:
Для бегемотов: [количество бегемотов^2]
Для филинов: [количество филинов * 3]
Для пингвинов: [(количество пингвинов ^ 3)/2]
Для обезьян: [количество обезьян * 10]
Ниже представлены решения по уже используемым шаблонам:
Решение switch-case на java 11
Немного переработанный код будет выглядеть следующим образом:
Для решения через enum требуется доработать перечисление:
И, собственно, сам работник:
Перейдем к заключительному примеру. Предположим, расчёт корма представляет собой сложную логику, которую не получится передать в качестве лямбды.
В этом случае для каждого животного создадим отдельного сотрудника, который будет кормить только его.
Представим, как решить задачу «покормить всех животных» «в лоб»: например, в вызывающем классе собираем список (множество) всех работников, и получаем возможность по очереди вызвать метод feed у элементов списка. Вышло бы что-то вроде этого:
Выглядит неплохо, однако, что делать, если потребуется отдельный метод для каждого животного, например, public void feedHippo(int animalCount) , или универсальный public void feedAnimal(Animal animal, int animalCount) ? На данном этапе возникнут проблемы. Решением может быть создание мапы,содержащей всех работников. Но тогда необходимо хранить ключи к ней (или хардкодить). Можно сделать ключом непосредственно значение перечисления, но все равно придется где-то собирать мапу. Другим решением может стать внедрение работников в качестве полей, но их [работников] может быть много, а feedAnimal будет опять работать на громоздком switch-case. И все эти варианты нужно поддерживать, а при добавлении нового животного придется искать по коду, где отрабатывает логика кормления.
Однако, если изменим перечисление следующим образом:
Все становится настолько простым:
Мы рассмотрели 3 варианта с нарастающей сложностью, где можно красиво применить перечисление вместо switch-case. Предложенные решения задач просты в реализации и более поддерживаемы и расширяемы по сравнению с решением «в лоб» с использованием switch-case.
Alternative to Switch Case in Java
Is there any alternative way to implement a switch case in Java other than if else which is not looking good. A set of values will be there in combination, according to the selection corresponding method has to be executed.
![]()
14 Answers 14
If you have plenty of switch/case statements around your code and they are driving you crazy.
You could opt for the Refactoring: Replace conditional with polymorphism.
Let’s say you have a piece of software that is used to save information to different devices: 4 persistence operations are defined: fetch, save, delete, update, which could be implemented by N number of persistence mechanism ( flat files, network, RDBMS, XML, etc ) .
Your code have to support them all so in 4 different places you have this:
BEFORE
Same for save/delete/update
Using switch/case statement becomes problematic:
Each time you want to add a new type you have to insert new switch/case in each section.
Many times, some types are similar, and they don’t need a different switch/case ( you could cascade them )
Some other they are, and some times they differ slightly
You may even need to load different type at runtime ( like plugins )
So the refactoring here would be to add an interface or abstract type and have the different types implement that interface and delegate the responsibility to that object.
So you would have something like this:
AFTER
And different implementations
And the other types would implement according to their logic. Network would deal with sockets, and streams, DB would deal with JDBC, ResultSets etc. XML with node etc.etc.
And finally you just have to delegate the invocations.
So, you just have to create the correct instance for the persistence manager according to the type only once. Then all the invocations are resolved by polymorphism. That’s one of the key features of Object Oriented Technology.
If you decide you need another persistence manager, you just create the new implementation and assigned to the class.
Java Notes
Complex switch statements with many possible case s tend to result in cluttered code. When code checkers start to complain about the complexity of a method, it becomes time to refactor that piece, but often that sounds simpler than it actually is.
With lambda’s in java 8 these switch statements can easily be simplified resulting in cleaner, easier to read and extend code.
Lets take for example the below switch statement and turn that into a nice and easy to understand piece of code using a Map and some lamba expressions.
Its easy to imagine that such a switch statement can grow and become pretty complex.
In java 8 we now have the ability to use lambda expressions and pass them around to functions. This allows us to replace the switch/case block with a more flexible and more natural implementation.
Lets first start with creating a map that associates the enum with the lambda that produces the result for the given enum value.
Now the find method can be changed to use the lambdas instead:
The example sources can be found on github
Чем заменить switch java
Здравствуйте, Hard_Club, Вы писали:
H_C>Если есть конструирование объекта с помощью вложенного switch с помощью анализа некоторых признаков, то каким паттерном проектирования / примитивом это можно перекрыть?
Если спрашивают про паттерн на собсеседовании, значит имеют что-то более сложное чем присваивание одной переменной.
Скорее всего, имели ввиду State pattern или Strategy pattern- они очень похожи, назначение немного разное.
State
This pattern is used in computer programming to encapsulate varying behavior for the same routine based on an object’s state object
Strategy
The Strategy pattern embodies two such principles—encapsulate the concept that varies and program to an interface, not an implementation.
Если больше ничего не спрашивали, значит это был State. Хотя можно было про оба рассказать.
| От: | megapoliss |
| Дата: | 24.12.13 14:05 |
| Оценка: |
Здравствуйте, Hard_Club, Вы писали:
H_C>Если есть конструирование объекта с помощью вложенного switch с помощью анализа некоторых признаков, то каким паттерном проектирования / примитивом это можно перекрыть?
| От: | ZeroLatency |
| Дата: | 24.12.13 14:08 |
| Оценка: |
Речь идет про Factory method
Factory methods encapsulate the creation of objects.
| От: | ZeroLatency |
| Дата: | 24.12.13 14:10 |
| Оценка: |
Здравствуйте, megapoliss, Вы писали:
M>Здравствуйте, Hard_Club, Вы писали:
H_C>>Если есть конструирование объекта с помощью вложенного switch с помощью анализа некоторых признаков, то каким паттерном проектирования / примитивом это можно перекрыть?
M>http://refactoring.com/catalog/replaceConditionalWithPolymorphism.html
И как вы собираетесь создавать инстансы конкретных объектов, на каких программных основаниях? Нужно же выбор делать при конструировании.
| От: | Hard_Club |
| Дата: | 24.12.13 14:44 |
| Оценка: |
H_C>>Если есть конструирование объекта с помощью вложенного switch с помощью анализа некоторых признаков, то каким паттерном проектирования / примитивом это можно перекрыть?
B>Возможно имеется ввиду, что если switch реализован по enum, то можно просто сделать абстрактный метод в этом конкретном enum-е и для каждого элемента прописать реализацию.
Можно пример, пожалуйста?
| От: | Hard_Club |
| Дата: | 24.12.13 14:46 |
| Оценка: |
Здравствуйте, ZeroLatency, Вы писали:
ZL>Здравствуйте, Hard_Club, Вы писали:
H_C>>Если есть конструирование объекта с помощью вложенного switch с помощью анализа некоторых признаков, то каким паттерном проектирования / примитивом это можно перекрыть?
ZL>Если спрашивают про паттерн на собсеседовании, значит имеют что-то более сложное чем присваивание одной переменной.
ZL>Скорее всего, имели ввиду State pattern или Strategy pattern- они очень похожи, назначение немного разное.
ZL>State
ZL>This pattern is used in computer programming to encapsulate varying behavior for the same routine based on an object’s state object
ZL>Strategy
ZL>The Strategy pattern embodies two such principles—encapsulate the concept that varies and program to an interface, not an implementation.
ZL>Если больше ничего не спрашивали, значит это был State. Хотя можно было про оба рассказать.
Правильный ответ был именно State и Strategy. только непонятно, как ими заменить switch, коструирующий по-разному объект в своих ветках
| От: | Аноним |
| Дата: | 24.12.13 15:05 |
| Оценка: |
H_C>Правильный ответ был именно State и Strategy. только непонятно, как ими заменить switch, коструирующий по-разному объект в своих ветках
у меня есть подозрение что вы не все рассказали, поэтому мы не можем вас понять.
Если правильный ответ State и Strategy, то, скорее всего, имеется ввиду что объект создается один и тот же, просто в него еще State или Strategy объект инжектится и во время выполнения, чтобы не запускать switch и для конкретного объекта выполнять конкретный код, просто выполняется конкретный State или Strategy.
| От: | Hard_Club |
| Дата: | 24.12.13 15:16 |
| Оценка: |
Здравствуйте, Аноним, Вы писали:
H_C>>Правильный ответ был именно State и Strategy. только непонятно, как ими заменить switch, коструирующий по-разному объект в своих ветках
А>у меня есть подозрение что вы не все рассказали, поэтому мы не можем вас понять.
А>Если правильный ответ State и Strategy, то, скорее всего, имеется ввиду что объект создается один и тот же, просто в него еще State или Strategy объект инжектится и во время выполнения, чтобы не запускать switch и для конкретного объекта выполнять конкретный код, просто выполняется конкретный State или Strategy.
Объект и в правду создается один, только с вызовом разных конструкторов.
| От: | Blazkowicz |
| Дата: | 24.12.13 15:48 |
| Оценка: |
Здравствуйте, Hard_Club, Вы писали:
H_C>>>Если есть конструирование объекта с помощью вложенного switch с помощью анализа некоторых признаков, то каким паттерном проектирования / примитивом это можно перекрыть?
B>>Возможно имеется ввиду, что если switch реализован по enum, то можно просто сделать абстрактный метод в этом конкретном enum-е и для каждого элемента прописать реализацию.
H_C>Можно пример, пожалуйста?
За точность синтаксиса не ручаюсь. Но идея, полагаю, понятна?
| От: | Hard_Club |
| Дата: | 24.12.13 15:53 |
| Оценка: |
B>За точность синтаксиса не ручаюсь. Но идея, полагаю, понятна?
B>
А как это вызывать?
| От: | andyag | |
| Дата: | 24.12.13 16:48 | |
| Оценка: | +1 | |
Здравствуйте, Hard_Club, Вы писали:
H_C>Если есть конструирование объекта с помощью вложенного switch с помощью анализа некоторых признаков, то каким паттерном проектирования / примитивом это можно перекрыть?
Сильно зависит от того, в чём именно вы видите проблему. В switch нет ничего плохого, даже если он длинный. Главное, чтобы читалось. Если у вас желание наархитектить, можно подумать вот в такую сторону:
Дисклеймер: я бы хорошо взвесил плюсы-минусы, прежде чем пилить такой дизайн
| От: | Blazkowicz |
| Дата: | 24.12.13 17:30 |
| Оценка: |
Здравствуйте, Hard_Club, Вы писали:
H_C>А как это вызывать?
| От: | Blazkowicz |
| Дата: | 24.12.13 17:48 |
| Оценка: |
| От: | Аноним |
| Дата: | 24.12.13 18:03 |
| Оценка: |
Здравствуйте, Blazkowicz, Вы писали:
B>Здравствуйте, ZeroLatency, Вы писали:
ZL>>И как вы собираетесь создавать инстансы конкретных объектов, на каких программных основаниях? Нужно же выбор делать при конструировании.
B>Из короткого описания, достаточно сложно понять какой изначально предполагается код и какую задачу он реализует. Но посыл верный — замена ветвления полиморфизмом.
для того чтобы был полиморфизм нужно ветвление для инициализации конкретным классом. И вообще вопрос был не об этом
| От: | Аноним |
| Дата: | 25.12.13 03:26 |
| Оценка: |
Здравствуйте, andyag, Вы писали:
A>Здравствуйте, Hard_Club, Вы писали:
H_C>>Если есть конструирование объекта с помощью вложенного switch с помощью анализа некоторых признаков, то каким паттерном проектирования / примитивом это можно перекрыть?
A>Сильно зависит от того, в чём именно вы видите проблему. В switch нет ничего плохого, даже если он длинный. Главное, чтобы читалось. Если у вас желание наархитектить, можно подумать вот в такую сторону:
A>
A>Дисклеймер: я бы хорошо взвесил плюсы-минусы, прежде чем пилить такой дизайн
А что в findFactoryBasedOnGivenContext не будет ветвления или выборки по какому-нибудь хэш-коду на уровне java алгоритмов?
| От: | andyag |
| Дата: | 25.12.13 04:50 |
| Оценка: |
Здравствуйте, Аноним, Вы писали:
А>Здравствуйте, andyag, Вы писали:
A>>Здравствуйте, Hard_Club, Вы писали:
H_C>>>Если есть конструирование объекта с помощью вложенного switch с помощью анализа некоторых признаков, то каким паттерном проектирования / примитивом это можно перекрыть?
A>>Сильно зависит от того, в чём именно вы видите проблему. В switch нет ничего плохого, даже если он длинный. Главное, чтобы читалось. Если у вас желание наархитектить, можно подумать вот в такую сторону:
А>А что в findFactoryBasedOnGivenContext не будет ветвления или выборки по какому-нибудь хэш-коду на уровне java алгоритмов?
Ветвление есть всегда и у всех, но в зависимости от подхода к реализации, оно либо будет торчать развесистым деревом из программы и своими ветками выкалывать глаза всем читающим, либо будет настолько неявным и незаметным, что о нём даже могут и забыть. Задача ТС вообще сводится к тому, чтобы выполнить N сравнений и в одном из них получить true. Есть примерно 2 способа выполнить N сравнений — написать N сравнений руками или выполнить 1 сравнение N раз. Первый способ грозит написанием одной длинной функции, которая будет становиться ещё длиннее при развитии программы. Второй способ грозит обвинениями в ООП головного мозга, хотя лишён недостатков первого