Статьи

Практический рефакторинг PHP: замените магическое число символической константой

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

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

Зачем мне вводить дополнительную константу?

Для начала, когда вы хотите устранить дублирование буквального значения в другом месте. Если число достаточно сложное (3.14, 0xCAFEBABE), вы можете подумать, что вы можете использовать его в случае его изменения; однако единственные допустимые магические числа для использования непосредственно в коде — это 0, 1 и 2 в некоторых случаях.

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

$deck[51]

который не будет найден grep или любым другим инструментом без искусственного интеллекта. Другая проблема заключается в разных версиях номера:

3.14
3.1415
3.14159

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

Вторая мотивация для введения константы заключается в том, что она выражает концепцию (второе правило простого проектирования, XP), которая служит для того, чтобы сделать код самообъяснимым: константа имеет имя, которое описывает ее использование. В некотором смысле вы сосредотачиваетесь на роли, которую играет константа (количество карточек) вместо ее конкретной реализации (52).

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

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

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

Фаулер предлагает искать альтернативы перед созданием константы. Альтернатива означает, что мы могли бы написать код, который не принимает определенного значения магического числа, но он все еще корректен:

$deck[count($deck)-1] // access the last element of an array

Другие примеры альтернатив включают замену кода типа классом (с рефакторингом, который мы увидим позже в этой серии) и использование уже существующих констант . Ядро PHP предоставляет константы для многих математических величин:

M_PI
M_LN2
M_PI_2
M_SQRT2

меры

  1. Объявите константу : ее значение должно быть магическим числом.
  2. Найдите все вхождения буквального числа: обратите внимание также на литералы, которые могут быть функцией магического числа, например, 51 в приведенном выше примере. К сожалению, нет алгоритмических способов их поиска (кроме тестов, которые не выполняются при изменении константы).
  3. Проверьте, является ли литерал экземпляром магического числа или столкновения. В нашем примере 52 также может быть числом разрешенных типов транспортных средств или итальянской налоговой ставкой. Измените соответствующие цифры на ссылку на константу.

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

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

пример

В этом примере мы начинаем с класса, в котором скрыты различные магические числа: 4 масти, 13 карт для каждой масти и 52 как общее количество.

<?php
class ReplaceMagicNumberWithSymbolicConstant extends PHPUnit_Framework_TestCase
{
    public function testDeckIsFilledWithCardsInitially()
    {
        $deck = new Deck();
        $this->assertEquals(52, count($deck));
    }
    public function testDeckCanDrawAllItsCards()
    {
        $deck = new Deck();
        for ($i = 0; $i < 52; $i++) {
            $card = $deck->draw();
            $this->assertGreaterThanOrEqual(1, $card);
            $this->assertLessThanOrEqual(13, $card);
        }
        $this->assertEquals(0, count($deck));
    }
}
class Deck implements Countable
{
    private $cards;
    public function __construct()
    {
        $this->cards = array();
        for ($i = 0; $i < 4; $i++) {
            $this->cards = array_merge($this->cards, range(1, 13));
        }
    }
    public function count()
    {
        return count($this->cards);
    }
    public function draw()
    {
        return array_shift($this->cards);
    }
}

Мы создаем константы и планируем извлечь 52 из других значений:

class Deck implements Countable
{
    const RANGE = 13;
    const SUITS = 4;
    private $cards;

    public function __construct()
    {
        $this->cards = array();
        for ($i = 0; $i < 4; $i++) {
            $this->cards = array_merge($this->cards, range(1, 13));
        }
    }
    public function count()
    {
        return count($this->cards);
    }

    public function draw()
    {
        return array_shift($this->cards);
    }
}

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

<?php
class ReplaceMagicNumberWithSymbolicConstant extends PHPUnit_Framework_TestCase
{
    private $totalCards;

    public function setUp()
    {
        $this->totalCards = Deck::SUITS * Deck::RANGE;
    }

    public function testDeckIsFilledWithCardsInitially()
    {
        $deck = new Deck();
        $this->assertEquals($this->totalCards, count($deck));
    }

    public function testDeckCanDrawAllItsCards()
    {
        $deck = new Deck();
        for ($i = 0; $i < $this->totalCards; $i++) {
            $card = $deck->draw();
            $this->assertGreaterThanOrEqual(1, $card);
            $this->assertLessThanOrEqual(Deck::RANGE, $card);
        }
        $this->assertEquals(0, count($deck));
    }
}

class Deck implements Countable
{
    const RANGE = 13;
    const SUITS = 4;
    private $cards;

    public function __construct()
    {
        $this->cards = array();
        for ($i = 0; $i < self::SUITS; $i++) {
            $this->cards = array_merge($this->cards, range(1, self::RANGE));
        }
    }
    public function count()
    {
        return count($this->cards);
    }

    public function draw()
    {
        return array_shift($this->cards);
    }
}