PHP и неизменяемость: экземпляры, которые могут быть изменены

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

В прошлой статье мы научились создавать неизменяемые структуры данных на PHP. Было несколько вещей, которые требовали доработки, но мы справились. Теперь приступим к улучшению неизменяемого класса, чтобы он был более полезным и давал бы проще созавать измененные копии. Заметьте, что это копии оригинального объекта, а не изменения в самом объекте.

Простое изменение параметров

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

Так просто… Слишком просто!

Эта техника работает сравнительно хорошо с таким малым количеством данных, но что будет, если у нас каждый раз 5 или 10 параметров для передачи? Вот утрированный пример для иллюстрации:

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

Изменение при клонировании

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

Вместо непосредственного изменения объекта, как вы делали бы в традиционном ООП, мы сделаем клон объекта и изменим его закрытые (приватные) свойства. Да-да, вы прочли это правильно, вы можете изменять приватные свойства экземпляра класса!

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

Так… Это не сработало! Я должен был сказать, что, чтобы можно это было так менять,  клонирование нужно выполнять только изнутри метода того же класса.

Таким образом легче изменять значение изнутри неизменной сущности — мы можем обернуть clone    и установить для них нужные значения. Сокращенный синтакс вроде этого очень помогает разработчикам в работе с неизменяемыми объектами.

Предотвращение установки неожидаемых свойств

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

В PHP свойства можно добавлять к классу во время выполнения программы — даже к финальному (final) классу. Этого мы не хотим, так как это изменило бы форму нашего класса и таким образом сделало бы его изменяемым. Простой способ этого избежать — добавить к классу пустую реализацию магического метода __set()  .

Можно также удалить значения свойств при помощи конутркции unset() . Мы опять же можем этого избежать при помощи другого пустого магического метода:

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

Объединенное изменение при клонировании

Пока что мы видели возможность менять одно свойсто при помощи метода в стиле withX , но что, если мы захотим поменять еще? Ну вы, конечно, могли бы просто соединить все изменения в цепочки наподобие такого:

Хотя это и будет работать, есть несколько вещей, которые мне не нравятся в таком подходе. Между вызовами withX()  и withY()  создаётся одноразовый экземпляр класса, так что вам понадобится создавать функцию with*() для каждого свойства, и соединение методов в цепочку быстро начнет раздражать.

Конечно, есть и другой способ.

Во-первых, давайте определим новый неизменяемый класс с несколькими свойствами.

Чтобы пример был короче, я применил маленький магический метод __get() вместо написания get-метода для каждого свойства класса. Вам нужно было бы писать по одному на каждое свойство, что привело бы к функциям вроде getEngineCc() , getBrakes()  и getTractionControl() . Вместо этого вы обращаетесь к ним напрямую, как к свойствам.

Но вернемся к изменениям! Чтобы дать легко манипулировать свойствами класса при клонировании мы можем добавить к классу простой метод.

Теперь, когда вам понадобится новый класс с модификациями — может, когда вы выпустите новую модель мопеда — вы можете просто вызвать with() и передать в него ассоциативный массив.

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

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

 

Один способ решения этой проблемы — заменить with() на функцию, которая использует Reflection, чтобы разобраться с порядком следования параметров конструктора и слить в него новые значения.

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

Вызывайте этот метод так же, как и последнюю реализацию with() . Он желает предположение, что свойства класса имеют те же имена, как и параметры конструктора (например, $this->engineC  — то же самое, что и параметр конструктора $engineCc ).

Это даст нам окончательный класс Bike :

Также держите в голове, что Reflection API, предоставляемое PHP, не особенно шустрое, так что если вы сторонник микрооптимизаций, то лучше его избегать. Но если вы все же готовы держать удар, то безопасность от проверки на тип стоит того.

Использование специального строителя для генерации неизменяемых объектов

Другой способ решения этой конкретной проблемы с неизменяемыми объектами может заключаться в использовании второго класса для генерирования таких объектов. Это даст вам возможность избегнуть использования Reflection API, но по-прежнему предоставит преимущество проверки на тип. Есть доля иронии в том, что мы будем использовать изменяемого строителя, чтобы создавать неизменяемый объект, но оставйтесь со мной…

Во-первых, нам нужно определить неизменяемый объект, который будет производить наш строитель. Я здесь также убираю магический __get(),  так как наша цель в облегчении статического анализа нашего кода. Это поможет IDE в подсказках при вводе кода и инструментам, обеспечивающим качество кода — прочесть наш код и предположительно сделать его легче для понимания.

 

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

BikiGenerator  дублирует часть кода оригинального класса и в действительности служит в качестве прославленной очереди. Мы ставим в очередь до тех пор, пока не удовлетворимся, затем выполняем build() , чтобы получить заново заполненный экземпляр Bike .

Этот пример иллюстрирует паттерн строитель в деле генерирования готового к использованию экземпляра неизменяемого класса Bike . Дальше вы можете вызвать ::with() , чтобы создать новую модифицированную версию существующего объекта.

Присваивание ещё большего количества свойств

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

Мы вынуждены были отказаться от системы типов в пользу маленькой «ручной» проверки на тип в свойстве класса $properties  и проверяемой в методе setValue() .

Тот способ, при помощи которого это работает, также облегчает управление опциональными аргументами при создании экземпляра — до сих пор все аргументы были обязательными. Здесь вы можете задавать массив, в котором пропущено одно или больше свойств, и они просто не будут установлены в массиве $this->data .

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

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

Объединение ещё больших коллекций свойств

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

Этим можно пользоваться так:

Когда мы пишем код таким образом, мы также фактически получаем нашу собственную маленькую реализацию именованных параметров. Это, конечно, замечательно, но с присущими такому подходу окольными путями мы также теряем некоторую ясность и подсказку типов в IDE.

Ну в любом случае, до вас дошло, что манипуляции неизменяемыми сущностями в PHP могут очень быстро начать раздражать. Похоже, что нет по-настоящему простых путей избегнуть набора большего количества кода на клавиатуре и/или дурного влияния на возможности IDE подсказывать при вводе кода. Здесь были представлены некоторые техники избежать фрустрации, но, конечно, всегда есть место для усовершенствования.

Поделиться: Share on LinkedIn
Linkedin
Share on VK
VK
Share on Facebook
Facebook
0Share on Google+
Google+
0Tweet about this on Twitter
Twitter

2 комментария

  1. Если у вас в коде появляются такие решения — по-ходу вы пишите что-то не то ))

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

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

Этот сайт использует Akismet для борьбы со спамом. Узнайте как обрабатываются ваши данные комментариев.

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