Перевод публикуется с сокращениями, автор оригинальной статьи Nicklas Millard.

Cкорее всего, if-else
и switch
– ваш обычный
подход к ветвлению кода, но в нем нет необходимости. Вы можете полностью
исключить ключевое слово else
из своего словаря по программированию.
Некоторые матерые кодеры говорят, что if-else
– полиморфизм новичков.
Что плохого в традиционном ветвлении?
Много чего. Традиционное ветвление быстро разрастается. Вам придется изменять существующий код каждый раз при добавлении новой функции. Это нарушает принцип Open-Closed. Функции должны быть реализованы с помощью новых классов.
Какие есть альтернативы?
Существует масса альтернатив, но мы рассмотрим три типовых подхода, которые часто применяются при удалении традиционного ветвления из кода:
- концепции моделей с классами;
- использование полиморфного выполнения при работе с изменяющимися состояниями объектов;
- инкапсуляция стратегии в отдельные классы.
Эти 3 подхода легко справятся с большинством повседневных ситуаций, с которыми вы можете столкнуться.
Все методы имеют общие черты:
- Новая функциональность реализуется с помощью новых классов. Добавление кода, а не его изменение, обычно является более безопасным вариантом. Каждый раз, когда изменяется уже используемый код, вы подвергаетесь огромному риску все сломать.
- Проще тестировать специализированные классы – это огромное преимущество. Простые классы и методы легче рассматривать – легче понять множество небольших и сплоченных классов, чем несколько монолитных.
- Управление концепциями на низком уровне лучше, чем расширение обязанностей одного класса.
Моделирование концепций с помощью простых классов
Допустим, у нас есть
класс User
и в нем имя пользователя. Оно является строкой и имеет два условия:
не может быть нулем или пустой строкой и не может превышать 50 символов.

Можно с уверенностью предположить, что нам понадобятся имена пользователей в других частях приложения. Каждый раз, когда мы получаем имя, придется выполнять одни и те же проверки, которые будут разбросаны по всему проекту.
Если что-то изменится и возникнет необходимость в валидации специальных символов, таких как «æøå», придется найти каждое место, где получено имя пользователя, и добавить новую проверку.
Гораздо лучшим подходом является перенос концепции имени пользователя и создание небольшого специализированного объекта, как показано ниже.

Этот фрагмент кода, несомненно, чище. Теперь каждый раз, когда потребуется обновление, его нужно будет делать только в одном месте.
Изменение реализации метода объекта в зависимости от его состояния
Иногда необходимо, чтобы объект вел себя по-разному в зависимости от его внутреннего состояния. Типичный ленивый способ реализации этого – традиционное ветвление, как в приведенном ниже примере.

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

Видите, какая плоская конструкция?
Теперь каждая ветвь инкапсулирована
в собственный класс, а класс account
делегирует ответственность
специализированному AccountState
.

Удвойте количество кода и повысьте читабельность.
У нас есть базовый класс,
от которого наследуется каждый объект состояния. Если мы получим новый запрос на добавление состояния RequiresValidation
или что-то еще, будет легко реализовать эту функцию, не касаясь существующих классов.
Внимательный читатель заметил, что нет никакого перехода в состояние. Очевидно, что классы не полностью реализованы, т. к. переход из одного состояния в другое выходит за рамки статьи.
Рефакторинг ветвлений на отдельные классы
Наиболее часто используемый способ устранения условных ветвлений – объекты стратегии.
Допустим, мы хотим
преобразовать любой тип в формат CSV и указать, как преобразуется
каждое свойство типа, но тип не должен определять это сам. [Csv Info]
– это атрибут, который по сути является метаинформацией о типе. Затем эта метаинформация считывается с помощью крошечной рефлексии внутри метода ToCsv()
.

Ниже приведен фрагмент класса
CsvInfoAttribute
. Этот код не является полным мусором, но он не очень
расширяемый и не очень гибкий. Каждый раз, когда нужно будет добавить новый параметр преобразования, придется добавлять и дополнительный enum, а затем реализовывать преобразование для него в методе Format()
. Это означает, что вам нужно изменить существующий код.
Предположим, что класс живет в какой-то общей библиотеке, от которой зависит много проектов в вашей организации. Если одной команде нужен новый вариант преобразования, ей придется обратиться к сопровождающим проекта, чтобы они добавили функциональность, создали новую версию и опубликовали ее.

