Статьи

Практический рефакторинг PHP: объединение условных выражений

В этой новой статье мы продолжаем заниматься условными выражениями и их эволюцией в сторону полиморфизма.

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

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

Почему один условный?

Дублирование в операциях для выполнения, так и в контрольных структурах , содержащие их , когда , если () есть запах, представьте себе ряд из них! Кроме того, вероятно, существует связь между различными условиями, поскольку они приводят к одному и тому же результату. Изменение одного может привести к изменению других, чтобы охватить остальные случаи.

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

Полиморфизм?

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

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

меры

Обязательным условием для применения этого рефакторинга является отсутствие побочных эффектов от выполнения условий. Если условия представляют собой сравнения или методы запроса, они не должны иметь никаких (разделение командного запроса). Побочные эффекты могут быть в выполняемом коде: например, если каждый затем блок добавляет что-то к переменной, этот рефакторинг (объединение блоков) не является жизнеспособным.

  1. Замените различные if одним выражением , связанным с или , и и ! ,
  2. Извлечь метод , инкапсулируя условие в нем.

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

Консолидация в одном выражении обычно следует этой эвристике:

  • Вложенные ifs разделяются и.
  • Если в последовательности становятся разделены или.

пример

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

<?php
class ConsolidateConditionalExpression extends PHPUnit_Framework_TestCase
{
    public function testAGroupIsInEvidenceWhenManyPostsArePresent()
    {
        $group = new Group('PHP forum', 100);
        $this->assertEquals('<span class="evidence">In evidence: PHP forum</span>', $group->__toString());
    }

    public function testAGroupIsShownAsNormalWhenThereAreNotManyPosts()
    {
        $group = new Group('PHP forum', 10);
        $this->assertEquals('<span>PHP forum</span>', $group->__toString());
    }

    public function testAGroupIsAlsoInEvidenceWhenItHasBeenRecentlyCreated()
    {
        $group = new Group('PHP forum', 0, true);
        $this->assertEquals('<span class="evidence">In evidence: PHP forum</span>', $group->__toString());
    }
}

class Group
{
    private $name;
    private $posts;
    private $recentlyCreated;

    public function __construct($name, $posts, $recentlyCreated = false)
    {
        $this->name = $name;
        $this->posts = $posts;
        $this->recentlyCreated = $recentlyCreated;
    }

    public function __toString()
    {
        if ($this->posts > 50) {
            return "<span class=\"evidence\">In evidence: $this->name</span>";
        }
        if ($this->recentlyCreated) {
            return "<span class=\"evidence\">In evidence: $this->name</span>";
        } 
        return "<span>$this->name</span>";
    }
}

Мы преобразуем условия в одно: поскольку они являются альтернативами, мы связываем их с помощью или:

class Group
{
    private $name;
    private $posts;
    private $recentlyCreated;

    public function __construct($name, $posts, $recentlyCreated = false)
    {
        $this->name = $name;
        $this->posts = $posts;
        $this->recentlyCreated = $recentlyCreated;
    }

    public function __toString()
    {
        if ($this->posts > 50 || $this->recentlyCreated) {
            return "<span class=\"evidence\">In evidence: $this->name</span>";
        } 
        return "<span>$this->name</span>";
    }
}

Затем мы извлекаем метод, содержащий условие, присваивая ему логическое имя isInEvidence ().

class Group
{
    private $name;
    private $posts;
    private $recentlyCreated;

    public function __construct($name, $posts, $recentlyCreated = false)
    {
        $this->name = $name;
        $this->posts = $posts;
        $this->recentlyCreated = $recentlyCreated;
    }

    public function __toString()
    {
        if ($this->isInEvidence()) {
            return "<span class=\"evidence\">In evidence: $this->name</span>";
        } 
        return "<span>$this->name</span>";
    }

    private function isInEvidence()
    {
        return $this->posts > 50 || $this->recentlyCreated;
    }
}

Мы могли бы продолжить, разделив класс на пару InEvidenceGroup и OrdinaryGroup, но это трудно сделать с помощью небольшого примера, так как мы должны также изменить код создания экземпляра (который здесь представлен отдельно от модульных тестов), чтобы решить, какой класс использовать. Условие будет перемещено туда; вот эскиз:

interface Group
{
    public function __toString();
}
class InEvidenceGroup
{
    /* ... */
    public function __toString()
    {
        return "<span class=\"evidence\">In evidence: $this->name</span>";
    }
}
class OrdinaryGroup
{
    /* ... */
    public function __toString()
    {
        return "<span>$this->name</span>";
    }
}

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