PHP и неизменяемость

Это перевод оригинальной статьи Саймона Холивелла PHP and Immutability. Продолжение — в другом посте.

Как у динамического языка со слабой типизацией, у PHP нет встроенной концепции неизменяемости (immutability). Конечно, все мы знаем заслуженные  define()  и КОНСТАНТЫ, но они ограничены в функционале. Хоть PHP и поставляется с как минимум одним неизменяемым классом в своей стандартной библиотеке, DateTimeImmutable , но не существует какого-то внятного способа создавать собственные неизменяемые объекты.

Константы

Единственное неизменяемое хранилище данных, доступное в PHP, это константы, которые устанавливаются в PHP-классе при помощи или define()  , или ключевого слова const . Однако, имеется существенная разница между этимя двумя опциями.

Константы класса ( const ) присваиваются при написании кода, и они не могут быть изменены в ходе выполнения программы. Это свойство делает из них наиболее неизменяемую пользовательскую структуру в PHP. Если б вы захотели присвоить другое значение по какому-то условию или присвоить значение другой переменной, считайте, что вам не повезло.


Традиционные константы, объявляемые с помощью define() , могут быть проинициализированы по условию и присвоены из других переменных, но, как только значение присвоено, с этого момента они уже неизменяемы.


Как можно видеть, если вы попытаетесь изменить константу, то получите уведомление, при этом само значение останется неизменным. Проверить, не была ли константа уже определена, вы можете функцией defined()  — обратите внимание на d на конце!

В общем, это отлично работает со скалярами. Однако, если вы хотите хранить любой тип структуры массива как незменяюмую сущность, то потребуется либо использовать константу класса, либо вы будете разочарованы любой версией PHP вплоть до PHP 7.


Если же вы чувствуете себя круто, потому что работаете с PHP 7, тогда у меня плохие новости — с массивами все получится, но с объектами не особо!


В любом случае, есть и другие причины, по которым вы, возможно, не захотите использовать константу.

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

Традиционные константы определены глобально. То ли это, что вам нужно? Наверняка вам бы лучше подошла соответствующая переменная, которую вы бы смогли передавать между функциями и которая следовала бы правилам зон видимости в PHP.

Оба эти метода дают в итоге странный синтаксис, тогда как намного более простой синтаксис для переменных был бы более подходящим и предсказуемым.

Произвольные неизменяемые классы

Вообще, вы можете писать собственные неизменяемые сущности, применяя некоторые простые и хитрые техники на PHP. Мы не будем предъявлять слишком больших требований к данным, чтобы следовать примерам в статье было проще. Для иллюстрации я воспользуюсь профессиональными скейтбордистами и трюками, которые они привнесли в мир.

В качестве первого примера вот простой неизменяемый класс, который принимает значения в качестве параметров конструктора. Я назвал этот класс Immutable , но, возможно, это плохая идея, так как, может, это слово когда-нибудь станет ключевым в языке. Сейчас, вообще, нет никакой разницы, какое имя мы присвоим этому классу.

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

Чтобы обеспечить внешний доступ к значениям, привязанным к объекту Immutable, можно просто написать открытый ( public ) метод, который возвращает требуемое значение. В нашем примере есть два свойства класса, к которым мы хотели бы иметь доступ, так что я добавил getSkater() , чтоб получать имя скейтбордиста, и getTrick() , чтоб получать название трюка, который он придумал.

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

Избавляемся от изменчивости

Важно, что к Immutable не было добавлено никаких методов, которые бы позволили менять значения внутри класса. Очевидно, вы не захотите писать в классе открытых методов наподобие setSkater() или setTrick()  , поскольку они позволили бы клиенту изменять наш неизменяемый класс.

Теперь, когда у вас есть рабочий пример и понимание неизменяемого объекта в PHP, настало время объявить, что существует еще пара других подводных каменей. Прошу прощения! Но вы же не ждали, что будет легко, верно?

Блокируем обходные пути

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

Перезаписываем изменяемую переменную

Самый легкий способ обойти неизменяемость в PHP — просто перезаписать переменную, которой присвоен неизменяемый объект класса.

При запуске кода $a $a  будет без особых уведомлений перезаписано новым значением new TrickRecord  вместо того, чтобы вызвать ошибку, как мы могли надеяться. Это привет из истории PHP, такое поведение ожидаемо. Конечно, если бы вы были злонамеренным разработчиком, то вам следовало бы убедиться, что они не сделали TrickRecord неизменяемым!

