Около 10 лет назад я столкнулся с анти-if кампанией и счел ее абсурдной концепцией. Как вы можете создать полезную программу без использования оператора if? Абсурдно.
Но потом это заставляет задуматься. Помните тот вложенный код, который вам пришлось разбирать на прошлой неделе? Это было ужасно, верно? Если бы только был способ сделать его проще.
На сайте кампании " Анти-if", к сожалению, мало практических советов. Данная статья призвана исправить это положение с помощью подборки паттернов, которые вы можете взять на вооружение, когда возникнет такая необходимость. Но сначала давайте рассмотрим проблему, которую создают операторы if.
Проблемы операторов if
Первая проблема с операторами if заключается в том, что они часто упрощают модификацию кода не в лучшую сторону. Давайте начнем с появления нового оператора if:
public void theProblem(boolean someCondition) {
// SharedState
if(someCondition) {
// CodeBlockA
} else {
// CodeBlockB
}
}
На данный момент все не так уж плохо, но мы уже столкнулись с некоторыми проблемами. Когда я читаю этот код, мне приходится проверять, как CodeBlockA и CodeBlockB изменяют одно и то же SharedState. Сначала это можно легко прочитать, но по мере увеличения CodeBlock'ов и возникновения сложностей с связанностью (coupling) это может стать трудной задачей.
Часто можно увидеть, как вышеупомянутые блоки CodeBlocks злоупотребляют дальнейшими вложенными операторами if и локальными возвратами. Это затрудняет понимание бизнес-логики через систему маршрутизации.
Вторая проблема с операторами if заключается в том, что они дублируются. Это означает, что концепция домена отсутствует. Можно легко увеличить связанность, объединяя то, что не нужно. В результате будет труднее читать и изменять код.
Третья проблема с операторами if заключается в том, что вам приходится моделировать процесс выполнения в своей голове. Вы должны стать мини-компьютером. Это отнимает у вас умственную энергию, которую лучше потратить на решение проблемы, а не на то, как переплетаются между собой отдельные ветви кода.
Я хочу перейти к тому, чтобы рассказать вам, какие паттерны мы можем применить вместо этого, но сначала несколько слов для предупреждения.
Умеренность во всем, особенно в умеренности
Операторы If обычно усложняют код. Но мы не хотим полностью запрещать их. Я видел довольно отвратительный код, созданный с целью удалить все следы операторов if. Мы хотим избежать попадания в эту ловушку.
Для каждого паттерна, о котором мы будем читать, я дам вам допустимое значение, когда его можно использовать.
Одиночный оператор if, который больше нигде не дублируется, — это, скорее всего, нормально. А вот когда у вас есть дублирующиеся операторы if, вам следует обратить внимание на свое чутье.
На внешней стороне вашей кодовой базы, где вы взаимодействуете с внешней средой, вы наверняка захотите проверить входящие ответы и соответствующим образом изменить свое поведение. Но внутри наших собственных кодовых баз, я думаю, у нас есть прекрасная возможность использовать простые, содержательные и более мощные альтернативы.
Паттерн 1: Boolean Параметры
Контекст: У вас есть метод, который принимает булево (Boolean) значение, изменяющее его поведение.
public void example() {
FileUtils.createFile("name.txt", "file contents", false);
FileUtils.createFile("name_temp.txt", "file contents", true);
}
public class FileUtils {
public static void createFile(String name, String contents, boolean temporary) {
if(temporary) {
// save temp file
} else {
// save permanent file
}
}
}
Проблема: Каждый раз, когда вы видите это, у вас на самом деле два метода, объединенные в один. Этот булев позволяет задать имя концепции в вашем коде.
Допустимые варианты: Обычно, когда вы видите этот контекст, вы можете определить во время компиляции, по какому пути пойдет код. Если это так, то всегда используйте этот паттерн.
Решение: Разделите метод на два новых метода. Вуаля, if исчезло.
public void example() {
FileUtils.createFile("name.txt", "file contents");
FileUtils.createTemporaryFile("name_temp.txt", "file contents");
}
public class FileUtils {
public static void createFile(String name, String contents) {
// save permanent file
}
public static void createTemporaryFile(String name, String contents) {
// save temp file
}
}
Паттерн 2: Вместо switch полиморфизм
Контекст: Вы переходите на другой тип.
public class Bird {
private enum Species {
EUROPEAN, AFRICAN, NORWEGIAN_BLUE;
}
private boolean isNailed;
private Species type;
public double getSpeed() {
switch (type) {
case EUROPEAN:
return getBaseSpeed();
case AFRICAN:
return getBaseSpeed() - getLoadFactor();
case NORWEGIAN_BLUE:
return isNailed ? 0 : getBaseSpeed();
default:
return 0;
}
}
private double getLoadFactor() {
return 3;
}
private double getBaseSpeed() {
return 10;
}
}
Проблема: Когда мы добавляем новый тип, нам нужно не забыть обновить оператор switch. Кроме того, в этом классе Bird страдает связность, поскольку добавляется несколько концепций различных Bird классов.
Допустимые варианты: Одиночный switch — это нормально. Когда их несколько, могут возникнуть ошибки, поскольку человек, добавляющий новый тип, может забыть провести обновления по всем switch, которые доступны для этого скрытого типа. В блоге 8thlight есть отличная статья по этому вопросу.
Решение: Используйте полиморфизм. Каждый, кто вводит новый тип, не может забыть про связанное с ним поведение.
public abstract class Bird {
public abstract double getSpeed();
protected double getLoadFactor() {
return 3;
}
protected double getBaseSpeed() {
return 10;
}
}
public class EuropeanBird extends Bird {
public double getSpeed() {
return getBaseSpeed();
}
}
public class AfricanBird extends Bird {
public double getSpeed() {
return getBaseSpeed() - getLoadFactor();
}
}
public class NorwegianBird extends Bird {
private boolean isNailed;
public double getSpeed() {
return isNailed ? 0 : getBaseSpeed();
}
}
Примечание: В этом примере для краткости включен только один метод, однако более убедительно выглядит вариант, когда несколько применяется несколько switch.
Паттерн 3: NullObject/Optional вместо нулевых значений
Контекст: Посторонний человек, которого просят понять основную цель вашей кодовой базы, отвечает: "нужно проверять, равны ли значения null".
public void example() {
sumOf(null);
}
private int sumOf(List<Integer> numbers) {
if(numbers == null) {
return 0;
}
return numbers.stream().mapToInt(i -> i).sum();
}
Проблема: Ваши методы должны проверять, передаются ли им ненулевые значения.
Допустимость: Необходимо обеспечивать защиту во внешних частях вашей кодовой базы, но защита внутри вашей кодовой базы, вероятно, означает, что код, который вы пишете, является "оскорбительным". Не пишите "оскорбительный код".
Решение: Используйте тип NullObject или Optional вместо того, чтобы проверять, передаются ли нулевые значения. Пустой набор данных — отличная альтернатива.
public void example() {
sumOf(new ArrayList<>());
}
private int sumOf(List<Integer> numbers) {
return numbers.stream().mapToInt(i -> i).sum();
}
Паттерн 4: inline заявления в выражения
Контекст: У вас есть дерево оператора if, которое вычисляет булево выражение.
public boolean horrible(boolean foo, boolean bar, boolean baz) {
if (foo) {
if (bar) {
return true;
}
}
if (baz) {
return true;
} else {
return false;
}
}
Проблема: Этот код заставляет вас использовать свой мозг, чтобы смоделировать, как компьютер будет проходить через ваш метод.
Допустимые варианты: Очень мало. Такой код легче читать в одну строку. Или разбить на разные части.
Решение: Упростите операторы if до одного выражения.
public boolean horrible(boolean foo, boolean bar, boolean baz) {
return foo && bar || baz;
}
Паттерн 5: Предоставьте стратегию решения проблем
Контекст: Вы вызываете какой-то другой код, но не уверены, что удастся вам успешной пройти этот путь.
public class Repository {
public String getRecord(int id) {
return null; // cannot find the record
}
}
public class Finder {
public String displayRecord(Repository repository) {
String record = repository.getRecord(123);
if(record == null) {
return "Not found";
} else {
return record;
}
}
}
Проблема: Подобные операторы if множатся каждый раз, когда вы имеете дело с одним и тем же объектом или структурой данных. Они имеют скрытую связь, где "null" что-то означает. Другие объекты могут возвращать другие "магические значения", которые означают отсутствие результата.
Допустимые варианты: Лучше поместить этот оператор if в одно место, чтобы он не дублировался, и устранить связь с "магическим значением" пустого объекта.
Решение: Предоставьте вызываемому коду стратегию решения проблем. Hash#fetch в Ruby — хороший пример, того, как справился Java. Этот паттерн можно развивать и дальше, чтобы устранить исключения.
private class Repository {
public String getRecord(int id, String defaultValue) {
String result = Db.getRecord(id);
if (result != null) {
return result;
}
return defaultValue;
}
}
public class Finder {
public String displayRecord(Repository repository) {
return repository.getRecord(123, "Not found");
}
}
Удачной работы
Надеюсь, вы сможете использовать некоторые из этих паттернов в коде, над которым вы сейчас работаете. Я нахожу их полезными при рефакторинге кода, чтобы лучше понять его.
Помните, что операторы if — это далеко не все зло. В современных языках у нас есть богатый набор возможностей, которыми мы должны воспользоваться.
Полезные статьи
Материал подготовлен в рамках курса «Архитектура и шаблоны проектирования». Если вам интересно узнать подробнее о формате обучения и программе, познакомиться с преподавателем курса — приглашаем на день открытых дверей онлайн. Регистрация здесь.
Комментарии (42)
Anamelash
04.08.2021 15:27+1В последнем примере вместо проверки на
null
будет проверка наdefaultValue
Чем это лучше?
Sklott
04.08.2021 16:58Там смысл немного в другом. На defaultValue как раз проверять ничего не надо, наоборот вы его просто получаете и просто используете. Конечно это неприминимо если у вас нет defaultValue которе бы вы использовали если бы не нашли record.
g6uru
04.08.2021 17:04Разделить на два класса?
TmpFile.new("file_name.txt").create RegularFile.new("file_name.txt").create
Myclass
04.08.2021 17:05+1Решение: Разделите метод на два новых метода. Вуаля, if исчезло.
в параметрах иногда true/false незначительно изменяют кое-какие например флаги. Т.е. убрав if, мне придётся дублировать полностью намного сложнее код.
Какое-то искуственное придумывание проблемы на пустом месте.Myclass
05.08.2021 00:43+3Дорогой автор, даже, если вам не нравятся другие мнения, всё равно стоит к ним прислушаться. Объясню почему.
В первом случае вы привели тривиальный случай, который 1:1 повторяет if оператор - те. вы заменили этот if оператор на две функции. А что делать, если внутри функции три и более if оператора? Заменять их на n! количество функций? У меня названий не хватит, если я вместо 4-х if(ов) 24 функции напишу, каждая из которых отдельную ситуацию воспроизведёт.
Это первое. Второе. Как бы вы не упрощали, вам всё равно выше по хирархии вызовов надо будет де-то принимать решение, какую из реализаций вызывать. Поэтому видение в if чего-то, чем надо пренебрегать - есть нежелание согласится с чем-то обычным. Таким-же как, если из одного города до другого надо доехать, то это потребует времени. Да, вместо велосипеда одно взять быструю машину но всё равно- какое-то время это потребуется. Очевидно.
В последнем примере опять-же тривиальный случай с типом string. А что делать, если функция возвращает другие типы данных? Например тот-же самый созданный файл, который не смог быть открытым, тк. места на плате нет или нет прав на создание. Что вы там за default value возьмёте?
Myclass
05.08.2021 00:03+1Не понимаю. весь ассемблер набит функциями по переходу в зависимости от значения или состояния в аккумуляторе. Под это и создаётся оператор if, ну те. он потом с лёгкостью переводится в набор ассемблер команд.
sshmakov
05.08.2021 08:41Интересно было бы увидеть способы избавления от if в алгоритмах обработки изображений, ведь для GPU каждый if - это боль.
tarekd
05.08.2021 15:18+1Современные архитектуры от Nvidia получше переносят это. Иногда замена ветвлений на арифметические выражение позволяет добиться улучшения производительности. Но код становится менее читаем и требует множества комментариев. Пример:
if (foo >= 0) { bar = 10; } else if (foo < 0) { bar = 15; }
можно свести к
bar = 10 + (foo < 0) * 5;
Много интересного можно найти в https://www.amazon.com/Hackers-Delight-2nd-Henry-Warren/dp/0321842685.
Во времена когда я работал с GPU, вычисления с double были болью, приходилось извращаться трюками типа https://en.wikipedia.org/wiki/Kahan_summation_algorithm
Myclass
05.08.2021 09:21И ещё один момент но его задам с осторожностью. Могу ошибиться , тк. уже более 20 лет не живу в среде русского языка, но мне кажется, что ваше выражение
Помните, что операторы if — это далеко не все зло.
...должно подниматься так, что if операторы - уже зло. Но они не одни есть зло, есть и другие злые вещи. А я вам скажу своё мнение, что сам оператор не есть зло. Злом может быть необдуманное его использование, которое сильно усложняет читаемость кода, а также возведение этого оператора в своего рода ранг табу и после - "борьба" против его использования. Так называемая охота на ведьм.
mvv-rus
05.08.2021 17:36Там, если писать по-русски, должно было бы быть:
«Помните, что далеко не все операторы if — это зло.»
Но не только лишь все русскоязычные копирайтеры умеют писать по-русски.
ldss
09.08.2021 18:08Хорошая статья
Но если человек работает в б-м современной IDE, ему линтер все расскажет:) Другое дело, что многие это просто игнорируют, и это, собссно, одна из частей проблемы
пс Смешно смотреть, как народ не догоняет про абстрактность примеров и не может их экстраполировать:)
Myclass
10.08.2021 19:46Смешно смотреть, как народ не догоняет про абстрактность примеров и не может их экстраполировать:)
Ну все не могут быть такими умными как вы #ирония.
А серьёзно- ведь как раз таки и была дискуссия о интерполяции этих примеров. И именно 'проблема' с этим. Но об этом уже были пару комментариев выше.
ldss
13.08.2021 16:03при чем тут я
умение экстраполировать абстракции - часть умения в анализ
странно не иметь его, будучи разработчиком
LuggerMan
Первый же совет — ржач полный. Иф в методе убрал, а вызываю как? Наоборот, имхо лучше один метод, отвечающий за запись, с параметром (если он один — вообще прекрасно)
boblgum
тот кто вызывает, должен знать, что он хочет.
если вы аргументируете тем, что последний, в данном случае, параметр может быть неизвестен вызывающему, то флаг вам в руки и удачи )
AjnaGame
ну так а как без if он там выберет что хочет?
mamento
Зависит от контекста, если это решает пользователь приложения создавать временный или постоянный, то конечно без if мы не сможем скорее всего выбрать, не рассматривая совсем экзотические случаи.
Ну а вдругих случаях, нам должно быть известно уже во время написания программы хотим ли бы в конкретном месте временный файл или постоянный.
ldss
плюс, читающему код будет понятнее, а что тут, собственно, происходит - логика описана именами функций
LuggerMan
Увидел ответы — защищусь, чтоль! Помнить один метод и глянуть быстренько параметр мне будет куда приятней, чем запоминать по десятку названий на каждый чих. Доведем до абсурда — вообще выкинем любые параметры и под каждый, опять же, чих, напишем по функции! Переиспользование кода? Миф, что Вы! Порка очередного «умника» за пару десятков ненужных методов со сложнопереводимыми названиями (плюсом идет уровень B1 по инглишу у пишущего)? Азаза, за что его, лучше разбираться в этой мусорке всей командой, лопатя бесконечные страницы Doxy…
ldss
а зачем их запоминать? В этом же и суть - читаешь код, видишь функцию с именем, примерно подходящим под искомое решение, лезешь внутрь и правишь/дебажишь/смотришь только там, игнорируя остальные.
Что до сложнопереводимых имен - вот как раз когда функция короткая, ей можно придумать короткое же имя.
ну и обратная сторона - вот у нас есть функция на 700 строк, которая считает емкость wifi сети. Какая там в ней логика, фиг поймешь, особенно через год. Натурально надо день в отладке провести просто чтоб понять, что происходит
LuggerMan
Правочки подьедут в логику - кровавыми слезами умоешься, правя везде. Захочешь фичу добавить - читай весь файл/юнит, чтобы чем-то воспользоваться (причем половина будет копипаст). Каша ради несущественной оптимизации
"Еще и комментировать код не надо, кек. Зачем, я же названия раздаю как боженька!" <- и от Д`Артаньяна идет явный запашок
ldss
эээ.. в этом же и суть коротких однозначных функций - не надо править везде, надо править толь-ко в одном месте - single point of truth, dry и прочее
Комментирование кода часто бессмысленное дело, т.к. код меняется, а каменты - нет
LuggerMan
Вот как раз-таки Dry, который Вы приводите в пример, рекомендует не перепечатывать функцию два раза при незначительном изменении, а добавить флаг (bool temporary)
ldss
понятно, что нужен баланс. Но чаще всего-то, этот флаг добавляется и развивается во много флагов
LuggerMan
Тут скорее функция или метод как действие. Выделение малого действия, описание возможных его вариантов. Раньше видал байт как набор флагов, из которых дергали побитовым сложением.Однозначные же функции переусложняют чтение и многократно перепечатывают одинаковые куски текста. (ну если там вообще что-то похожее по смыслу, что имело смысл быть запиханным в одну функцию => надо править только в одном месте - single point of truth, ага)
Вот откуда еще, кстати, взялась мысль, что сложнопереводимая = короткая? Наоборот, с автоподстановкой можно не экономить!
Насчет комментариев - это смотря как готовить. К примеру, если оставлять комментарием развернутое описание функции, параметров и флагов в заголовке, а затем откомментировать только нетривиальные места - они не склонны к изменению (попробуй повтори). Тогда еще и документуха в один прогон соберется.
Решительно не понима. Ваших доводов.
LuggerMan
Умножением, пардон.
ldss
Это прямо противоречит тому, о чем я писал:) Если есть одинаковые куски кода, они как раз и сводятся в _одну функцию
э.. я такого не говорил
я наоборот говорил, что простая логика внутри функции приведет к простому же имени данной функции
это в теории
на практике часто код меняется, а каменты нет
LuggerMan
Еще кекну над "днем на функцию на 700 строк" - ну реально, если лид такой код пропустил - его надо увольнять. Не подроблено на "действия" (функции), ничего не описано нигде и никем - "НО ОНО ЖЕ РАБОТАЕТ", да? Гугл и прочая свои кодгайды для лошков делает, видать
qark
Ты уж определись над чем "кекаешь": короткие функции или "700 строк и 70 флагов".
LuggerMan
Золотой середины нету, да?
qark
По твоей логике нет, "нету".
LuggerMan
Поразвернутей, пожалуйста. Для меня пока что все в точности наоборот, и Ваш тон, мягко говоря, непонятен. Может быть я Вам что-то сделал личное или неприятное, и Вы не можете унять жжение?
Dim0v
TheShock
Было:
Стало:
gecube
Ага, только вместо одной функции с if, мы теперь имеем две функции, скорее всего с реализацией по типу copy-paste, что будет плодить ошибки. А если там есть общие части, которые мы вынесем, например, в commonCreateFile(), то у нас вместо 1 функции уже будет три. Ну, что, гениально. Как создать себе работу на ровном месте…
qark
Это называется рефакторинг.
gecube
Главное, чтобы он был осмысленен. Это как ускорять работу функции в том месте, которое не "тормозит". Рефакторинг ради рефакторинга - зло.
qark
Волга впадает в Каспийское море. Лошади кушают овес и сено.
TheShock
Вы видели пример? Это для тех функций, у которох на верхнем уровне иф. Вы видите, что в этой функции просто два независимых блока кода без общей логики? Как вы вообще программистом стали с таким низким уровнем внимательности?
Но даже "ужасный" пример, который вы привели - это хорошо, а большая функция с кучей разных ответственностей - это плохо. Разнос на два метода может помочь проанализировать функцию и выделить дублирующийся код в отдельный метод с понятным названием.
Но я слышал, что некоторым людям момент, когда надо подумать минутку - это слишком сложно и они хотят просто кодить.
gecube
Вы не можете ни подтвердить это, ни отрицать. Автор скромно схлопнул реальный код в комментарий. Блок "save temp file" может быть точно таким же как "save permanent file" на 90-99%. Ну, вот - написали как написали.
разделять ответственность тоже нужно разумно.
может да, а может нет. Точнее - не обязательно разносить на два метода, чтобы можно было выделить дублирующийся код.
троллинг не засчитан.
такое тоже бывает.