Статьи

Практический рефакторинг PHP: Hide Delegate

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

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

Почему другой закон?

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

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

В этом случае клиентский код делает предположение по всему графу, окружающему $ this:

$object = $this->collaborator->getOtherObject();
$anotherObject = $object->getAnotherObject();
$field = $anotherObject->getSomeField();
// do some work with $field

Тестирование кода, который делает все эти предположения, является беспорядком; и любое изменение в одном из этих объектов будет иметь следы этого клиентского кода: изменение имени или подписи getSomeField () влечет за собой изменение в $ this, удаленном объекте. Инкапсуляция — это предотвращение этой ряби.

Мы можем избежать распространения знаний всего графа в одном объекте с помощью делегирования:

(Это графическое представление объектов вдохновлено
растущим объектно-ориентированным программным обеспечением .)

 

меры

В терминологии Фаулера клиентский объект вызывает объект сервера , который возвращает делегат, который был таким образом выставлен. В исходном состоянии другие методы вызываются для делегата.

  1. Создать метод делегирования на объекте сервера; сервер уже ссылается на делегата, который уже должен быть там на $ this, и поэтому метод делегирования должен быть простым для записи.
  2. Настройте клиентские вызовы, чтобы использовать новый метод сервера.
  3. Выполнять тесты на функциональном уровне ; модульные тесты клиента должны измениться (или быть введены: теперь вы можете легко использовать Test Double, потому что для изоляции клиента от реального графа не требуется альтернативный граф, имитирующий весь реальный граф, просто Test Double для сервера Фасад).

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

пример

В исходном состоянии делегат передается клиенту, классу UserController.

<?php
class HideDelegate extends PHPUnit_Framework_TestCase
{
    /**
     * We are imagine an User is registered and an activation number is sent
     * via mail to him. This test regards the activation process enacted
     * by a user following a link in the mail.
     */
    public function testUserIsActivatedIfActivationTokenIsCorrect()
    {
        $userCollection = new UserCollection(array(
            'giorgio' => new User('giorgio', 42)
        ));
        $controller = new UserController($userCollection);
        $controller->activation(array(
            'name' => 'giorgio',
            'activationNumber' => 42
        ));
        $this->assertTrue($userCollection->getUser('giorgio')->isActive());
    }
}

/**
 * The Delegate: This class contains the business logic.
 */
class User
{
    private $name;
    private $activationNumber;
    private $active = false;

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

    public function activate($number)
    {
        if ($this->activationNumber == $number) {
            $this->active = true;
        }
    }

    public function isActive()
    {
        return $this->active;
    }
}

/**
 * The Server: this class hands out a User instance.
 */
class UserCollection
{
    private $users;

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

    public function getUser($name)
    {
        return $this->users[$name];
    }
}

/**
 * The Client: this class gets an User instance and calls a method on it,
 * violating the Law of Demeter.
 */
class UserController
{
    private $userCollection;

    public function __construct(UserCollection $collection)
    {
        $this->userCollection = $collection;
    }

    public function activation(array $request)
    {
        if (!isset($request['name'])) {
            throw new InvalidArgumentException('No user specified.');
        }
        if (!isset($request['activationNumber'])) {
            throw new InvalidArgumentException('No activation number.');
        }
        $user = $this->userCollection->getUser($request['name']);
        $user->activate($request['activationNumber']);
    }
}

Мы добавили метод делегирования, чтобы Server не был вынужден возвращать объект User для этого варианта использования:

/**
 * The Server: this class hands out a User instance.
 */
class UserCollection
{
    private $users;

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

    public function getUser($name)
    {
        return $this->users[$name];
    }

    public function activationOfUser($name, $activationNumber)
    {
        $this->users[$name]->activate($activationNumber);
    }
}

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

class UserController
{
    private $userCollection;

    public function __construct(UserCollection $collection)
    {
        $this->userCollection = $collection;
    }

    public function activation(array $request)
    {
        if (!isset($request['name'])) {
            throw new InvalidArgumentException('No user specified.');
        }
        if (!isset($request['activationNumber'])) {
            throw new InvalidArgumentException('No activation number.');
        }
        $this->userCollection->activationOfUser($request['name'], $request['activationNumber']);
    }
}

Мы могли бы продолжить, разбив тест на два модульных теста: один, который проверяет логику UserCollection, и другой, который проверяет делегирование UserController в Test Double of UserCollection.