Лучшие структуры данных с неизменяемым состоянием

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

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

Почему важна неизменность

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

  • Данные не изменятся «за нашими спинами».
  • После проверки он будет действовать бессрочно.
  • Никаких скрытых побочных эффектов.
  • Никаких полуинициализированных объектов, перемещающихся разными методами до завершения. Это разделит методы и, надеюсь, превратит их в чистые функции.
  • Безопасность потоков: больше никаких состояний гонки. Если структура данных никогда не меняется, мы можем безопасно использовать ее в нескольких потоках.
  • Лучшая кешируемость.
  • Возможные методы оптимизации, такие как ссылочная прозрачность и мемоизация.

Состояние неизменности Java

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

Встроенные неизменяемые типы

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

«Последнее» ключевое слово

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

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

Java 14

В марте 2020 года будет выпущен JDK 14 с предварительным просмотром записей: держателей данных для поверхностно неизменяемых данных. Надеюсь, мы сможем заменить многие типы JavaBean записями, хотя есть несколько ограничений по сравнению с традиционными классами. Они сделают записи более безопасными в использовании, но нам, возможно, придется переосмыслить то, как мы строим наши структуры данных.

Java 14 - это далекое будущее для многих разработчиков, в том числе и для меня. Я использую Java 8 и, надеюсь, скоро 9. Итак, давайте сегодня рассмотрим другие варианты достижения неизменности.

Как стать неизменным

Фактически есть два способа создания неизменяемых структур данных без записей Java 14: сделать это самостоятельно или с помощью стороннего фреймворка.

Сделай сам неизменяемые

Подумайте о типичном JavaBean:

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

Еще одна ловушка - побочные эффекты. Установщики могут делать больше, чем просто устанавливать одно значение - они могут отслеживать «грязное состояние» или устанавливать несколько значений. Это не очень хорошая практика, но это происходит постоянно.

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

Давайте порвем с традиционным bean-дизайном и сделаем его неизменным:

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

Если мы не добавим больше полей с изменяемым типом…

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

Коллекции также являются проблемными типами. Неизменяемые коллекции существуют со времен Java 7. В Java 9 были добавлены простые в использовании фабричные методы. Но, как и final, это означает, что невозможно изменить саму коллекцию, а не содержащиеся в ней объекты. Поэтому не забудьте сохранить в них только уже неизменные структуры данных.

Строитель шаблон

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

Все поля должны быть установлены при инициализации, поэтому нам нужен конструктор со всеми полями. Но что, если нам не нужно настраивать все поля? Должны ли мы предоставлять несколько конструкторов или статических методов-построителей? И как мы можем гарантировать, что все обязательные поля установлены и полученный объект действителен?

Используя шаблон конструктора.

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

Предположим более сложный тип пользователя:

Поля active и lastLogin являются необязательными - по умолчанию пользователь не активен, пока явно не указано иное, и никогда не входил в систему. Либо мы предоставляем аргументы каждый раз, когда создаем пользователя, либо добавляем дополнительные конструкторы для соответствия различным комбинациям аргументов.

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

Теперь мы можем создать пользователя с плавным API:

Или мы можем построить его в несколько этапов:

Чтобы сделать наш конструктор еще лучше, мы можем добавить проверку методов или конструктора - например, проверка на null или проверка адреса электронной почты / пароля и выдача соответствующего исключения:

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

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

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

Сторонние фреймворки

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

Неизменяемые

Как описывает себя проект Неизменяемые:

Процессоры аннотаций Java для создания простых, безопасных и согласованных объектов значений. Не повторяйтесь, попробуйте Immutables, самый полный инструмент в этой области!

Создать неизменяемый тип так же просто, как создать абстрактный тип, abstract class или interface, и добавить правильные аннотации:

Процессор аннотаций генерирует фактическую реализацию за кулисами, включая:

  • Поддержка сериализации.
  • Строитель класс.
  • Проверка требований.
  • Удобные способы копирования и т. Д.
  • equals, hashCode и toString

Давайте создадим неизменяемого пользователя с помощью предоставленного конструктора:

Благодаря использованию аннотации @Value.Default и типа Optional мы автоматически получаем подтверждение при вызове build(). Если не все требования выполнены, выдается IllegalStateException.

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

Или мы можем получить новый строитель для работы с:

Это лишь верхушка айсберга - список функций исчерпывающий:

Проект Ломбок

Project Lombok - это комплексный инструмент, пытающийся уменьшить количество обычного шаблонного кода Java, такого как геттеры и сеттеры, проверки на нуль, equals / hashCode или toString.

И, конечно же, неизменяемые:

Аннотация @Value эквивалентна использованию этих довольно понятных аннотаций:

Не хватает только @Builder, что добавляет User.builder() в наш пример.

Project Lombok - отличный инструмент для сокращения шаблонов. Но для построения гибких неизменяемых структур данных я бы рекомендовал Неизменяемые. У обоих проектов разные цели, и я думаю, что это само собой разумеющееся, что проект под названием Неизменяемые имеет лучшие характеристики в отношении неизменяемости!

Заключение

Неизменяемость - хорошая идея для многих программных проектов, а не только для языков без встроенной поддержки. Даже для хранения данных он может дать преимущества, как, например, система контроля версий Git. Он использует неизменяемые коммиты для обеспечения целостности.

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

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

Наш реальный пример

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

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

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

Ресурсы

Библиотеки

Статьи