Статьи

Практический PHP-рефакторинг: замена вложенных условных выражений выражениями Guard

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

Пример запутанных условных выражений на самом деле представляет собой группу вложенных:

if ($user->isAdmin()) {
    if ($this->closed) {
        $displayed = "Closed (not for you)";
    } else if ($this->inEvidence) {
        $displayed = "Open and in evidence";
    } else {
        $displayed = "";
    }
} else {
    if ($this->closed) {
        $displayed = "Closed";
    } else {
        $displayed = "";
    }
}

Как код получает это плохо? Одна строка в то время. Это классический пример кода, где каждая строка добавляется в другой день, и никто не пытается ее улучшить. Если вы бросите лягушку в кипящую воду, она отскочит; если вы положите его в пресную воду и медленно нагреете, вы получите лягушачий суп .

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

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

Охранные предложения лучше масштабируются для длинных условий , так как вы можете поместить много защитных предложений, которые покрывают много угловых случаев в начале, а затем продолжить как обычно.
Однако их распространение вызывает проблемы, как и для других условных выражений, но если вы посвящаете тестирование каждому угловому случаю, вы покрываете все пункты охраны. Задача пунктов охраны состоит в том, чтобы упростить проблемный код, в котором джунгли if / else / elseif будут общим результатом ; если они хранятся в листовых классах, они не могут дублироваться в разных местах кодовой базы.

Рефакторинг для защиты пунктов является еще одним примером упадка одной записи, одного правила выхода .

меры

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

  1. Для каждого угла случае , поставить пункт охраны в начале метода , который выполняет раннее возвращение с правильным результатом. Тесты все равно должны пройти.
  2. Проверьте тесты . Цель состоит в том, чтобы продолжать проводить «зеленый» тест, исключая при этом части кода для рефакторинга; пункты охраны делают это так, чтобы проблемные случаи не достигли основной части условных джунглей.

Последний случай с крышкой является основной , который должен быть из условных.

пример

В исходном состоянии у нас есть небольшие джунгли кода.

<?php
class ReplaceNestedConditionalWithGuardClauses extends PHPUnit_Framework_TestCase
{
    public function testTheOpenTopicTitleIsDisplayedNormally()
    {
         $topic = new Topic("Hello");
         $this->assertEquals("Hello", $topic->__toString());
    }

    public function testTheClosedTopicTitleIsDisplayedWithACorrespondingIndication()
    {
         $topic = new Topic("Hello", true);
         $this->assertEquals("Closed: Hello", $topic->__toString());
    }

    public function testTheClosedTopicTitleIsDisplayedNormallyToAdmins()
    {
         $topic = new Topic("Hello", true, true);
         $this->assertEquals("Closed (not for you): Hello", $topic->__toString());
    }
}

class Topic
{
    private $title;
    private $isClosed;
    private $isAdminViewing;

    public function __construct($title, $isClosed = false, $isAdminViewing = false)
    {
        $this->title = $title;
        $this->isClosed = $isClosed;
        $this->isAdminViewing = $isAdminViewing;
    }

    public function __toString()
    {
        if (!$this->isClosed) {
            $displayed = $this->title;
        } else {
            if ($this->isAdminViewing) {
                $displayed = "Closed (not for you): $this->title";
            } else {
                $displayed = "Closed: $this->title";
            }
        }
        return $displayed;
    }
}

Мы добавляем первый пункт охраны.

    public function __toString()
    {
        if ($this->isClosed && $this->isAdminViewing) {
            return "Closed (not for you): $this->title";
        }
        if (!$this->isClosed) {
            $displayed = $this->title;
        } else {
            if ($this->isAdminViewing) {
            } else {
                $displayed = "Closed: $this->title";
            }
        }
        return $displayed;
    }

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

    public function __toString()
    {
        if ($this->isClosed && $this->isAdminViewing) {
            return "Closed (not for you): $this->title";
        }
        if ($this->isClosed) {
            return "Closed: $this->title";
        }
        if (!$this->isClosed) {
            $displayed = $this->title;
        } else {
            if ($this->isAdminViewing) {
            } else {
            }
        }
        return $displayed;
    }

Мы добавляем основной случай в качестве безусловного оператора возврата.

    public function __toString()
    {
        if ($this->isClosed && $this->isAdminViewing) {
            return "Closed (not for you): $this->title";
        }
        if ($this->isClosed) {
            return "Closed: $this->title";
        }
        return $this->title;

        if (!$this->isClosed) {
            $displayed = $this->title;
        } else {
            if ($this->isAdminViewing) {
            } else {
            }
        }
        return $displayed;
    }

Теперь мы можем упростить код, так как его часть недоступна.

    public function __toString()
    {
        if ($this->isClosed && $this->isAdminViewing) {
            return "Closed (not for you): $this->title";
        }
        if ($this->isClosed) {
            return "Closed: $this->title";
        }
        return $this->title;
    }

Теперь мы продолжим и попробуем полиморфное решение: чтобы исключить одну из двух осей изменения (закрытая или открытая тема), мы вводим объект TopicState.

Обратите внимание, что код разбит на разные меньшие объекты. Я реализую один из них с предложением guard, последнее if (), которое остается.

<?php
class ReplaceNestedConditionalWithGuardClauses extends PHPUnit_Framework_TestCase
{
    public function testTheOpenTopicTitleIsDisplayedNormally()
    {
         $topic = new Topic("Hello", new OpenTopicState);
         $this->assertEquals("Hello", $topic->__toString());
    }

    public function testTheClosedTopicTitleIsDisplayedWithACorrespondingIndication()
    {
         $topic = new Topic("Hello", new ClosedTopicState);
         $this->assertEquals("Closed: Hello", $topic->__toString());
    }

    public function testTheClosedTopicTitleIsDisplayedNormallyToAdmins()
    {
         $topic = new Topic("Hello", new ClosedTopicState, true);
         $this->assertEquals("Closed (not for you): Hello", $topic->__toString());
    }
}

class Topic
{
    private $title;
    private $isClosed;
    private $isAdminViewing;

    public function __construct($title, TopicState $isClosed, $isAdminViewing = false)
    {
        $this->title = $title;
        $this->isClosed = $isClosed;
        $this->isAdminViewing = $isAdminViewing;
    }

    public function __toString()
    {
        return $this->isClosed->topicCaption($this->isAdminViewing)
             . $this->title;
    }
}

interface TopicState
{
    /**
     * @return string
     */
    function topicCaption($isAdminViewing);
}

class OpenTopicState implements TopicState
{
    function topicCaption($isAdminViewing)
    {
        return '';
    }
}

class ClosedTopicState implements TopicState
{
    function topicCaption($isAdminViewing)
    {
        if ($isAdminViewing) {
            return 'Closed (not for you): ';
        }
        return 'Closed: ';
    }
}

Мы также переименовываем поле, чтобы отразить имя класса, чтобы завершить операцию.

class Topic
{
    private $title;
    private $topicState;
    private $isAdminViewing;

    public function __construct($title, TopicState $topicState, $isAdminViewing = false)
    {
        $this->title = $title;
        $this->topicState = $topicState;
        $this->isAdminViewing = $isAdminViewing;
    }

    public function __toString()
    {
        return $this->topicState->topicCaption($this->isAdminViewing)
             . $this->title;
    }
}