PHP RFC: Immutable классы и свойства

Введение

В данном RFC дается введение в immutable (неизменяемые) классы и свойства. На данный момент добиться неизменности для классов и их свойств можно только с помощью инкапсуляции. Из-за этого пользовательские приложения используют сторонние библиотеки или прибегают к пользовательским реализациям, но до сих пор нет простого способа добиться запрета на изменения для классов и переменных. Введение этой функции поможет принести одно унифицированное решение этой проблемы, а также устранит ненужную логику из пользовательских приложений. Если этот механизм будет введен, разработчики смогут быть уверены, что их код не будет подвержен каким-либо побочным эффектам, что означает, что состояние объекта не сможет быть изменено без уведомления разработчиков. Это особенно полезно при работе с параллелизмом, где мы можем гарантировать, что потоки всегда будут считывать одно и то же значение.

Плюсы:

  • Неизменность гарантируется языком, а не пользовательской реализацией.
  • Программирование без "побочных эффектов".
  • Безопасная многопоточность.
  • Появится возможность широко использовать DTO (Data Transfer Object).
  • Свойства класса (properties) могут быть public. Отпадает необходимость в getters и setters.
  • (возможно и другие приемущества)

Минусы

  • На данный момент невозможно использовать массивы для immutable переменных.

Предложение

Неизменяемые Классы

Класс, определенный как неизменный, будет подразумевать неизменность по всем его свойствам по умолчанию. После создания объекта невозможно изменить состояние объекта из любой области.

immutable class Email {
  public $email;

  public function __construct ($email) {
    $this->email = $email;
  }
}

$email = new Email("[email protected]");
$email->email = "[email protected]" // Call will result in Fatal Error

Изменения в наследовании заключаются в добавлении ограничений при расширении неизменяемого класса - дочерний класс также должен быть неизменным, чтобы сохранить неизменность по всему объекту.

immutable class Foo{}
class Bar extends Foo{} // Will result in Fatal Error

Неизменяемые свойства

Классы имеют возможность принудительно обеспечивать неизменность только подмножеством свойств, если это необходимо, и в этом случае неизменяемость будет подразумеваться только для свойств, объявленных неизменяемыми.

class User {
  private $id;
  public immutable $email;

  public function __construct ($id, $email) {
    $this->id = $id;
    $this->email = $email;
  }
}

Если неизменяемое свойство содержит объект, чтобы сохранить неизменность, объект, который присваивается неизменяемому свойству, также должен быть неизменным.

immutable class Email{}

class User {
  public immutable $email;

  public function __construct (Email $email) {
    $this->email = $email;
  }
}

Ресурсы не могут быть присвоены неизменяемым свойствам из-за того, что ресурсы по своей природе не являются неизменными.

class File {
  public immutable $handle;

  public function __construct ($handle) {
    $this->handle = $handle;
  }
}

$file = new File(fopen('file.txt'));

Массивы не могут быть отнесены к неизменяемым свойствам.

class A {
  public immutable $x;

  public function __construct ($x) {
    $this->x = $x;
  }
}

$a = new A(['foo']);

Если неизменяемое свойство родительского класса переопределяется в дочернем классе, оно должно быть объявлено неизменным. Поскольку целью этого RFC являются "Immutable Objects", использование статических свойств в таких классах будут приводить к ошибке компиляции.

Ссылки (References)

Присвоение ссылки на неизменяемые свойства приведет к ошибке, так как объект теряет контроль над свойствами и неизменяемость может быть нарушена.

immutable class Email {
  public $email;

  public function __construct ($email) {
    // validation

    $this->email = $email;
  }
}

$email = new Email("[email protected]");
$emailRef = &$email->email;
$emailRef = "[email protected]" // Call will result in Fatal Error

Сравнение (Comparison)

Идентичность неизменяемого объекта основана на его содержимом. Таким образом, две неизменяеммые переменные идентичны, если они одного типа и содержат одно и то же значение.

immutable class Email {
  public $email;

  public function __construct ($email) {
    // validation

    $this->email = $email;
  }
}

$email1 = new Email("[email protected]");
$email2 = new Email("[email protected]");
var_dump($email1 === $email2); // bool(true)

Примеры