Что до записи, то не существует никакого способа в PHP ее предотвратить. Как мы уже знаем, объекты не могут быть присвоены константе (только скаляры, а в PHP 7 — еще и массивы). В JavaScript у нас теперь есть ключевое слово const  , которое может запретить запись, но в PHP нет такого счастья.

Единственный способ предотвратить запись — это использовать явное указание типов всюду, где ожидается Immutable , но, как мы еще увидим, существуют способы обойти и это!

Множественные вызовы конструктора

Еще один способ нарушить неизменяемость объекта — просто вызвать конструктор снова, но уже с другими параметрами.

Очевидно, это проблема, к тому же очень неприятная. К счастью, мы можем обойти эту проблему путем включения в наш класс некоего флага, чтобы избежать изменяемости. Я бы сказал, что Daewon Song — великий скейтбордист, однако, он не изобретал frontside 540 , так что мы не можем это разрешать.

С внесением этого изменения множественные вызовы конструктора больше не будут разрешены, и вызовут фатальное исключение.

Расширяем класс Immutable

Также разработчик может написать класс, расширяющий Immutable . Это даст возможность перегрузить его конструктор своим собственным.

Как только у него будет свой конструктор, он сможет присвоить $skater  и $trick  каким-то своим свойствам класса. Это поломает неизменяемость класса, так как открытые ( public ) свойства могут быть изменены. Подобным образом он сможет добавить новые методы, чтоб присваивать свойствам произвольные значения.

Поскольку NaughtyDev  наследует от Immutable  , любая проверка типа, осуществленная на экземпляре класса, пройдет успешно. Здесь хитрый разработчик преедает нам экземпляр NaughtyDev  , где наш код хочет Immutable , но мы все равно окажемся умнее.

Это абсолютно валидный код и ожидаемое поведение PHP. Так что мы поступаем несколько странно, настаивая на неизменяемости. В общем, это забавный подводный камушек во всем процессе.

Есть, однако, один способ обойти эту проблему, с помощью ключевого слова final  . Оно говорит парсеру, что класс является окончательным, и от него нельзя наследоваться.

С помощью одного небольшого изменения нашего класса Immutable  мы можем избежать атаки на наш класс.

Так что теперь мы знаем, что нам нужен класс, объявленный final  и использующий закрытые ( private ) свойства для хранения значений. Важно убедиться, что ни один метод в Immutable  не позволяет вносить изменения ни в какие хранимые значения после того, как экземпляр класса создан.

Наконец, более полный пример

В завершение давайте разберем окончательный рабочий пример неизменяемого класса в PHP. В этом примере также было добавлено указание типов в PHP 7.

Строгий режим объявления типов и его применение для аргументов функций еще больше усиливает неизменяемый класс. Например, мы не получим неожиданно объекта в качестве аргумента. Это было бы плохо, потому что такой объект мог бы быть изменен где-то еще, что изменило бы содержимое нашего неизменяемого объекта — это плохо! Больше об этом в следующей статье.

Заключение

Методы и свойства, определенные в final классе SkateboardTrick  дают гарантию, что объект неизменяем и не может быть дополнен. Это, естественно, и есть наша цель.

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

В продолжении этой статьи я расскажу, как мы можем оптимизировать процесс создания новых экземпляров класса с измененной информацией, хранящейся в них. Они, конечно, все равно останутся неизменяемыми, только потребуется немного синтаксического сахара, чтобы сделать вещи проще.

Поделиться: Share on LinkedIn0Share on VKShare on Facebook0Share on Google+0Tweet about this on Twitter

1 комментарий

  1. На самом деле в PHP можно запретить переопределение переменной. Достигается это с помощью расширения SPL Types, которое доступно из PECL репозитория. Оное содержит класс SplEnum — единственная альтернатива типу enum в PHP. Этот класс можно наследовать и перечислить свои константы, а дальше пользоваться как обычным объектом. Но где магия? При повторном переопределении переменной, которой присвоен SplEnum, будет выбрасываться исключение UnexpectedValueException, посмотрите сами: https://git.io/vQKVg

Оставить комментарий

Ваш e-mail не будет опубликован.

Яндекс.Метрика