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

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

13 мая 2022 г.

По моему опыту, подавляющее большинство классов данных Java было написано так, как они были написаны многими моими коллегами, включая меня. Сотни человеко-часов на исправление настолько глупых ошибок, что их вообще не должно быть. Иногда это были пресловутые NullPointerExceptions, а иногда они были связаны с консистентностью — ровным согласованием частей друг с другом.


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


Эта проблема


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


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


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


```java


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


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


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


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


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


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


this.status = статус;


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


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


вернуть идентификатор;


public void setId (идентификатор строки) {


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


публичный AccountStatus getStatus () {


статус возврата;


public void setStatus (статус AccountStatus) {


this.status = статус;


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


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


public void setEmail (строка электронной почты) {


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


И перечисление для поля «статус».


```java


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


СОЗДАННЫЙ,


ПРОВЕРЕНО,


АКТИВНЫЙ


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


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


```java


@Контрольная работа


пустота should_successfully_instantiate_and_validate_nothing() {


// дано


результат var = новая учетная запись (null, null, null);


// когда тогда


assertThat(result.getId()).isNull();


assertThat(result.getEmail()).isNull();


assertThat(result.getStatus()).isNull();


И здесь мы устанавливаем статус «АКТИВНЫЙ», который не может быть без «электронной почты». В конце концов, у нас будет много ошибок бизнес-логики из-за этой несогласованности, таких как NullPointerException и многое другое.


```java


@Контрольная работа


пустота should_allow_to_set_any_state_and_any_email() {


// дано


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


// когда


account.setStatus(АКТИВНЫЙ);


account.setEmail (нулевой); // Любая часть кода в этом проекте может изменить класс по своему усмотрению. Нет согласованности


// потом


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


assertThat(account.getEmail()).isBlank();


Решение


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


  1. Проверьте поля на нуль или пустость и проверьте контракт между полями. Я предлагаю сделать это в «Конструкторах» и «Сеттерах».

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

  1. Создавайте сложные мутации как методы внутри ответственного класса. Например, для проверки учетной записи у нас есть метод «verify», поэтому у нас есть полный контроль над мутацией при проверке учетной записи.

Вот согласованная версия класса Account, для проверки я использую apache commons-lang:


```java


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


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


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


частная Необязательная электронная почта;


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


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


this.status = notNull (статус);


this.email = checkEmail (notNull (email));


public void verify (необязательный адрес электронной почты ) {


this.status = ПРОВЕРЕНО;


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


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


вернуть идентификатор;


public void setId (идентификатор строки) {


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


публичный AccountStatus getStatus () {


статус возврата;


public Необязательный getEmail() {


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


public void setEmail (необязательный адрес электронной почты ) {


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


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


правда(


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


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


этот.статус


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


Как видно из этого теста, его невозможно создать с пустыми полями или установить пустой адрес электронной почты, когда статус «АКТИВЕН».


```java


@Контрольная работа


пустота should_validate_parameters_on_instantiating() {


assertThatThrownBy(() -> new Account("", CREATED, empty())).isInstanceOf(IllegalArgumentException.class);


assertThatThrownBy(() -> new Account("example-id", null, empty())).isInstanceOf(NullPointerException.class);


assertThatThrownBy(() -> new Account("example-id", ACTIVE, empty()))


.isInstanceOf(IllegalArgumentException.класс)


.hasMessage(format("Электронная почта должна быть заполнена при статусе %s", АКТИВНО));


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


```java


@Контрольная работа


пустота should_verify_and_validate() {


// дано


var email = "example@example.com";


var account = new Account("example-id", CREATED, empty());


// когда


account.verify (из (электронной почты)); // Аккаунт контролирует согласованность своего состояния и не будет с неправильными данными


// потом


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


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


утверждать, что бросил(


() -> account.verify(empty()) // Невозможно подтвердить учетную запись без электронной почты


.isInstanceOf(IllegalArgumentException.класс)


.hasMessage(format("Электронная почта должна быть заполнена при статусе %s", ПРОВЕРЕНО));


Если у вас есть АКТИВНАЯ учетная запись, попробуйте установить для нее пустой адрес электронной почты, что невозможно, и мы это предотвратим:


```java


@Контрольная работа


пустота should_fail_when_set_empty_email_for_activated_account() {


// дано


varactivatedAccount = новая учетная запись("example-id", ACTIVE, of("example@example.com"));


// когда тогда


assertThatThrownBy(() ->activatedAccount.setEmail(пусто()))


.isInstanceOf(IllegalArgumentException.класс)


.hasMessage(format("Электронная почта должна быть заполнена при статусе %s", АКТИВНО));


Вывод


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


  1. Создайте проверки для каждого поля конструктора и установщика.

  1. Используйте java.util.Optional.

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

Вы можете найти полный рабочий пример на [GitHub] (https://github.com/sutulovai/java-objects-more-consistent-way).



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