Статьи

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

В сегодняшнем сценарии мы видим повторные проверки на предмет равенства объекта с нулевым , ложным или другим скалярным значением без поведения. Эти проверки принимают форму, подобную ! == null и ! == false в PHP. Эти многочисленные проверки являются признаком того, что соответствующий случай не моделируется объектом: вся логика, относящаяся к нулевому значению для этой роли, распространяется между его клиентскими объектами.

Введение Null Object четко указывает точку, где собрать всю эту логику.

Нулевой объект? нуль не объект

Шаблон Null Object предоставляет способ сделать нулевое значение и реальный объект неразличимым: заставить их поддерживать одинаковые вызовы методов.

Вы уже используете нулевые объекты все время. Но они не являются объектами:

$sum = 0;
foreach ($rows as $row) {
    $sum += $row['amount'];
}

$ sum — это пустой объект, который поддерживает сумму ( обычно это значение может быть нулевым в случае целых чисел из-за неявных приведений, но не во всех языках).

Без этого вы бы написали непрерывные проверки на ничтожность:

$sum = null;
foreach ($rows as $row) {
    if ($sum) {
        $sum += $row['amount'];
    } else {
        $sum = $row['amount'];
    }
}

Это еще один простой пример:

$list = array();
foreach ($list as $element) {
    $this->doSomethingWith($element);
}

Логика шаблона Null Object такая же, но распространяется на ваши собственные типы вместо интерпретатора . Ваши классы будут иметь объекты «особый случай» для передачи вместо простых старых массивов, имеющих особый случай в пустом экземпляре.

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

Сценарий, в котором этот рефакторинг крайне необходим, — это когда проверки на ничтожность повторяются в разных местах кодовой базы. Нулевые объекты могут быть подклассом конкретного класса или альтернативной реализацией; трудно сломать принцип подстановки Лискова нулевым объектом. Я буду придерживаться подкласса в примере, так как он менее инвазивен.

меры

  1. Создайте подкласс вашего конкретного класса.
  2. Добавьте метод isNull () , который возвращает true в случае исходного класса и false в его переопределенной версии.
  3. Когда возвращается значение null или false, возвращает новый экземпляр объекта Null. Обычно достаточно реализации Flyweight: все нулевые объекты определенного конкретного класса равны (в противном случае они становятся реальными объектами, просто другой пример полиморфизма).
  4. Найдите все сравнения и используйте isNull () вместо нулевых проверок. Моя альтернатива этой процедуре, представленной Фаулером, заключается в использовании * instanceof NullObjectSubclass * в качестве рудиментарной isNull (), когда вы уверены, что она исчезнет к концу рефакторинга. Не очень полезно заменять * === null * эквивалентным логическим методом, который вскоре будет заменен снова.
  5. Найдите случаи, когда операция вызывается только для реального объекта , и переместите этот код в исходный класс .
  6. Найдите случаи, когда операция вызывается, когда объект имеет значение null , и переместите его в нулевой объект . Передайте в качестве параметров найденные зависимости.

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

пример

Проблема похожа на предыдущий пример , но на этот раз у нас нет иерархии, в которую можно разбить условное выражение. Мы не можем переместить метод на нуль, верно?

<?php
class IntroduceNullObject extends PHPUnit_Framework_TestCase
{
    public function testAUserWithAGroupShowsHisAffiliation()
    {
        $user = new User('giorgio', new Group('Engineers'));
        $this->assertEquals('giorgio belongs to Engineers', $user->getDescription());
    }

    public function testAUserWithoutAGroupDoesNotHaveABadge()
    {
        $user = new User('giorgio', null);
        $this->assertEquals('giorgio does not belong to a group yet', $user->getDescription());
    }
}

class User
{
    private $name;
    private $group;

    public function __construct($name, Group $group = null)
    {
        $this->name = $name;
        $this->group = $group;
    }

    public function getDescription()
    {
        if ($this->group === null) {
            return $this->name . ' does not belong to a group yet';
        }
        return $this->name . ' belongs to ' . $this->group->getName();
    }
}

class Group
{
    private $name;

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

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

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

Есть небольшие изменения, которые необходимо внести также в соответствующий тест (который не передает значение null, а экземпляр объекта Null) и в подсказку типа, которая больше не принимает значение null.

<?php
class IntroduceNullObject extends PHPUnit_Framework_TestCase
{
    public function testAUserWithAGroupShowsHisAffiliation()
    {
        $user = new User('giorgio', new Group('Engineers'));
        $this->assertEquals('giorgio belongs to Engineers', $user->getDescription());
    }

    public function testAUserWithoutAGroupDoesNotHaveABadge()
    {
        $user = new User('giorgio', new NoGroup);
        $this->assertEquals('giorgio does not belong to a group yet', $user->getDescription());
    }
}

class User
{
    private $name;
    private $group;

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

    public function getDescription()
    {
        if ($this->group instanceof NoGroup) {
            return $this->name . ' does not belong to a group yet';
        }
        return $this->name . ' belongs to ' . $this->group->getName();
    }
}

class Group
{
    private $name;

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

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

class NoGroup extends Group
{
    public function __construct() {}
}

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

class User
{
    private $name;
    private $group;

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

    public function getDescription()
    {
        return $this->group->belonging($this->name);
    }
}

class Group
{
    private $name;

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

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

    public function belonging($name)
    {
        return $name . ' belongs to ' . $this->name;
    }
}

class NoGroup extends Group
{
    public function __construct() {}

    public function belonging($name)
    {
        return $name . ' does not belong to a group yet';
    }
}