Статьи

Практический рефакторинг PHP: замените код ошибки на исключение

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

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

Исключительные преимущества

Исключения избегают непрерывных операторов if (), вставляемых для проверки возвращаемого значения метода: в случае ошибки исключение всплывает до ближайшего предложения catch, а промежуточный код не знает о его существовании:

try {
    $object->method();
} catch (SomeException $e) {
    /* ... */
}

В этом случае трассировка стека содержит много методов, вызываемых внутренне $ object. Только те, кто генерирует исключение, знают, что класс SomeException существует: методы, которые не генерируют его, не замечают этого факта . Кроме того, методы, вызывающие другие методы, которые, в свою очередь, генерируют исключения, не обязаны знать (все исключения PHP не проверяются, и поэтому RuntimeException в PHP является бесполезным классом):

public function method()
{
    $this->collaborator->otherMethod(); // this call may throw an exception, but this fact can be decoupled from this method
}

Таким образом, большая часть кода менее связана с исключением, и его изменение или расширение будет проще.

Сравните эту свободу с подходом кода ошибки; у вас будет два варианта.

  • Каждый метод проверяет возвращаемое значение вызываемого метода с помощью if () (для каждого вызова, где возможна ошибка).
  • Или вы будете вынуждены немедленно устранить любую ошибку:
mysql_connect($host, $user, $password) or die('...');

Есть несколько других преимуществ исключений перед кодами ошибок, возвращаемыми методами:

  • объекты исключений могут реализовывать методы и, таким образом, являются способом избежать Primitive Obsession. Например, они могут реализовать методы, которые возвращают переведенную версию сообщения об ошибке.
  • Исключения могут использоваться в конструкторах для сигнализации об ошибке, в то время как вызов new () не может возвращать значение, отличное от экземпляра.

Исключениями можно легко злоупотреблять

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

На мой взгляд, валидация — это предел использования исключений: любые обычные варианты использования и исключения не должны использоваться.

меры

  1. Найдите все вызовы и настройте их для использования блока try / catch вместо проверки возвращаемого значения. Позже вы сможете удалить большинство этих конструкций, если есть ошибки управления верхнего уровня. Вы сможете удалить большой объем кода, если между генерацией ошибки и ее обработкой будет длинный след методов.
  2. Чтобы тесты снова прошли, выведите исключение вместо возврата значения специальной ошибки.
  3. Измените сигнатуру метода: добавьте предложения @throws для документации.

если это слишком много для изменения (слишком много вызовов), Фаулер предлагает обрабатывать один вызов за один раз, дублируя оригинальный метод и поддерживая две версии в течение некоторого времени (некоторые коммиты).

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

пример

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

Тесты проверяют счастливый путь (без проблем с весом) и конкретный путь отказа (слишком большой вес помещается в нашу сумку, который сломается).

<?php
class ReplaceErrorCodeWithException extends PHPUnit_Framework_TestCase
{
    public function testItemsCanBeAddedAtWill()
    {
        $bag = new Bag(10);
        $result = $bag->addItem('Domain-Driven Design', 10);
        $this->assertEquals(0, $result);
        $this->assertEquals(10, $bag->getWeight());
    }

    public function testTheWeightLimitCannotBeInfringed()
    {
        $bag = new Bag(10);
        $result = $bag->addItem('Land of Lisp', 11);
        $this->assertEquals(Bag::TOO_MUCH_WEIGHT, $result);
        $this->assertEquals(0, $bag->getWeight());
    }
}

class Bag
{
    private $weightLimit;
    private $weight;

    const TOO_MUCH_WEIGHT = 1;

    public function __construct($weightLimit)
    {
        $this->weightLimit = $weightLimit;
        $this->weight = 0;
    }

    /**
     * @return int  0 if no errors
     */
    public function addItem($name, $itemWeight)
    {
        if ($this->weightLimit < $this->weight + $itemWeight) {
            return self::TOO_MUCH_WEIGHT;
        }
        $this->weight += $itemWeight;
        return 0;
    }

    public function getWeight()
    {
        return $this->weight;
    }
}

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

<?php
class ReplaceErrorCodeWithException extends PHPUnit_Framework_TestCase
{
    public function testItemsCanBeAddedAtWill()
    {
        $bag = new Bag(10);
        $result = $bag->addItem('Domain-Driven Design', 10);
        $this->assertEquals(10, $bag->getWeight());
    }

    public function testTheWeightLimitCannotBeInfringed()
    {
        $bag = new Bag(10);
        try {
            $result = $bag->addItem('Land of Lisp', 11);
            $this->fail('Adding the item should not be allowed.');
        } catch (TooMuchWeightException $e) {
            $this->assertEquals(0, $bag->getWeight());
        }
    }
}

Первый тест все еще проходит, но больше не проверяет возвращаемое значение, так как случай по умолчанию является хорошим. Второй тест использует try / catch и вызов $ this-> fail () для проверки наличия исключения; мы могли бы использовать аннотацию @expectedException для PHPUnit, но не в этом случае, когда нам нужно сделать утверждение о состоянии экземпляра Bag после ошибки.

Теперь контракт между клиентским кодом и этим классом нарушен. Я не советую совершать здесь; во-первых, давайте исправим ошибку, добавив необходимый класс и код метания.

class Bag
{
    private $weightLimit;
    private $weight;

    const TOO_MUCH_WEIGHT = 1;

    public function __construct($weightLimit)
    {
        $this->weightLimit = $weightLimit;
        $this->weight = 0;
    }

    /**
     * @return int  0 if no errors
     */
    public function addItem($name, $itemWeight)
    {
        if ($this->weightLimit < $this->weight + $itemWeight) {
            throw new TooMuchWeightException("Weight has exceeded the limit for this Bag: $this->weightLimit");
        }
        $this->weight += $itemWeight;
        return 0;
    }

    public function getWeight()
    {
        return $this->weight;
    }
}

class TooMuchWeightException extends Exception {}

Теперь испытания проходят снова. Мы можем обновить докблок класса и удалить ненужный код (например, константу).

class Bag
{
    private $weightLimit;
    private $weight;

    public function __construct($weightLimit)
    {
        $this->weightLimit = $weightLimit;
        $this->weight = 0;
    }

    /**
     * @throws TooMuchWeightException
     */
    public function addItem($name, $itemWeight)
    {
        if ($this->weightLimit < $this->weight + $itemWeight) {
            throw new TooMuchWeightException("Weight has exceeded the limit for this Bag: $this->weightLimit");
        }
        $this->weight += $itemWeight;
        return 0;
    }

    public function getWeight()
    {
        return $this->weight;
    }
}