Статьи

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

В сегодняшнем сценарии if выбирает другое поведение в зависимости от типа объекта. Мы должны определить «тип» очень вяло; например, это может быть:

  • класс объекта или один из реализуемых им интерфейсов (instanceof).
  • Значение одного из полей объекта (обычно перечислительное).
  • Результат метода запроса, такого как isXXX () или getTotalValue ().

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

Зачем устранять это условно? И заменить его чем?

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

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

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

Обратите внимание, что этот рефакторинг работает не только для ifs и elses, но и для select.

Недостающий шаг

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

Если иерархии еще нет, у вас есть два варианта предварительного рефакторинга:

  • наследование : заменить код типа на подклассы . Текущий класс приобретает новых детей; это менее инвазивный подход, но также и менее ясный. Наследование — это одноразовая стратегия, поскольку вы не сможете использовать ее для другой оси изменений.
  • состав : замените код типа на State или Strategy . Новый класс извлекается, который получает bew детей. Более гибкий, как вы можете извлечь много соавторов, как это, и заставляет назвать новую концепцию и ее новую иерархию.

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

меры

  1. Извлечь метод, содержащий условные.
  2. Переместите этот метод в начало иерархии наследования.
  3. Скопируйте оригинальный метод в каждый подкласс и устраните все механизмы, чтобы оставить только одну ногу. Некоторые данные исходного класса могут стать защищенными, а не частными.
  4. Удалите скопированную ветвь и повторяйте со следующим подклассом, пока метод в суперклассе не станет пустым, а переопределения не содержат ссылок на классы, отличные от текущего.

пример

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

<?php
class ReplaceConditionalWithPolymorphism extends PHPUnit_Framework_TestCase
{
    public function testAUserProfileShouldHaveAStandardURL()
    {
        $renderer = new Renderer(new User('giorgio'));
        $this->assertEquals('<a href="/giorgio">giorgio</a>', $renderer->__toString());
    }

    public function testABrandPageShouldHaveACustomURL()
    {
        $renderer = new Renderer(new Brand('Coca Cola', 'coke'));
        $this->assertEquals('<a href="/coke">Coca Cola</a>', $renderer->__toString());
    }
}

class User 
{
    private $name;

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

    public function getName()
    {
        return $this->name;
    }
}

class Brand 
{
    private $name;
    private $url;

    public function __construct($name, $url)
    {
        $this->name = $name;
        $this->url = $url;
    }

    public function getName()
    {
        return $this->name;
    }

    public function getURL()
    {
        return $this->url;
    }
}

class Renderer
{
    private $domainObject;

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

    public function __toString()
    {
        if ($this->domainObject instanceof User)
        {
            return '<a href="/' . $this->domainObject->getName() . '">' . $this->domainObject->getName() . '</a>';
        }
        if ($this->domainObject instanceof Brand)
        {
            return '<a href="/' . $this->domainObject->getURL() . '">' . $this->domainObject->getName() . '</a>';
        }
    }
}

Есть две проблемы с этим дизайном:

  • if we add a new domain object (e.g. Group, with its own page) we have to open up the hood and insert new code in an already existing class. It’s generally easier to deal with a change by adding brand new classes, since we can’t break existing code if we do not touch it.
  • Not only Renderer contains these different cases, but probably many objects composing Brand and User.

The first move is to isolate the variability into a single hierarchy of classes.Since there is no common ancestor, we create a common superclass in Brand and User. getURL() would be better named as getSlug() actually (an url-friendly label).

abstract class Addressable
{
    public function render($template)
    {
        if ($this instanceof User)
        {
            return sprintf($template, $this->getName(), $this->getName());
        }
        if ($this instanceof Brand)
        {
            return sprintf($template, $this->getUrl(), $this->getName());
        }
    }
}

We only hid the mess, not resolved it: the base class is still a single point that knows everything about the subclasses. At least there would be no duplication if someone wants to use name or URL in other HTML fragments.

We copy the method into the various subclasses. In this case we augment duplication, but that’s a temporary move since we will eliminate most of the code in the methods shortly.

class User extends Addressable
{
    private $name;

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

    public function getName()
    {
        return $this->name;
    }

    public function render($template)
    {
        if ($this instanceof User)
        {
            return sprintf($template, $this->getName(), $this->getName());
        }
        if ($this instanceof Brand)
        {
            return sprintf($template, $this->getUrl(), $this->getName());
        }
    }
}

class Brand extends Addressable
{
    private $name;
    private $url;

    public function __construct($name, $url)
    {
        $this->name = $name;
        $this->url = $url;
    }

    public function getName()
    {
        return $this->name;
    }

    public function getURL()
    {
        return $this->url;
    }

    public function render($template)
    {
        if ($this instanceof User)
        {
            return sprintf($template, $this->getName(), $this->getName());
        }
        if ($this instanceof Brand)
        {
            return sprintf($template, $this->getUrl(), $this->getName());
        }
    }
}

In the next step, we make the original method abstract: since each concrete class has its own copy, it will never be executed.

abstract class Addressable
{
    public abstract function render($template);
}

We eliminate impossible cases: now dynamic dispatch is doing the work of choosing which method to execute (instead of a chain of ifs). Brand and User only refer to their own methods and do not know anything about each other’s presence.

abstract class Addressable
{
    public abstract function render($template);
}

class User extends Addressable
{
    private $name;

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

    public function getName()
    {
        return $this->name;
    }

    public function render($template)
    {
        return sprintf($template, $this->getName(), $this->getName());
    }
}

class Brand extends Addressable
{
    private $name;
    private $url;

    public function __construct($name, $url)
    {
        $this->name = $name;
        $this->url = $url;
    }

    public function getName()
    {
        return $this->name;
    }

    public function getURL()
    {
        return $this->url;
    }

    public function render($template)
    {
        return sprintf($template, $this->getUrl(), $this->getName());
    }
}

The last step, although not strictly part of this refactoring, is to eliminate the getters since no one calls them from outside the class in this particular example.

class User extends Addressable
{
    private $name;

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

    public function render($template)
    {
        return sprintf($template, $this->name, $this->name);
    }
}

class Brand extends Addressable
{
    private $name;
    private $url;

    public function __construct($name, $url)
    {
        $this->name = $name;
        $this->url = $url;
    }

    public function render($template)
    {
        return sprintf($template, $this->url, $this->name);
    }
}

We have now a solution where new classes can be added freely, and new Renderer can use all Addressable classes. It’s a Bridge pattern, or just good factoring.