Статьи

Практический рефакторинг PHP: снятие флажка управления

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

Однако эти флаги являются пережитком тех времен, когда у старого максима одна точка входа, одна точка выхода была санкционирована как одно из правил чистого кода. Даже Фаулер в своей книге 1999 года говорит, что она устарела. Единственная точка входа (сигнатура метода) хороша, так как избегает gotos, но одна точка выхода является ограничением. Иногда логика лучше выражена с помощью дополнительной точки выхода.

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

Зачем устранять контрольный флаг?

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

while ($i < $limit && !$elementFound) {
    ...
    if (...) {
        $elementFound == true;
    }
}

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

for ($i = 0; $i < $limit && $array[$i] != $valueToSearch; $i++) {
}

альтернативы

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

Эти инструкции доступны на всех языках, производных от C, включая PHP:

  • перерыв инструкция прерывает цикл, и делает немного скачок к линии после ее окончания.
  • Продолжают INTERRUPTS инструкция текущей итерации цикла, и переходит к пересматривают состояние. Большую часть времени он начинает следующую итерацию (как в циклах for и foreach).
  • Ранняя инструкция возврата выходит из метода и дает (фактически * возвращает *) значение, переданное ему в качестве результата. Вы также можете просто написать * return; *, чтобы просто завершить метод, если его тип void.

меры

  1. Найдите присваивание (я) для значения, которое завершает цикл.
  2. Замените назначения оператором перерыва или оператором продолжения .
  3. Упростите код, удалив дополнительный механизм, который заставлял флаг работать: переменные, тип цикла, другие блоки, которые теперь могут быть частью основного блока цикла.

Используйте возврат, когда вы хотите выйти из всего метода; если вы извлекаете метод, содержащий цикл, вы будете использовать return вместо разрыва. Возврат является более распространенным и легко понимаемым, чем разрыв, поэтому он предпочтительнее.

пример

В исходном состоянии мы уже следуем принципу « Говори, не спрашивай» с объектом «Пользователи», первоклассной коллекцией; Это хорошее начало, чтобы не распространять один и тот же if () во всем клиентском коде, который обращается к коллекции Users.

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

<?php
class RemoveControlFlag extends PHPUnit_Framework_TestCase
{
    public function testFindsTheUserWhoseNameIsShortEnough() {
        $users = new Users(array('Giorgio', 'John', 'Tim'));
        $user = $users->findUserWithNameAsShortAs(3);
        $this->assertEquals('Tim', $user);
    }
}

class Users
{
    private $users;

    public function __construct(array $users)
    {
        $this->users = $users;
    }

    public function findUserWithNameAsShortAs($length)
    {
        $found = false;
        $i = 0;
        $length = count($this->users);
        while ($i < $length && !$found) {
            $user = $this->users[$i];
            if (strlen($user) == 3) {
                $found = true;
            }
            $i++;
        }
        return $user;
    }
}

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

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

class Users
{
    private $users;

    public function __construct(array $users)
    {
        $this->users = $users;
    }

    public function findUserWithNameAsShortAs($length)
    {
        $found = false;
        $i = 0;
        $length = count($this->users);
        while ($i < $length && !$found) {
            $user = $this->users[$i];
            if (strlen($user) == 3) {
                $found = true;
                return $user;
            }
            $i++;
        }
    }
}

Нам флаг больше не нужен:

class Users
{
    private $users;

    public function __construct(array $users)
    {
        $this->users = $users;
    }

    public function findUserWithNameAsShortAs($length)
    {
        $i = 0;
        $length = count($this->users);
        while ($i < $length) {
            $user = $this->users[$i];
            if (strlen($user) == 3) {
                return $user;
            }
            $i++;
        }
    }
}

Но теперь while всегда будет охватывать весь массив (и возвращать ноль), если не будет выполнено раннее возвращение. Таким образом, мы можем использовать простой foreach и исключить $ i и $ length.

<?php
class RemoveControlFlag extends PHPUnit_Framework_TestCase
{
    public function testFindsTheUserWhoseNameIsShortEnough() {
        $users = new Users(array('Giorgio', 'John', 'Tim'));
        $user = $users->findUserWithNameAsShortAs(3);
        $this->assertEquals('Tim', $user);
    }
}

class Users
{
    private $users;

    public function __construct(array $users)
    {
        $this->users = $users;
    }

    public function findUserWithNameAsShortAs($length)
    {
        foreach ($this->users as $user) {
            if (strlen($user) == 3) {
                return $user;
            }
        }
    }
}

Только теперь я понимаю, что у меня есть жестко закодированный 3 как длина. Более того, я столкнулся с именами, так как игнорировал ввод $ length, который сейчас пропал. Другой тест сказал бы мне об этом, но я могу просто написать очевидную реализацию сейчас (раньше это не было очевидно).

class Users
{
    private $users;

    public function __construct(array $users)
    {
        $this->users = $users;
    }

    public function findUserWithNameAsShortAs($length)
    {
        foreach ($this->users as $user) {
            if (strlen($user) == $length) {
                return $user;
            }
        }
    }
}

Обратите внимание, что, учитывая требования линейного поиска, я написал бы только этот код, и держу пари, что многие из вас сделали бы то же самое. Однако рефакторинги не всегда ориентированы на короткий цикл красно-зелёного рефакторинга, но также и на большие рефакторинги кода, который выглядел уродливым. Например, while (), с которого мы начали, возможно, несколько дней назад сопровождался дополнительным кодом или имели некоторые другие условия для выхода …