Узнайте, как жить с неизменяемыми и надежными объектами в 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) не имеет большого значения и непоследовательно состояние между потоками не заставит себя долго ждать.
Первые вещи, которые приходят на ум при мысли о создании неизменяемых объектов:
- Не делайте сеттер-методы
- Сделайте все поля окончательными
- Не делитесь экземплярами с изменяемыми объектами
- Не позволяйте подклассам переопределять методы (в этой статье я это опущу)
Но как жить с таким объектом? Когда нам нужно изменить его, нам нужно сделать копию; как мы можем сделать это хорошо, не копируя каждый раз код и логику?
Несколько слов о наших примерах классов
Допустим, у нас есть Аккаунты. У каждой учетной записи есть идентификатор, статус и адрес электронной почты. Аккаунты можно проверить по электронной почте. Когда статус «СОЗДАН», мы не ожидаем, что письмо будет заполнено. Но когда это «ПРОВЕРЕНО» или «НЕАКТИВНО», электронная почта должна быть заполнена.
```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 (из (электронной почты))
.строить();
публичная учетная запись деактивировать () {
вернуть копию ()
.статус(НЕАКТИВНО)
.строить();
частный Необязательный
правда(
notNull(email).map(StringUtils::isNotBlank).orElse(false) || this.status.equals(СОЗДАН),
"Электронная почта должна быть заполнена при статусе %s",
этот.статус
ответная электронная почта;
общедоступный статический конечный класс Builder {
частный строковый идентификатор;
статус закрытого аккаунта;
private Необязательный
частный строитель () {
общедоступная статическая учетная запись 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);
Вывод
Жить с неизменяемыми, непротиворечивыми и надежными объектами тяжело, но при правильном подходе это может стать намного проще.
При его реализации не забудьте:
- Сделайте все поля окончательными
- Не предоставлять сеттеры
- Не делитесь ссылками на изменяемые объекты
- Не позволяйте подклассам переопределять методы
- Предоставьте методы мутации из вашего класса
- Реализуйте метод private
copy
внутри ответственного класса, который возвращаетBuilder
, и используйте его для создания новых экземпляров внутри вашего класса.
- Поддерживайте согласованность полей, используя проверки значений
- Используйте «Необязательно» с полями, допускающими значение NULL
Вы можете найти полностью рабочий пример с дополнительными модульными тестами на GitHub.
Главное фото – Адам Нескёрук на Unsplash
Оригинал