Около 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)


  1. LuggerMan
    04.08.2021 15:09
    -1

    Первый же совет — ржач полный. Иф в методе убрал, а вызываю как? Наоборот, имхо лучше один метод, отвечающий за запись, с параметром (если он один — вообще прекрасно)


    1. boblgum
      04.08.2021 15:52
      +1

      тот кто вызывает, должен знать, что он хочет.

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


      1. AjnaGame
        04.08.2021 17:09
        +2

        ну так а как без if он там выберет что хочет?


        1. mamento
          04.08.2021 18:12
          +1

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


          1. ldss
            09.08.2021 18:02

            плюс, читающему код будет понятнее, а что тут, собственно, происходит - логика описана именами функций


            1. LuggerMan
              27.08.2021 17:26

              Увидел ответы — защищусь, чтоль! Помнить один метод и глянуть быстренько параметр мне будет куда приятней, чем запоминать по десятку названий на каждый чих. Доведем до абсурда — вообще выкинем любые параметры и под каждый, опять же, чих, напишем по функции! Переиспользование кода? Миф, что Вы! Порка очередного «умника» за пару десятков ненужных методов со сложнопереводимыми названиями (плюсом идет уровень B1 по инглишу у пишущего)? Азаза, за что его, лучше разбираться в этой мусорке всей командой, лопатя бесконечные страницы Doxy…


              1. ldss
                27.08.2021 19:07

                а зачем их запоминать? В этом же и суть - читаешь код, видишь функцию с именем, примерно подходящим под искомое решение, лезешь внутрь и правишь/дебажишь/смотришь только там, игнорируя остальные.

                Что до сложнопереводимых имен - вот как раз когда функция короткая, ей можно придумать короткое же имя.

                ну и обратная сторона - вот у нас есть функция на 700 строк, которая считает емкость wifi сети. Какая там в ней логика, фиг поймешь, особенно через год. Натурально надо день в отладке провести просто чтоб понять, что происходит


                1. LuggerMan
                  28.08.2021 23:36

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

                  "Еще и комментировать код не надо, кек. Зачем, я же названия раздаю как боженька!" <- и от Д`Артаньяна идет явный запашок


                  1. ldss
                    30.08.2021 15:34

                    эээ.. в этом же и суть коротких однозначных функций - не надо править везде, надо править толь-ко в одном месте - single point of truth, dry и прочее

                    Комментирование кода часто бессмысленное дело, т.к. код меняется, а каменты - нет


                    1. LuggerMan
                      31.08.2021 12:24

                      Вот как раз-таки Dry, который Вы приводите в пример, рекомендует не перепечатывать функцию два раза при незначительном изменении, а добавить флаг (bool temporary)


                      1. ldss
                        01.09.2021 17:37

                        понятно, что нужен баланс. Но чаще всего-то, этот флаг добавляется и развивается во много флагов


                      1. LuggerMan
                        01.09.2021 22:01

                        Тут скорее функция или метод как действие. Выделение малого действия, описание возможных его вариантов. Раньше видал байт как набор флагов, из которых дергали побитовым сложением.Однозначные же функции переусложняют чтение и многократно перепечатывают одинаковые куски текста. (ну если там вообще что-то похожее по смыслу, что имело смысл быть запиханным в одну функцию => надо править только в одном месте - single point of truth, ага)

                        Вот откуда еще, кстати, взялась мысль, что сложнопереводимая = короткая? Наоборот, с автоподстановкой можно не экономить!

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

                        Решительно не понима. Ваших доводов.


                      1. LuggerMan
                        01.09.2021 22:06

                        Умножением, пардон.


                      1. ldss
                        07.09.2021 18:34

                        Однозначные же функции переусложняют чтение и многократно перепечатывают одинаковые куски текста.

                        Это прямо противоречит тому, о чем я писал:) Если есть одинаковые куски кода, они как раз и сводятся в _одну функцию

                        Вот откуда еще, кстати, взялась мысль, что сложнопереводимая = короткая? Наоборот, с автоподстановкой можно не экономить!

                        э.. я такого не говорил

                        я наоборот говорил, что простая логика внутри функции приведет к простому же имени данной функции

                        К примеру, если оставлять комментарием развернутое описание функции, параметров и флагов в заголовке, а затем откомментировать только нетривиальные места - они не склонны к изменению (попробуй повтори)

                        это в теории

                        на практике часто код меняется, а каменты нет


                1. LuggerMan
                  28.08.2021 23:48

                  Еще кекну над "днем на функцию на 700 строк" - ну реально, если лид такой код пропустил - его надо увольнять. Не подроблено на "действия" (функции), ничего не описано нигде и никем - "НО ОНО ЖЕ РАБОТАЕТ", да? Гугл и прочая свои кодгайды для лошков делает, видать


                  1. qark
                    29.08.2021 14:02

                    Ты уж определись над чем "кекаешь": короткие функции или "700 строк и 70 флагов".


                    1. LuggerMan
                      30.08.2021 10:07

                      Золотой середины нету, да?


                      1. qark
                        30.08.2021 16:27

                        По твоей логике нет, "нету".


                      1. LuggerMan
                        31.08.2021 12:22

                        Поразвернутей, пожалуйста. Для меня пока что все в точности наоборот, и Ваш тон, мягко говоря, непонятен. Может быть я Вам что-то сделал личное или неприятное, и Вы не можете унять жжение?


    1. Dim0v
      04.08.2021 18:26

      Обычно, когда вы видите этот контекст, вы можете определить во время компиляции, по какому пути пойдет код. Если это так, то всегда используйте этот паттерн.


    1. TheShock
      04.08.2021 19:04
      +3

      Первый же совет — ржач полный. Иф в методе убрал, а вызываю как? Наоборот, имхо лучше один метод, отвечающий за запись, с параметром (если он один — вообще прекрасно)

      Было:

      <button onClick={FileUtils.createFile("name.txt", "file contents", false)}>
      	Create Permanent File
      </button>
      
      <button onClick={FileUtils.createFile("name.txt", "file contents", true)}>
      	Create Temporary File
      </button>

      Стало:

      <button onClick={FileUtils.createPermanentFile("name.txt", "file contents")}>
      	Create Permanent File
      </button>
      
      <button onClick={FileUtils.createTemporaryFile("name.txt", "file contents")}>
      	Create Temporary File
      </button>


      1. gecube
        07.08.2021 17:06
        +1

        Ага, только вместо одной функции с if, мы теперь имеем две функции, скорее всего с реализацией по типу copy-paste, что будет плодить ошибки. А если там есть общие части, которые мы вынесем, например, в commonCreateFile(), то у нас вместо 1 функции уже будет три. Ну, что, гениально. Как создать себе работу на ровном месте…


        1. qark
          07.08.2021 18:14
          -1

          Это называется рефакторинг.


          1. gecube
            08.08.2021 16:47
            +1

            Главное, чтобы он был осмысленен. Это как ускорять работу функции в том месте, которое не "тормозит". Рефакторинг ради рефакторинга - зло.


            1. qark
              09.08.2021 18:09
              -1

              Волга впадает в Каспийское море. Лошади кушают овес и сено.


        1. TheShock
          08.08.2021 16:07

          Вы видели пример? Это для тех функций, у которох на верхнем уровне иф. Вы видите, что в этой функции просто два независимых блока кода без общей логики? Как вы вообще программистом стали с таким низким уровнем внимательности?

          public static void createFile(String name, String contents, boolean temporary) {
              if(temporary) {
                  // save temp file
              } else {
                  // save permanent file
              }
          }

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

          Но я слышал, что некоторым людям момент, когда надо подумать минутку - это слишком сложно и они хотят просто кодить.


          1. gecube
            08.08.2021 16:45

            Вы видите, что в этой функции просто два независимых блока кода без общей логики?

            Вы не можете ни подтвердить это, ни отрицать. Автор скромно схлопнул реальный код в комментарий. Блок "save temp file" может быть точно таким же как "save permanent file" на 90-99%. Ну, вот - написали как написали.

            а большая функция с кучей разных ответственностей - это плохо.

            разделять ответственность тоже нужно разумно.

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

            может да, а может нет. Точнее - не обязательно разносить на два метода, чтобы можно было выделить дублирующийся код.

            Как вы вообще программистом стали с таким низким уровнем внимательности?

            троллинг не засчитан.

            Но я слышал, что некоторым людям момент, когда надо подумать минутку - это слишком сложно и они хотят просто кодить.

            такое тоже бывает.


  1. Anamelash
    04.08.2021 15:27
    +1

    В последнем примере вместо проверки на null будет проверка на defaultValue

    Чем это лучше?


    1. Sklott
      04.08.2021 16:58

      Там смысл немного в другом. На defaultValue как раз проверять ничего не надо, наоборот вы его просто получаете и просто используете. Конечно это неприминимо если у вас нет defaultValue которе бы вы использовали если бы не нашли record.


  1. g6uru
    04.08.2021 17:04

    Разделить на два класса?

    TmpFile.new("file_name.txt").create
    RegularFile.new("file_name.txt").create


  1. Myclass
    04.08.2021 17:05
    +1

    Решение: Разделите метод на два новых метода. Вуаля, if исчезло.

    в параметрах иногда true/false незначительно изменяют кое-какие например флаги. Т.е. убрав if, мне придётся дублировать полностью намного сложнее код.

    Какое-то искуственное придумывание проблемы на пустом месте.


    1. Emulyator
      04.08.2021 17:25
      +1

      Одинаковый код можно вынести в еще один метод. Итого: + 2 метода в место одного if. )


      1. ldss
        09.08.2021 18:05
        +1

        если он настолько одинаковый, то он станет library method и будет использоваться в куче других мест. Что даст нам single point of truth для кучи рутинных вызовов


    1. Myclass
      05.08.2021 00:43
      +3

      Дорогой автор, даже, если вам не нравятся другие мнения, всё равно стоит к ним прислушаться. Объясню почему.

      В первом случае вы привели тривиальный случай, который 1:1 повторяет if оператор - те. вы заменили этот if оператор на две функции. А что делать, если внутри функции три и более if оператора? Заменять их на n! количество функций? У меня названий не хватит, если я вместо 4-х if(ов) 24 функции напишу, каждая из которых отдельную ситуацию воспроизведёт.

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

      В последнем примере опять-же тривиальный случай с типом string. А что делать, если функция возвращает другие типы данных? Например тот-же самый созданный файл, который не смог быть открытым, тк. места на плате нет или нет прав на создание. Что вы там за default value возьмёте?


  1. Myclass
    05.08.2021 00:03
    +1

    Не понимаю. весь ассемблер набит функциями по переходу в зависимости от значения или состояния в аккумуляторе. Под это и создаётся оператор if, ну те. он потом с лёгкостью переводится в набор ассемблер команд.


  1. sshmakov
    05.08.2021 08:41

    Интересно было бы увидеть способы избавления от if в алгоритмах обработки изображений, ведь для GPU каждый if - это боль.


    1. 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


  1. Myclass
    05.08.2021 09:21

    И ещё один момент но его задам с осторожностью. Могу ошибиться , тк. уже более 20 лет не живу в среде русского языка, но мне кажется, что ваше выражение

    Помните, что операторы if — это далеко не все зло.

    ...должно подниматься так, что if операторы - уже зло. Но они не одни есть зло, есть и другие злые вещи. А я вам скажу своё мнение, что сам оператор не есть зло. Злом может быть необдуманное его использование, которое сильно усложняет читаемость кода, а также возведение этого оператора в своего рода ранг табу и после - "борьба" против его использования. Так называемая охота на ведьм.


    1. mvv-rus
      05.08.2021 17:36

      Там, если писать по-русски, должно было бы быть:
      «Помните, что далеко не все операторы if — это зло.»
      Но не только лишь все русскоязычные копирайтеры умеют писать по-русски.


  1. ldss
    09.08.2021 18:08

    Хорошая статья

    Но если человек работает в б-м современной IDE, ему линтер все расскажет:) Другое дело, что многие это просто игнорируют, и это, собссно, одна из частей проблемы

    пс Смешно смотреть, как народ не догоняет про абстрактность примеров и не может их экстраполировать:)


    1. Myclass
      10.08.2021 19:46

       Смешно смотреть, как народ не догоняет про абстрактность примеров и не может их экстраполировать:)

      Ну все не могут быть такими умными как вы #ирония.

      А серьёзно- ведь как раз таки и была дискуссия о интерполяции этих примеров. И именно 'проблема' с этим. Но об этом уже были пару комментариев выше.


      1. ldss
        13.08.2021 16:03

        при чем тут я
        умение экстраполировать абстракции - часть умения в анализ
        странно не иметь его, будучи разработчиком