- PVSM.RU - https://www.pvsm.ru -
В этой короткой статье мы рассмотрим, что собой представляют неизменяемые объекты и почему нам следует их использовать. Неизменяемыми называются объекты, чьё состояние остаётся постоянным с момента их создания. Обычно такие объекты очень просты. Наверняка вы уже знакомы с типами enum или примитивами наподобие DateTimeImmutable
. Ниже мы увидим, что если делать простые объекты неизменяемыми, то это поможет избежать определённых ошибок и сэкономить немало времени.
При реализации неизменяемых объектов необходимо:
final
, чтобы его нельзя было переопределить при добавлении методов, изменяющих внутреннее состояние.private
, чтобы опять же их нельзя было изменить.Если в одном месте изменить объект, то в другом могут проявиться нежелательные побочные эффекты, которые с трудом поддаются отладке. Это может произойти где угодно: в сторонних библиотеках, в структурах языка и т. д. Использование неизменяемых объектов позволит избежать подобных неприятностей.
Итак, в чём заключаются преимущества правильно реализованных неизменяемых объектов:
Примечание: неизменяемость всё же можно нарушить с помощью «отражений», сериализации/десериализации, биндинга анонимных функций или магических методов. Однако всё это довольно непросто реализовать и вряд ли будет использовано случайно.
Перейдём к примеру неизменяемого объекта:
<?php
final class Address
{
private $city;
private $house;
private $flat;
public function __construct($city, $house, $flat)
{
$this->city = (string)$city;
$this->house = (string)$house;
$this->flat = (string)$flat;
}
public function getCity()
{
return $this->city;
}
public function getHouse()
{
return $this->house;
}
public function getFlat()
{
return $this->flat;
}
}
После того как создан, этот объект уже не меняет состояние, поэтому его можно считать неизменяемым.
Давайте теперь разберём ситуацию с переводом денег на счетах, в которой отсутствие неизменяемости приводит к ошибочным результатам. У нас есть класс Money
, который представляет собой некую сумму денег.
<?php
class Money
{
private $amount;
public function getAmount()
{
return $this->amount;
}
public function add($amount)
{
$this->amount += $amount;
return $this;
}
}
Используем его следующим образом:
<?php
$userAmount = Money::USD(2);
/**
* Марк собирается отправить Алексу 2 доллара. Комиссия составляет 3%,
* и мы прибавляем её к основному переводу.
*/
$processedAmount = $userAmount->add($userAmount->getAmount() * 0.03);
/**
* Получаем с карты Марка для последующего перевода 2 доллара + 3% комиссии
*/
$markCard->withdraw($processedAmount);
/**
* Отправляем Алексу 2 доллара
*/
$alexCard->deposit($userAmount);
Примечание: тип float здесь применён только для простоты примера. В реальной жизни для выполнения операции с необходимой точностью вам нужно будет использовать расширение bcmath или какие-то другие библиотеки вендоров.
Всё должно быть в порядке. Но в связи с тем, что класс Money
изменяемый, вместо двух долларов Алекс получит 2 доллара и 6 центов (комиссия 3%). Причина в том, что $userAmount
и $processedAmount
ссылаются на один и тот же объект. В данном случае рекомендуется применить неизменяемый объект.
Вместо модифицирования существующего объекта необходимо создать новый либо сделать копию существующего объекта. Давайте изменим приведённый код, добавив в него создание другого объекта:
<?php
final class Money
{
private $amount;
public function getAmount()
{
return $this->amount;
}
}
<?php
$userAmount = Money::USD(2);
$commission = $userAmount->val() * 3 / 100;
$processedAmount = Money::USD($userAmount->getAmount() + $commission);
$markCard->withdraw($processedAmount);
$alexCard->deposit($userAmount);
Это хорошо работает для простых объектов, но в случае сложной инициализации лучше начать с копирования существующего объекта:
<?php
final class Money
{
private $amount;
public function getAmount()
{
return $this->amount;
}
public function add($amount)
{
return new self($this->amount + $amount, $this->currency);
}
}
Используется он точно так же:
<?php
$userAmount = Money::USD(2);
/**
* Марк собирается отправить Алексу 2 доллара. Комиссия составляет 3%,
* и мы прибавляем её к основному переводу.
*/
$processedAmount = $userAmount->add($userAmount->val() * 0.03);
/**
* Получаем с карты Марка для последующего перевода 2 доллара + 3% комиссии
*/
$markCard->withdraw($processedAmount);
/**
* Отправляем Алексу 2 доллара
*/
$alexCard->deposit($userAmount);
В этот раз Алекс получит свои два доллара без комиссии, а с Марка правильно спишут эту сумму и комиссию.
При реализации изменяемых объектов программисты могут допускать ошибки, из-за которых объекты становятся изменяемыми. Очень важно это знать и понимать.
У нас есть изменяемый класс, и мы хотим, чтобы его использовал неизменяемый объект.
<?php
class MutableX
{
protected $y;
public function setY($y)
{
$this->y = $y;
}
}
class Immutable
{
protected $x;
public function __construct($x)
{
$this->x = $x;
}
public function getX()
{
return $this->x;
}
}
У неизменяемого класса есть только геттеры, а единственное свойство присвоено конструктором. На первый взгляд, всё в порядке, верно? Теперь давайте используем это:
<?php
$immutable = new Immutable(new MutableX());
var_dump(md5(serialize($immutable))); // f48ac85e653586b6a972251a85dd6268
$immutable->getX();
var_dump(md5(serialize($immutable))); // f48ac85e653586b6a972251a85dd6268
Объект остался прежним, состояние не изменилось. Прекрасно!
Теперь немного поиграем с Х:
<?php
$immutable->getX()->setY(5);
var_dump(md5(serialize($immutable))); // 8d390a0505c85aea084c8c0026c1621e
Состояние неизменяемого объекта изменилось, так что он на самом деле оказался изменяемым, хотя всё говорило об обратном. Это произошло потому, что при реализации было проигнорировано правило «не хранить ссылки на изменяемые объекты», приведённое в начале этой статьи. Запомните: неизменяемые объекты должны содержать только неизменяемые данные или объекты.
Использование коллекций — явление распространённое. А что, если вместо конструирования неизменяемого объекта с другим объектом мы сконструируем его с коллекцией объектов?
Для начала давайте реализуем коллекцию:
<?php
class Collection
{
protected $elements = [];
public function __construct(array $elements)
{
$this->elements = $elements;
}
public function add($element)
{
$this->elements[] = $element;
}
public function get($key)
{
return isset($this->elements[$key]) ? $this->elements[$key] : null ;
}
}
Теперь воспользуемся этим:
<?php
$immutable = new Immutable(new Collection([new XMutable(), new XMutable()]));
var_dump(md5(serialize($immutable))); // 9d095d565a96740e175ae07f1192930f
$immutable->getX();
var_dump(md5(serialize($immutable))); // 9d095d565a96740e175ae07f1192930f
$immutable->getX()->get(0)->setY(5);
var_dump(md5(serialize($immutable))); // 803b801abfa2a9882073eed4efe72fa0
Как мы уже знаем, лучше не держать изменяемые объекты внутри неизменяемого. Поэтому заменим изменяемые объекты скалярами.
<?php
$immutable = new Immutable(new Collection([1, 2]));
var_dump(md5(serialize($immutable))); // 24f1de7dc42cfa14ff46239b0274d54d
$immutable->getX();
var_dump(md5(serialize($immutable))); // 24f1de7dc42cfa14ff46239b0274d54d
$immutable->getX()->add(10);
var_dump(md5(serialize($immutable))); // 70c0a32d7c82a9f52f9f2b2731fdbd7f
Поскольку наша коллекция предоставляет метод, позволяющий добавить новые элементы, мы можем косвенно менять состояние неизменяемого объекта. Так что при работе с коллекцией внутри неизменяемого объекта удостоверьтесь, что она сама не является изменяемой. Например, убедитесь, что она содержит только неизменяемые данные. И что нет методов, которые добавляют новые элементы, убирают их или иным способом изменяют состояние коллекции.
Другая распространённая ситуация связана с наследованием. Мы знаем, что нужно:
Давайте модифицируем класс Immutable
, чтобы он принимал только Immutable
-объекты.
<?php
class Immutable
{
protected $x;
public function __construct(Immutable $x)
{
$this->x = $x;
}
public function getX()
{
return $this->x;
}
}
Выглядит неплохо… пока кто-то не расширит ваш класс:
<?php
class Mutant extends Immutable
{
public function __construct()
{
}
public function getX()
{
return rand(1, 1000000);
}
public function setX($x)
{
$this->x = $x;
}
}
<?php
$mutant = new Mutant();
$immutable = new Immutable($mutant);
var_dump(md5(serialize($immutable->getX()->getX()))); // c52903b4f0d531b34390c281c400abad
var_dump(md5(serialize($immutable->getX()->getX()))); // 6c0538892dc1010ba9b7458622c2d21d
var_dump(md5(serialize($immutable->getX()->getX()))); // ef2c2964dbc2f378bd4802813756fa7d
var_dump(md5(serialize($immutable->getX()->getX()))); // 143ecd4d85771ee134409fd62490f295
Всё опять пошло не так. Вот поэтому неизменяемые объекты должны быть объявлены как final
, чтобы их нельзя было расширить.
Мы узнали, что такое неизменяемый объект, где он может быть полезен и какие правила необходимо соблюдать при его реализации:
final
, чтобы его нельзя было переопределить при добавлении методов, изменяющих внутреннее состояние.private
, чтобы опять же их нельзя было изменить.Автор: Mail.Ru Group
Источник [1]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/web-razrabotka/121068
Ссылки в тексте:
[1] Источник: https://habrahabr.ru/post/301004/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best
Нажмите здесь для печати.