Мы можем сделать это,
разделив каждую ветку switch/case
на специализированные классы, что делает enum
ненужным.
Каждая стратегия должна реализовывать
общий интерфейс, а CsvInfoAttribute
больше не должен иметь собственного метода Format()
.
Вместо этого он делегирует ответственность за форматирование специализированным
объектам.
Рассмотрим приведенный ниже код:

Чтобы это сработало, необходимо
определить некоторые ограничения: любой форматер должен реализовать
IValueFormatter
и иметь конструктор по умолчанию.
Представьте, как легко сейчас протестировать и исправить ошибки. Вы всегда будете точно знать, что проверять и где искать. Больше никаких следов безумной switch-логики.
Заключение
К сожалению, традиционное ветвление используется очень широко. Многие разработчики придерживаются своего испытанного и верного способа ветвления кода, не осознавая, что обслуживание и добавление функциональности превратилось в кошмар.
Если вы действительно хотите стать сильным разработчиком, начните искать способы устранения традиционного ветвления или используйте описанные в статье.
Дополнительные материалы:
Комментарии
зачем так все усложнять, читабильность ухудшается в разы, тут даже с функциями надо быть осторожнее ибо если увлечься можно ветку так разбить и распихать по разным местам исходника что потом концов не найдешь))
Если можно сделать проще не городи всяку хрень - завещал старина Оккам.
Может я просто не понимаю, но не могу найти преимущество вынесения username в отдельный класс. И так и так валидация проходит при создании экземпляра класса User. Просто стало как-то все запутанно (через одно место).
Судя по всему автор никогда не имел дело с проектами содержащими 500+ классов отвечающих за различные сервисы, логику и представление и не работал в распределённой команде. 500 классов при таком подходе превратятся в 2000+ классов и для того чтобы человеку вне контеста понять код придётся "разматывать" все эти зависим ости и наследования. Особо одарённые еще и интерфейсы к этому хозяйству прикручивают. А ещё часто приходится менять логику или вводить/убирать параметры. И, если используя if можно было обойтись изменением пары тройки строк, в такой конструкции придётся менять иерархию.
И снова спрошу - как-то же распределенные разработчики разбираются в 100500 классах в случае с котлином, джавой и андроидом? Ничего им не приходится разматывать и создавать 2000 классов. Если человек вне контекста, то на такую должность он подходит т.к. для работы вне контекста нужен опыт работы с внеКонтекстом, опыт работы в распределенных командах и просто опыт работы, а не после 4го курса (или не дай бог без универа вообще)... Код должен быть нормальным, должна быть документация (если это уважающий себя проект, а не 500 классов мусора) и его нужно в дальнейшем адекватно поддерживать, а не говнокодить, кто во что горазд.
Господа! О чем вы все здесь вообще?!! ООП появилось не вчера и все ООП-ные ЯП юзаются не один десяток лет. Как вы считаете, если бы с ООП было что-то не так, как тут считают, "мировые правящие IT-головы" не решили бы это все хозяйство упразднить, переживая о 2000 классах или о неокрепшей психике молодых-зеленых??!!!!! Придите в себя! Если вы пришли в ОО-язык из мультипарадигмального или вообще из процедурного, то не болтайте чепухой, а соблюдайте правила этого языка, писанные 10-летиями и будет вам умиротворение и покой.
Документация ?? спасибо посмеялся... Актуальной можно поддерживать только ограниченную доку которая даёт общее представление об архитектуре и интерфейсах больших модулей. Иначе, она только будет запутывать. Принцип KISS наше всё, не усложняйте без нужды, иначе привет отладчик для каждого нового разработчика в команду. И причём здесь ООП и IF? Разве они контрят друг-друга? Да, если портянка ifov не помещается в экран, то что-то надо менять и переходить на стейты. Я опечален, что вы восприняли мои слова так буквально
До появления ООП появились идеи Дейкстры о структурном программировании. If-else там - одна из главных управляющих структур. Даже в макроассемблерах применяется, где никакого ООП нет.
Не могу согласиться с подобным подходом. Главное в программе - это её наглядность. Добавился новый блок - добавляем условие if-else, ставим комментарий в начале программы - и вс видно с первого взгляда. Здесь объектный подход проигрывает со сравнением со структурным. Не рекомендую следовать советам данной статьи.
Посадите обратно в клетку автора и не выпускайте. ООП зло во плоти и тут это просто на лицо. ООП это не только путаница в коде, но и жутчайшие тормоза, а также горы ненужного кода. Лучше использовать if-else/switch чем гору ООП.
Автор явно кодит только крестики нолики. Если кодить что то больше, то быстро тонешь в классах, код становится не читаем, количество кода растет в прогрессии, дебажить становится сложней. ООП это зло! Не пользуйтесь одним ООП.
Если считать три столпа: инкапсуляция, наследование, полиморфизм злом, то можно смело обратиться в Google, Apple, Windows и сказать, чтобы не юзали C++, JS, Swift, Java, Objective C, Go и гору прочих ЯП, используемых и создаваемых. Пишем все в процедурном стиле и плодим простыни из неподдерживаемого кода! Если необходимо написать код для вывода "Hello world!", то, конечно нафиг ООП, а если вы собрались наваять приложение в телефон - попробуйте это сделать на процедурке...
Еще только начиная программировать жутко раздражали эти if -else конструкции, которые быстро становились неуправляемыми при росте ветвлений. ООП я тогда совсем не знал, но понимал, что должен быть способ лучше. На данный момент пользуюсь исключительно ООП подходом для реализации if else . Да, кода больше, но это мелочь по сравнению с тем, насколько это более эффективно и элегантно
Кода больше - больше тормозов. Да и наглядней if-else ничего нет.
"Использование if-else и switch влечет за собой отказ от многих объектно-ориентированных практик"
а если я пишу на фп-языке(лисп), на языке без направленности (си) или на языке с неполной или неявной реализацией ооп (go)?
Тогда забейте на эту дурную статью и уменьшайте вложенность условий другими способами (не вижу, кстати, ничего плохого в одноуровневом условии без else)
Просто этих Java-проггереров хлебом не корми лишь бы написать тысячу-другую лишних классов и интерфейсов, чтобы код стал совсем не читаемым за толстым слоем абстракции и перенаследования.
"Добавление кода, а не его изменение, обычно является более безопасным вариантом. Каждый раз, когда изменяется уже используемый код, вы подвергаетесь огромному риску все сломать."
То-то за последние 25 лет объём кода в софте вырос в 1000 раз. В Win 95 было примерно 20 МБ, а в Win 7 - уже примерно 20 ГБ. И тем не менее, код всё равно "ломается".
Не нужно забывать, что каждый фреймворк и каждая фича весит в 1000 раз больше, чем в 95й винде. В Win7 напихано в 1000 раз больше всякого-ненужного, ну, и говнокод, бесконтрольно писанный индусами, а также заплатка на заплатке и патчем погоняет - поэтому всё "ломается". Windows будет всегда ломаться потому, что это Windows... В статье речь не о Windows (как минимум потому, что неизвестно, какой там код), а об общем подходе написания правильного кода, типс&трикс, так сказать.
Сильный разработчик будет код писать, а не думать, как бы ему лишний раз от if-else/switch избавиться.
А потом другой сильный разработчик, он же тимлид, заставит первого рефакторить код, дабы оптимизировать эти горы условий... Сразу нужно избегать ветвлений (на пример, как в статье) и строить свой проект в таком ключе.
Да ну не, сильный разработчик заметит, что ветвлений слишком много и сразу задизайнит всё так, чтобы и читалось хорошо, и масштабировалось нормально. Тут опыт порешает, а не стремление "избавляться от if-else/switch ради ООП". Да и, думаю, такие вещи заранее обсуждаются и чаще всего учитываются в общей архитектуре. Естественно, не без исключений.
Горы условий в переключателе не подлежат оптимизации. Это - как стрелки на железной дороге. Сколько надо, столько и ветвятся.