Код клиента вызывает метод для коллаборатора (делегата) другого объекта, полученного геттером или другой последовательностью вызовов. Hide Delegate — это соблюдение закона Деметры : не разговаривайте с незнакомцами, избегая полагаться на объекты, которые не находятся непосредственно в окружении текущего.
В этом рефакторинге класс делегата скрыт путем репликации используемой части его протокола на класс верхнего уровня; это только один из способов соответствовать закону, а не единственный.
Почему другой закон?
Все мы знаем заклинание частных полей, в котором говорится, что в объектно-ориентированном программировании не должно быть открытых полей . Тем не менее, раскрытие всех частных полей через геттеры не является улучшением.
Я в порядке с getter для почти скаляров, так как вам нужно каким-то образом их выставить (хотя есть альтернативы .) Но проблема с getter в том, что вскоре клиентский код вызовет другой метод для возвращаемого объекта, а затем другой метод на возвращенном объекте предыдущего метода и перемещайтесь по всему графу объекта.
В этом случае клиентский код делает предположение по всему графу, окружающему $ this:
$object = $this->collaborator->getOtherObject(); $anotherObject = $object->getAnotherObject(); $field = $anotherObject->getSomeField(); // do some work with $field
Тестирование кода, который делает все эти предположения, является беспорядком; и любое изменение в одном из этих объектов будет иметь следы этого клиентского кода: изменение имени или подписи getSomeField () влечет за собой изменение в $ this, удаленном объекте. Инкапсуляция — это предотвращение этой ряби.
Мы можем избежать распространения знаний всего графа в одном объекте с помощью делегирования:
(Это графическое представление объектов вдохновлено
растущим объектно-ориентированным программным обеспечением .)
меры
В терминологии Фаулера клиентский объект вызывает объект сервера , который возвращает делегат, который был таким образом выставлен. В исходном состоянии другие методы вызываются для делегата.
- Создать метод делегирования на объекте сервера; сервер уже ссылается на делегата, который уже должен быть там на $ this, и поэтому метод делегирования должен быть простым для записи.
- Настройте клиентские вызовы, чтобы использовать новый метод сервера.
- Выполнять тесты на функциональном уровне ; модульные тесты клиента должны измениться (или быть введены: теперь вы можете легко использовать 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.