Статьи

Практический рефакторинг PHP: инкапсуляция коллекции

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

Этот рефакторинг, Encapsulate Collection, предпочитает инкапсуляцию по сравнению с представлением примитивного типа: он заменяет пару аксессор / мутатор более конкретными методами, например add () и remove () для изменения содержимого коллекции элемента в то время.

В случае PHP коллекции — это в основном массивы, но также и специализированные классы SPL, такие как ArrayObject или SplDoublyLinkedList и его производные ; другие классы инфраструктуры, такие как реализации Doctrine \ Common \ Collections \ Collection , все еще считаются коллекциями в результате этого рефакторинга.

Зачем скрывать коллекцию?

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

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

меры

  1. Введите новые методы , такие как add () и remove (). Каждый должен взять элемент коллекции в качестве аргумента.
  2. Оцените поле коллекции с пустым объектом коллекции, если он еще не инициализирован.
  3. В клиентском коде замените вызовы метода set на код, вызывающий add () или remove ().
  4. По-прежнему в клиентском коде замените вызовы метода get, которые затем изменят коллекцию с помощью add () и remove ().
  5. Измените метод получения : удаление не обязательно, но метод должен возвращать доступную только для чтения копию коллекции. Быстрый способ получить эту семантику — открыть копию массива (ArrayObject :: getArrayCopy () или Collection :: toArray ()) или объект Traversable (ArrayObject :: getIterator ()).

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

Вы можете добавить дополнительные методы, такие как clear () для удаления всех элементов или removeByPosition ($ elementNumber); Суть в том, чтобы предоставить более детальные операции, которые описывают доступные операции коллекции, вместо всеобъемлющего установщика / получателя, который всегда будет отталкивать логику в клиентском коде.

пример

В исходном состоянии есть установщик и получатель для содержащего объекта. Мы используем ArrayObject для выполнения этого рефакторинга над объектом вместо array (), который является примитивным типом, передаваемым по значению; процедура по-прежнему действительна для коллекций Dotrine или других общих объектов коллекций, взятых из библиотек или реализованных самостоятельно.

<?php
class EncapsulateCollection extends PHPUnit_Framework_TestCase
{
    public function testCollectionCanBePopulatedAndInspected()
    {
        $user = new User();
        $groups = new ArrayObject(array(
            new Group('sysadmins'),
            new Group('developers'),
            new Group('economists')
        ));
        $user->setGroups($groups);
        $this->assertEquals(3, count($user->getGroups()));
    }
}

class User
{
    private $groups;

    public function setGroups(ArrayObject $groups)
    {
        $this->groups = $groups;
    }

    public function getGroups()
    {
        return $this->groups;
    }
}

/**
 * A simple Value Object in this example, little more than a string.
 */
class Group
{
    private $name;

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

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

На этом этапе мы также инициализируем поле с пустым экземпляром.

class User
{
    private $groups;

    public function __construct()
    {
        $this->groups = new ArrayObject();
    }

    public function addGroup(Group $group)
    {
        $this->groups->append($group);
    }

    public function setGroups(ArrayObject $groups)
    {
        $this->groups = $groups;
    }

    public function getGroups()
    {
        return $this->groups;
    }
}

Теперь мы заменим использование сеттера вызовами add (). Затем сеттер может быть удален.

<?php
class EncapsulateCollection extends PHPUnit_Framework_TestCase
{
    public function testCollectionCanBePopulatedAndInspected()
    {
        $user = new User();
        $user->addGroup(new Group('sysadmins'));
        $user->addGroup(new Group('developers'));
        $user->addGroup(new Group('economists'));
        $this->assertEquals(3, count($user->getGroups()));
    }
}

class User
{
    private $groups;

    public function __construct()
    {
        $this->groups = new ArrayObject();
    }

    public function addGroup(Group $group)
    {
        $this->groups->append($group);
    }

    public function getGroups()
    {
        return $this->groups;
    }
}

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

class User
{
    private $groups;

    public function __construct()
    {
        $this->groups = new ArrayObject();
    }

    public function addGroup(Group $group)
    {
        $this->groups->append($group);
    }

    /**
     * @return array
     */
    public function getGroups()
    {
        return $this->groups->getArrayCopy();
    }
}

Мы могли бы вернуться

new ArrayObject($this->groups->getArrayCopy());

в случае, если нам нужно сохранить объект в качестве вывода.