Узнайте, как жить с неизменяемыми и надежными объектами в Java

Узнайте, как жить с неизменяемыми и надежными объектами в Java

29 мая 2022 г.

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


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


В моей предыдущей [статье] (https://hackernoon.com/learn-how-to-make-java-classes-more-consistent-with-minimal-effort) я показал, как мы можем улучшить согласованность и надежность стандартных объектов. В нескольких словах:


  • Добавлены проверки при установке значений

  • Используйте java.util.Optional для каждого поля, допускающего значение NULL.

  • Разместите сложные мутации в нужном месте - в самом ответственном классе.

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


Проблема


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


Например, он используется в HashMaps и, возможно, в многопоточной среде. Итак, это уже не кажется хорошей идеей — писать по умолчанию. [Взлом HashMap] (https://hackernoon.com/how-to-use-hashmap-with-custom-keys-and-avoid-shooting-yourself-in-the-leg) не имеет большого значения и непоследовательно состояние между потоками не заставит себя долго ждать.


Первые вещи, которые приходят на ум при мысли о создании неизменяемых объектов:


  1. Не делайте сеттер-методы

  1. Сделайте все поля окончательными

  1. Не делитесь экземплярами с изменяемыми объектами

  1. Не позволяйте подклассам переопределять методы (в этой статье я это опущу)

Но как жить с таким объектом? Когда нам нужно изменить его, нам нужно сделать копию; как мы можем сделать это хорошо, не копируя каждый раз код и логику?


Несколько слов о наших примерах классов


Допустим, у нас есть Аккаунты. У каждой учетной записи есть идентификатор, статус и адрес электронной почты. Аккаунты можно проверить по электронной почте. Когда статус «СОЗДАН», мы не ожидаем, что письмо будет заполнено. Но когда это «ПРОВЕРЕНО» или «НЕАКТИВНО», электронная почта должна быть заполнена.


Статусы аккаунтов


```java


общественное перечисление AccountStatus {


СОЗДАННЫЙ,


ПРОВЕРЕНО,


НЕАКТИВНЫЙ


Реализация канона Account.java:


```javascript


Аккаунт открытого класса {


закрытый конечный идентификатор строки;


закрытый окончательный статус AccountStatus;


закрытая финальная строка электронной почты;


общедоступная учетная запись (идентификатор строки, статус AccountStatus, строка электронной почты) {


это.id = идентификатор;


this.status = статус;


this.email = электронная почта;


// равно / hashCode / геттеры


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


```java


var account = new Account ("some-id", CREATED, null);


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


```javascript


account.setEmail("example@example.com");// мы не можем этого сделать, у нас нет сеттеров


Единственный способ сделать это — создать новый экземпляр и поместить в конструктор предыдущие значения:


```javascript


var withEmail = новая учетная запись (account.getId(), СОЗДАН, "example@example.com");


Но это не лучший способ изменить значение поля, он производит слишком много копий/вставок, а класс Account не отвечает за его непротиворечивость.


Решение


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


Чтобы построить объект, я использую шаблон «Строитель». Он довольно известен, и для вашей IDE существует множество плагинов для его автоматического создания.


```java


Аккаунт открытого класса {


закрытый конечный идентификатор строки;


закрытый окончательный статус AccountStatus;


личный окончательный вариант Необязательный адрес электронной почты;


общедоступная учетная запись (строитель) {


this.id = непустой (builder.id);


this.status = notNull (builder.status);


this.email = checkEmail(builder.email);


проверка общедоступной учетной записи (строка электронной почты) {


вернуть копию ()


.status(ПРОВЕРЕНО)


.email (из (электронной почты))


.строить();


публичная учетная запись changeEmail (строка электронной почты) {


вернуть копию ()


.email (из (электронной почты))


.строить();


публичная учетная запись деактивировать () {


вернуть копию ()


.статус(НЕАКТИВНО)


.строить();


частный Необязательный checkEmail (Необязательный электронная почта) {


правда(


notNull(email).map(StringUtils::isNotBlank).orElse(false) || this.status.equals(СОЗДАН),


"Электронная почта должна быть заполнена при статусе %s",


этот.статус


ответная электронная почта;


общедоступный статический конечный класс Builder {


частный строковый идентификатор;


статус закрытого аккаунта;


private Необязательный электронная почта = empty();


частный строитель () {


общедоступная статическая учетная запись Builder () {


вернуть новый Builder();


общедоступный идентификатор Builder (идентификатор строки) {


это.id = идентификатор;


вернуть это;


публичный статус Builder (статус AccountStatus) {


this.status = статус;


вернуть это;


общедоступная электронная почта Builder (необязательная электронная почта) {


this.email = электронная почта;


вернуть это;


публичная сборка учетной записи () {


вернуть новую учетную запись (эту);


// равно / hashCode / геттеры


Как видите, в нашем классе есть приватный метод copy, который возвращает Builder с точной копией. Это устраняет копирование и вставку всех полей, однако очень важно, чтобы этот метод был недоступен извне Account.java, потому что с этим Builder снаружи мы теряем контроль над состоянием и согласованностью.


Смена аккаунтов по-новому


Теперь давайте создадим учетную запись:


```java


переменная учетная запись = учетная запись ()


.id ("пример-идентификатор")


.статус(СОЗДАН)


.email((пусто())


.строить();


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


```java


var withNewEmail = account.changeEmail("new@new.com");


Демонстрация этого в модульном тесте:


```java


@Тест


пустота should_successfully_change_email() {


// данный


переменная учетная запись = учетная запись ()


.id ("пример-идентификатор")


.status(ПРОВЕРЕНО)


.email(из("old@old.com"))


.строить();


var newEmail = "new@new.com";


// когда


var withNewEmail = account.changeEmail(newEmail);


// тогда


assertThat(withNewEmail.getId()).isEqualTo(account.getId());


assertThat(withNewEmail.getStatus()).isEqualTo(account.getStatus());


assertThat(withNewEmail.getEmail()).isEqualTo(of(newEmail));


Для проверки учетной записи мы не создаем копию со статусом VERIFIED и новый адрес электронной почты. Мы просто вызываем метод verify, который не только создаст для нас копию, но и проверит действительность электронного письма.


```java


@Тест


пустота should_successfully_verify_account () {


// данный


переменная создана = учетная запись ()


.id ("пример-идентификатор")


.статус(СОЗДАН)


.строить();


var email = "example@example.com";


// когда


var Verified = created.verify(email);


// тогда


assertThat(verified.getId()).isEqualTo(created.getId());


assertThat(verified.getStatus()).isEqualTo(VERIFIED);


assertThat(verified.getEmail().get()).isEqualTo(email);


Вывод


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


При его реализации не забудьте:


  1. Сделайте все поля окончательными

  1. Не предоставлять сеттеры

  1. Не делитесь ссылками на изменяемые объекты

  1. Не позволяйте подклассам переопределять методы

  1. Предоставьте методы мутации из вашего класса

  1. Реализуйте метод private copy внутри ответственного класса, который возвращает Builder, и используйте его для создания новых экземпляров внутри вашего класса.

  1. Поддерживайте согласованность полей, используя проверки значений

  1. Используйте «Необязательно» с полями, допускающими значение NULL

Вы можете найти полностью рабочий пример с дополнительными модульными тестами на GitHub.


Главное фото – Адам Нескёрук на Unsplash



Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE