Статьи

Практический PHP-рефакторинг: удалите Middle Man

На прошлой неделе мы говорили о Hide Delegate как о средстве соответствия закону Деметры и о том, что нужно избегать непрерывной очистки графа объектов во всех направлениях. Другой способ следовать этому закону состоит не в делегировании, а в реорганизации ссылок на поля на другие объекты: рефакторинг Remove Middle Man.

Мы будем использовать ту же терминологию, что и для рефакторинга Hide Delegate: один или несколько Клиентов получают доступ к объекту Delegate, передавая объект Server, который возвращает ссылку на него. В этом случае Сервер также называется Middle Man.

С помощью функции «Удалить посредника» мы начинаем ссылаться непосредственно на делегата от клиента с помощью частного поля . Рефакторинг отделяет контракт с делегатом от остальной части соседнего графика. Недостаток Hide Delegate заключался в том, что контракт (например, общедоступные методы) Сервера менялся несколько раз, чтобы соответствовать части делегата.

Некоторые ошибки

Удаление Middle Man не обязательно означает, что класс Middle Man или этот конкретный объект должен быть удален : только то, что Клиент должен ссылаться на Delegate напрямую с полем, а не проходить через геттер или набор вызовов Middle Man. В свою очередь, Средний Человек может быть упрощен только путем удаления методов, которые больше не называются.

Сервер должен быть фактически удален, когда больше нет звонков от Клиентов; но это не проблема, если Клиенты теперь получают и Сервер, и Делегат в качестве соавторов.

меры

  1. Создайте геттер для объекта Delegate на сервере.
  2. Замените вызовы от Клиентов к Серверу вызовами на объекте, возвращенном получателем.
  3. Внедрите непосредственно делегата в клиенты, обновив их фабрики или их творческий код.

Запустите тесты на функциональном или сквозном уровне для проверки регрессии, соответственно обновив модульные тесты. Дополнительным шагом является удаление класса Server, если у него в настоящее время нет других обязанностей, кроме предоставления доступа к Delegate.

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

пример

Мы продолжаем на примере статьи Hide Delegate — на этот раз мы отбрасываем посредника UserCollecton и вводим User прямо в наш контроллер.

В исходном состоянии UserController проходит через UserCollection, чтобы добраться до пользователя:

<?php
class RemoveMiddleMan extends PHPUnit_Framework_TestCase
{
    /**
     * In the previous example, we were delegating the activation logic to the
     * UserCollection, modeling only HTTP request-related concerns into our 
     * UserController.
     */
    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 hides 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);
    }
}

/**
 * The Client.
 */
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']);
    }
}

Контроллер может быть модифицирован для вызова методов объекта User:

/**
 * The Client now accesses a User object.
 */
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->getUser($request['name'])->activate($request['activationNumber']);
    }
}

И, наконец, мы можем упростить картинку, добавив User в UserController и удалив UserCollection:

<?php
class RemoveMiddleMan extends PHPUnit_Framework_TestCase
{
    /**
     * The User is now directly injected (it may be found at construction time.)
     * We maintain the reference to the object in the test for verification
     * purposes; this test has become a unit test and we could even use a Mock.
     */
    public function testUserIsActivatedIfActivationTokenIsCorrect()
    {
        $controller = new UserController($user = new User('giorgio', 42));
        $controller->activation(array(
            'activationNumber' => 42
        ));
        $this->assertTrue($user->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 Client now only refers to an User object since it does not have any 
 * other use for UserCollection.
 */
class UserController
{
    private $user;

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

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