Методы получения и установки являются одним из первых этапов абстракции, который продуман для открытых полей в объектно-ориентированном программировании. Однако парадигма никогда не заключалась в инкапсуляции свойств путем предоставления специального механизма чтения / записи через методы, а в отношении объектов, отвечающих на сообщения.
Другими словами, инкапсуляция — это (не только, но и) возможность изменять частные поля. Если вы пишете геттеры и сеттеры, вы вводите негерметичную абстракцию над частными полями , так как с ними связаны имена и количество ваших открытых методов. Они больше не являются частными : например, в классах обслуживания я передаю зависимости для закрытых методов в конструкторе, и клиентский код никогда не изменяется при изменении этих зависимостей. При использовании методов получения и установки добавление, удаление или переименование полей влияет на контракт класса.
В этой статье мы рассмотрим этот класс User Entity и рассмотрим множество способов удаления методов получения и установки. Выбор между ними зависит, главным образом, от того, для чего они вам нужны: именно клиентский код должен решать, чего хотят контракты от этих классов.
<?php
class User
{
private $nickname;
private $password;
private $location;
private $favoriteFood;
private $active;
private $activationKey;
public function setNickname($nickname)
{
$this->nickname = $nickname;
}
public function getNickname()
{
return $nickname;
}
public function setPassword($password)
{
$this->password = $password;
}
public function getPassword()
{
return $this->password;
}
// ... you get the picture: other 8 getters or setters
}
Различные техники упорядочены по сложности. Как всегда, я выражаю варианты использования для класса User через контрольный пример. Весь код на Github .
Конструктор
Во-первых, мы можем передать некоторые поля в конструкторе . Если мы не предоставим поле, значение будет неизменным, и клиентский код никогда не узнает, что это значение существует.
Например, наш пользователь имеет неизменный ник, который также служит первичным ключом:
public function testPassWriteOnlyDataInTheConstructor()
{
$user = new User('giorgiosironi');
// should not explode
//removed the setter as it cannot be changed
}
class User
{
public function __construct($nickname, $activationKey = '')
{
$this->nickname = $nickname;
$this->activationKey = $activationKey;
}
}
Информационный Эксперт
Далее у нас есть шаблон информационного эксперта: назначить операцию объекту с наибольшим знанием для его выполнения . Если вы сделаете это, вам не нужно будет открывать приватные поля через методы получения и установки, так как вы будете моделировать некоторое поведение как код в этом классе, который может видеть приватные поля.
Например, когда мы регистрируем пользователя, мы хотим активировать его с помощью подтверждения по электронной почте, поэтому мы отправляем ключ активации по почте. Но чтобы проверить это, нам не нужно извлекать значение из объекта User.
// Information Expert
/**
* @expectedException InvalidArgumentException
*/
public function testAnUserIsActivated()
{
$user = new User('giorgiosironi', 'ABC');
$user->activate('AB');
}
class User
{
public function activate($key)
{
if ($this->activationKey === $key) {
$this->active = true;
return;
}
throw new InvalidArgumentException('Key for activation is incorrect.');
}
}
Этот стиль является примером принципа « Говори, а не спрашивай» : мы просим нашего Пользователя сделать что-то вместо того, чтобы запрашивать информацию и делать это самостоятельно.
Двойная отправка
Помещать поведение в класс Entity хорошо, но иногда для работы нужна некоторая внешняя зависимость, например, механизм входа в систему, требующий хранилища для идентификации пользователя (обычно сеанса). У нас есть два типа соединения, чтобы решить здесь:
- статический: класс User не должен зависеть от какого-либо другого класса инфраструктуры , который, в свою очередь, ссылается на базу данных или хранилище сеанса.
- время выполнения: класс User не может содержать ссылку на объект инфраструктуры , например, потому что мы хотим сериализовать его или создать его просто с новым оператором, или наш ORM не поддерживает внедрение соавторов.
Первые проблемы решаются путем введения интерфейса, реализованного классом инфраструктуры; во-вторых, передавая зависимость через Double Dispatch, а не через конструктор, как мы делаем с классами обслуживания.
public function testUsersLogin()
{
$user = new User('giorgiosironi');
$user->setPassword('gs'); // will be removed in next tests
// in reality, we would use a SessionLoginAdapter or something like that
$loginAdapterMock = $this->getMock('LoginAdapter');
$loginAdapterMock->expects($this->once())
->method('storeIdentity')
->with('giorgiosironi');
$user->login('gs', $loginAdapterMock);
}
class User
{
public function login($password, LoginAdapter $loginAdapter)
{
if ($this->password == $password) {
$loginAdapter->storeIdentity($this->nickname);
return true;
} else {
return false;
}
}
}
Еще пример « Скажи, не спрашивай» , но более реальный мир сейчас.
Команда и наборы изменений
Вы действительно должны поместить данные в этот объект: пользователь только что скомпилировал форму, и вы должны ввести эти входные значения. Итак, как мы определим эту операцию? Как атомарный вызов метода, я передаю то, что я называю Changeset, но это специализация команды (не шаблон команды, а CommandQueryResponsibilitySegregation). В простейших случаях это просто объект значения или объект передачи данных без поведения.
public function testCommandForChangingPassword()
{
$user = new User('giorgiosironi');
$passwordChange = new ChangeUserPassword('', 'gs');
$user->handle($passwordChange);
$this->assertEquals('gs', $user->getPassword()); //deprecated, will be removed in next tests
}
class User
{
public function handle($command)
{
if ($command instanceof ChangeUserPassword) {
$this->handleChangeUserPassword($command);
}
if ($command instanceof SetUserDetails) {
$this->handleSetUserDetails($command);
}
// support other commands here...
}
private function handleChangeUserPassword(ChangeUserPassword $command)
{
if ($command->getOldPassword() == $this->password) {
$this->password = $command->getNewPassword();
} else {
throw new Exception('The old password is not correct.');
}
}
}
Подумайте об этом: вам придется куда-то класть эти геттеры и сеттеры; Лучше всего поместить их в объект, который является структурой данных, чем в вашей сущности. Сюда:
- вы будете связаны с текущими полями только для этой конкретной операции, а не когда вы будете передавать пользователя.
- Вы дадите понять, что поддерживаете только полную операцию обновления , и вызывать изолятор не нормально.
На самом деле в PHP вы можете просто использовать массив в качестве Changeset, но класс обеспечивает более строгий контракт. Также открытые поля не являются действительными для контракта, так как PHP не выдаст никакой ошибки, если вы назначите несуществующее поле в объекте Changeset.
Рендеринг на холсте
В списке рассылки «Растущее объектно-ориентированное программное обеспечение» недавно обсуждалось, как эмулировать геттеры с помощью обратных вызовов. Это решение является отражением нашего аргумента Changeset, используемого для извлечения данных вместо их обновления.
public function testCanvasForRenderingAnObject()
{
$user = new User('giorgiosironi');
$detailsSet = new SetUserDetails('Italy', 'Pizza'); //THIS may have set/get
$user->handle($detailsSet);
// canvas can also be a form, or xml, or json...
$canvas = new HtmlCanvas('<p>{{location}}</p><p>{{favoriteFood}}</p>');
$user->render($canvas);
$this->assertEquals('<p>Italy</p><p>Pizza</p>', (string) $canvas);
}
class User
{
public function render(Canvas $canvas)
{
$canvas->nickname = $this->nickname;
$canvas->location = $this->location;
$canvas->favoriteFood = $this->favoriteFood;
}
}
Опять же, холст скрыт за интерфейсом и может быть чем угодно: представлением HTML, формой, генератором каналов JSON или RSS …
CQRS
В сегрегации ответственности Command-Query вы используете ORM для сопоставления ваших объектов в базе данных и заполняете экраны отчетов, запрашивая его напрямую или даже запрашивая другое хранилище, которое непрерывно перестраивается из вашей основной базы данных.
Я не знаю ни одной реализации CQRS в PHP, но этот механизм обещает, по крайней мере, исключить геттеры, поскольку ваши доменные объекты будут только для записи.
Вывод
Полный код в этом Github-хранилище , как всегда.
Теперь у тебя нет оправданий: иди и немедленно брось один из своих добытчиков и установщиков. Ваш код вдохнет немного свежего воздуха.