Обратите внимание, что в приведенном выше примере замена методов getters и seters на public свойства является необязательным. Их (getters и setters) просто больше нет нужны использовать, так как неизменные экземпляры класса являются замороженными и вызывают исключение (exception) при попытке записи.

Каждый пример показывает, где важно состояние внутреннего объекта. Любые ссылки на объекты, переданные в конструктор неизменяемых классов, не могут быть ссылками на скаляры или могут быть неизменяемыми экземплярами класса. Это дает высокую гарантию, что вы сохраните внутреннее состояние без изменений на все время.

Деньги

Money Pattern, определенный Мартином Фаулером и опубликованный в «Шаблонах архитектуры корпоративных приложений», представляет собой отличный способ представления пар единиц измерения стоимости. Он называется «Шаблон денег», потому что он появился в финансовом контексте.

class Currency {

  private $centFactor;
  private $stringRepresentation;

  private function __construct(int $centFactor, string $stringRepresentation) {
    $this->centFactor = $centFactor;
    $this->stringRepresentation = $stringRepresentation;
  }

  public function getCentFactor() : int {
    return $this->centFactor;
  }

  public function getStringRepresentation() : string {
    return $this->stringRepresentation;
  }

  public static function USD() : Currency {
    return new self(100, 'USD');
  }

  public static function EUR() : Currency {
    return new self(100, 'EUR');
  }
}

class Money {

  private $amount;
  private $currency;

  public function __construct($amount, Currency $currency) {
    $this->amount = $amount;
    $this->currency = $currency;
  }

  public function getAmount() : float {
    return $this->amount;
  }

  public function getCurrency() : Currency {
    return $this->currency;
  }

  public function add(Money $other) : Money {
    $this->ensureSameCurrencyWith($other);
    return new Money($this->amount + $other->getAmount(), $this->currency);
  }

  public function subtract(Money $other) {
    $this->ensureSameCurrencyWith($other);
    return new Money($this->amount - $other->getAmount(), $this->currency);
  }

  public function multiplyBy($multiplier, $roundMethod = PHP_ROUND_HALF_UP) {
    $product = round($this->amount * $multiplier, 0, $roundMethod);
    return new Money($product, $this->currency);
  }

  private function ensureSameCurrencyWith(Money $other) {
    if ($this->currency != $other->getCurrency()) {
      throw new \Exception("Both Moneys must be of same currency");
    }
  }
}

$oneThousand = new Money(1000, Currency::USD());

После того как мы отрефакторим классы выше с использование "неизменности", код выше будет выглядеть так:

immutable class Currency {

  /** @var int */
  public $centFactor;
  /** @var string */
  public $stringRepresentation;

  private function __construct(int $centFactor, string $stringRepresentation) {
    $this->centFactor = $centFactor;
    $this->stringRepresentation = $stringRepresentation;
  }

  public static function USD() : Currency {
    return new self(100, 'USD');
  }

  public static function EUR() : Currency {
    return new self(100, 'EUR');
  }
}

immutable class Money {

  /** @var float */
  public $amount;
  /** @var Currency */
  public $currency;

  public function __construct(float $amount, Currency $currency) {
    $this->amount = $amount;
    $this->currency = $currency;
  }

  public function add(Money $other) : Money {
    $this->ensureSameCurrencyWith($other);
    return new Money($this->amount + $other->amount, $this->currency);
  }

  public function subtract(Money $other) {
    $this->ensureSameCurrencyWith($other);
    return new Money($this->amount - $other->amount, $this->currency);
  }

  public function multiplyBy($multiplier, $roundMethod = PHP_ROUND_HALF_UP) {
    $product = round($this->amount * $multiplier, 0, $roundMethod);
    return new Money($product, $this->currency);
  }

  private function ensureSameCurrencyWith(Money $other) {
    if ($this->currency != $other->currency) {
      throw new \Exception("Both Moneys must be of same currency");
    }
  }
}

$oneThousand = new Money(1000, Currency::USD());

Нет необходимости в геттерах, потому что наш объект является immutable, и ни одно из его свойств больше не может быть перезаписанно. Все свойства принимают скалярные значения или объекты, которые реализуют неизменяемый класс, поэтому имеется высокая гарантия, что такой объект Money сохранит свое внутреннее состояние нетронутым.

Опубликовано:

Категории: Статьи