Код клиента вызывает метод для коллаборатора (делегата) другого объекта, полученного геттером или другой последовательностью вызовов. 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.