Почему ранний возврат из функций так важен? +42


Привет, Хабр! Представляю вашему вниманию перевод статьи «Why should you return early?» автора Szymon Krajewski

image

В начале моего приключения в роли программиста мой код зачастую напоминал вермишель. В любых условных выражениях я только и делал, что сразу переходил к описанию верного исхода, оставляя на конец остальное. «Это работает, вот и все», — говорил я себе, а код продолжал расти, как на дрожжах. Тысячи написанных методов в итоге заставили меня задуматься, а не стоит ли поменять их внутреннюю логику, возвращая отрицательные результаты как можно раннее. Таким образом, я пришел к тому, что теперь называю правилом «неотложного провала».

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

Два подхода проверки требований


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

function sendEmail(string $email, string $message)
{
    if (filter_var($email, FILTER_VALIDATE_EMAIL)) {
        if ($message !== '') {
            return Mailer::send($email, $message);
        } else {
            throw new InvalidArgumentException('Cannot send an empty message.');
        }
    } else {
        throw new InvalidArgumentException('Email is not valid.');
    }
}

Предыдущий отрывок кода является именно тем, что было описано раннее. Работает ли это? Абсолютно. Читаемо ли? Не совсем.

В чем проблема написания требований в самом коде?


Существует две стилистические проблемы в написанной функции sendMail():

  1. Присутствует более одного уровня отступов в теле функции;
  2. Невозможно сразу определить путь успешности функции без ее полного изучения;

Так как первый пункт касается понятия «чистого кода», позвольте мне сфокусироваться на втором. Наша функция имеет несколько ветвей, а оператор return недостаточно хорошо обозреваем. Конечно, приведенный пример очень прост и короток, но на практике в функции может наблюдаться огромное множество условий и возвратов значений. В этот момент и вступает в дело герой-спаситель «обратное условие».

function sendEmail(string $email, string $message)
{
    if (! filter_var($email, FILTER_VALIDATE_EMAIL)) {
        throw new InvalidArgumentException('Email is not valid.');
    }

    if ($message === '') {
        throw new InvalidArgumentException('Cannot send an empty message.');
    }

    return Mailer::send($email, $message);
}

Я понимаю, что избавился от одного else после второго условного оператора, но, субъективно говоря, новый образец кода выглядит заметно чище и эстетичней. Явно, что «обратное условие» заслужило печеньку за помощь (прим. переводчика, тоже печеньку ему).

Чего я достигну, используя «обратные условия»?


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

Наверное, сейчас вы недоумеваете, а что именно я назвал подходом «неотложного провала». К сожалению, Jim Shore и Martin Fowler дали определение этому термину еще задолго до моего “Hello world”. Конечно, хотя и цели обоих подходов одинаковые, я решил переименовать мое видение в «ранний возврат». Термин описывает сам себя, так что я закрепил это название «на вывеске».

Концепт «раннего возврата»


Для начала требуется формально определить данный термин.
«Ранний возврат» — это концепт написания функций таким образом, что ожидаемый положительный результат возвращается в конце, когда остальной код в случае расхождения с целью функции должен завершить ее выполнение настолько раньше, насколько возможно.
Вы не поверите, насколько долго я думал над данным определением, но в любом случае концепт является обобщенным. Что такое «ожидаемый положительный результат», «завершение выполнения» и «цель функции»? Постараюсь описать это в дальнейшем.

Следуйте «пути к счастью»


«Горшочек с золотом в конце радуги», помните ли вы эту историю? А может вы уже нашли такой горшок? Это именно то, чем можно описать подход «раннего возврата». Ожидаемый приз, успех нашей функции находится именно в конце пути. Глядя на новую версию sendMail, можно уверенно сказать, что целью функции была отправка сообщения на указанный почтовый адрес. Это и есть «ожидаемый положительный результат», иными словами «путь к счастью».
image
Основной путь в функции четко обозреваем

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

Избавьтесь от отрицательных случаев как можно ранее


Не всегда исполнение функции приводит к ожидаемому результату. Порой пользователь может ввести неверный email адрес в нашем примере, а порой и пустую строку в качестве сообщения. В обоих случаях требуется завершить работу функции немедленно. Завершением функции может быть или возврат отрицательного значение, или возврат ошибки. Данный выбор индивидуален. Некоторых людей может смутить использование нескольких операторов возврата, но это вполне нормальная практика. К тому же данный подход зачастую делает функцию более понятной для человеческого взора.

«Шаблон вышибалы» — это раннее описанная методология, которая завершает выполнение функции в случаях ее неверного состояния. Взгляните на следующий пример:

function matrixAdd(array $mA, array $mB)
{
    if (! isMatrix($mA)) {
        throw new InvalidArgumentException("First argument is not a valid matrix.");
    }

    if (! isMatrix($mB)) {
        throw new InvalidArgumentException("Second argument is not a valid matrix.");
    }

    if (! hasSameSize($mA, $mB)) {
        throw new InvalidArgumentException("Arrays have not equal size.");
    }

    return array_map(function ($cA, $cB) {
        return array_map(function ($vA, $vB) {
            return $vA + $vB;
        }, $cA, $cB);
    }, $mA, $mB);
}

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

Возвращайте пораньше из действий контроллеров


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

/* PostController.php */

