Около 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 — это далеко не все зло. В современных языках у нас есть богатый набор возможностей, которыми мы должны воспользоваться.

Полезные статьи


Материал подготовлен в рамках курса «Архитектура и шаблоны проектирования». Если вам интересно узнать подробнее о формате обучения и программе, познакомиться с преподавателем курса — приглашаем на день открытых дверей онлайн. Регистрация здесь.