Незмінні об'єкти в PHP

В цій короткій статті ми розглянемо, що собою представляють незмінні об'єкти і чому нам слід їх використовувати. Незмінними називаються об'єкти, чий стан залишається постійним з моменту їх створення. Зазвичай такі об'єкти дуже прості. Напевно ви вже знайомі з типами enum або примітивами зразок
DateTimeImmutable
. Нижче ми побачимо, що якщо робити прості об'єкти незмінними, то це допоможе уникнути певних помилок і заощадити чимало часу.

При реалізації незмінних об'єктів необхідно:

  • Оголосити клас
    final
    , щоб його не можна було змінити при додаванні методів, які змінюють внутрішній стан.
  • Оголосити властивості
    private
    , щоб знову ж таки їх не можна було змінити.
  • Уникати сеттерів і використовувати конструктор для завдання параметрів.
  • Не зберігати посилання на змінювані об'єкти або колекції. Якщо ви всередині незмінного об'єкта зберігайте колекцію, то вона теж повинна бути незмінною.
  • Перевіряти, що, якщо вам потрібно модифікувати незмінний об'єкт, ви робили його копію, а не переиспользовали існуючий.
Якщо в одному місці змінити об'єкт, то в іншому можуть проявитися небажані побічні ефекти, які важко піддаються налагодження. Це може відбутися де завгодно: у сторонніх бібліотеках, у структурах мови і т. д. Використання незмінних об'єктів дозволить уникнути подібних неприємностей.

Отже, в чому полягають переваги правильно реалізованих незмінних об'єктів:

  • Стан програми стає більш передбачуваним, тому що менша кількість об'єктів змінюють власні стану.
  • Завдяки тому що стають неможливі ситуації з розділяються посиланнями (shared references), спрощується налагодження.
  • Незмінні об'єкти зручно застосовувати для створення паралельно виконуваних програм (у цій статті не розглядається).
Примітка: незмінюваність все ж можна порушити за допомогою «відображень», серіалізації/десеріалізації, биндинга анонімних функцій або магічних методів. Однак усе це досить непросто реалізувати і навряд чи буде використано випадково.

Перейдемо до прикладу незмінного об'єкта:

<?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
    , щоб знову ж таки їх не можна було змінити.
  • Уникати сеттерів і використовувати конструктор для завдання параметрів.
  • Не зберігати посилання на змінювані об'єкти або колекції. Якщо ви всередині незмінного об'єкта зберігайте колекцію, то вона теж повинна бути незмінною.
  • Перевіряти, що, якщо вам потрібно модифікувати незмінний об'єкт, ви робили його копію, а не переиспользовали існуючий.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.