public function updatePostAction(Request $request, $postId)
{
    $error = false;

    if ($this->isGranded('POST_EDIT')) {
        $post = $this->repository->get($postId);

        if ($post) {
            $form = $this->createPostUpdateForm();
            $form->handleRequest($post, $request);

            if ($form->isValid()) {
                $this->manager->persist($post);
                $this->manager->flush();

                $message = "Post has been updated.";
            } else {
                $message = "Post validation error.";
                $error = true;
            }
        } else {
            $message = "Post doesn't exist.";
            $error = true;
        }
    } else {
        $message = "Insufficient permissions.";
        $error = true;
    }

    $this->addFlash($message);

    if ($error) {
        $response = new Response("post.update", ['id' => $postId]);
    } else {
        $response = new RedirectResponse("post.index");
    }

    return $response;
}

Можно заметить, что кусок кода является достаточно объемным, включая множество вложенных условий. Этот же отрывок можно переписать, использую метод «раннего возврата».

/* PostController.php */

public function updatePostAction(Request $request, $postId)
{
    $failResponse = new Response("post.update", ['id' => $postId]);

    if (! $this->isGranded('POST_EDIT')) {
        $this->addFlash("Insufficient permissions.");
        return $failResponse;
    }

    $post = $this->repository->get($postId);

    if (! $post) {
        $this->addFlash("Post doesn't exist.");
        return $failResponse;
    }

    $form = $this->createPostUpdateForm();
    $form->handleRequest($post, $request);

    if (! $form->isValid()) {
        $this->addFlash("Post validation error.");
        return $failResponse;
    }

    $this->manager->persist($post);
    $this->manager->flush();

    return new RedirectResponse("post.index");
}

Теперь же любой программист может четко увидеть «путь к счастью». Все очень просто: ожидаете, что что-то может пойти не так, — проверьте это и верните отрицательный результат пораньше. Вышеуказанный код также имеет один уровень отступов и присущую большинству подобных функций читаемость. Опять-таки, больше не нужно использовать else условия.

Возвращайте пораньше из рекурсивных функций


Рекурсивные функции также должны прерываться как можно ранее.

function reverse($string, $acc = '')
{
    if (! $string) {
        return $acc;
    }

    return reverse(substr($string, 1), $string[0] . $acc);
}

Недостатки подхода «раннего возврата»


Возникает вопрос, а является ли описанный концепт панацеей? В чем же заключены недостатки?

Проблема 1. Стилистика кода – понятие субъективное


Мы, программисты, зачастую тратим больше времени на чтение кода, нежели его написание. Это довольно-таки известная правда. Таким образом, требуется написание как можно более простого и изящного кода, насколько возможно. Я настаиваю на следовании концепта «раннего возврата» именно потому, что основной механизм функции заключен именно в ее конце.

Вот только выбор стилистики кода подвержен субъективному мнению. Кому-то удобней единое использование оператора возврата, а кому-то нет. Выбор, в конечном итоге, остается за каждым программистом.

image
Если представить две абстрактные функции, какая из них будет казаться проще для чтения и понимания?

Проблема 2. Иногда «ранний возврат» — лишнее усложнение


Можно привести множество примеров, когда «ранний возврат» негативно влияет на конечный код. Типичным примером можно привести функции-сеттеры, в которых параметры зачастую отличаются от false:

public function setUrl($url)
{
    if (! $url) {
        return;
    }

    $this->url = $url;
}

Это не самое значительное улучшение читабельности кода. Ситуативность использования подхода остается заботой самого программиста, ведь, используя стандартный подход, можно добиться лучшего результата:

public function setUrl($url)
{
    if ($url) {
        $this->url = $url;
    }
}

Проблема 3. Все это выглядит как оператор break


Так как в функции может быть расположено большое количество операторов return, не всегда удобно понять, откуда был получен результат. Но до тех пор, пока вы используете данный подход для завершения некорректных состояний функции, все будет в порядке. Ах, да, пишете ли вы тесты?

Проблема 4. Иногда лучше использовать одну переменную вместо множества return операторов


Существуют структуры кода, в которых подход «раннего возврата» ничего не изменяет. Только взгляните на следующие примеры кода:

function nextState($currentState, $neighbours)
{
    $nextState = 0;

    if ($currentState === 0 && $neighbours === 3) {
        $nextState = 1;
    } elseif ($currentState === 1 && $neighbours >= 2 && $neighbours <= 3) {
        $nextState = 1;
    }

    return $nextState;
}

Эта же функция с вышеуказанным концептом выглядит так:

function nextState($currentState, $neighbours)
{
    if ($currentState === 0 && $neighbours === 3) {
        return 1;
    }

    if ($currentState === 1 && $neighbours >= 2 && $neighbours <= 3) {
        return 1;
    }

    return 0;
}

Очевидно, что больших преимуществ у второго примера над первым нет.

Заключение


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

Концепт «раннего возврата» вводит некоторые правила, необязательные для исполнения в 100% случаев, но которые зачастую помогают делать код чище и читабельней. Очень важно сохранять дисциплину при написании кода, ведь следование единой стилистике кода важней, чем ее выбор.

Лично я использую метод «раннего возврата» настолько часто, насколько возможно, особенно когда неотложное завершение функций с некорректными состояниями улучшает эстетику кода. Я привык, что успешное завершение всегда находится в конце функций, ведь недаром одна из мантр гласит:
Следуйте пути к успеху и реагируйте в случае ошибок.


Оригинал статьи
Заметка: переводчик оставляет за собой право стилистически менять фразы из источника.




К сожалению, не доступен сервер mySQL