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

В этой статье используется Java 21, поскольку это первая выпущенная версия Java LTS с сопоставлением шаблонов. В примере также используется Spring Boot, но этот подход можно использовать в любой подобной ситуации.

Краткое описание запечатанных классов и сопоставления с образцом

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

Герметичные классы (JEP 409)

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

Синтаксис:

public sealed interface Fruit permits Apple, Orange {
    // Обратите внимание на ключевое слово permits:
    // список разрешенных имплементаций определяется после него.
}


public class Apple implements Fruit {
    // Имплементация определяется как обычно.
}

Сопоставление с образцом (JEP 441)

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

Синтаксис:

switch (fruit) {
    case Apple apple -> eat(apple);
    case Orange orange -> give(orange);
    // обратите внимание, что default ветка здесь не нужна
}

Давайте применим это на практике

Давайте представим себе простой бэкэнд, реализованный с помощью Spring Boot. Этот бэкэнд имеет API с конечной точкой POST/session, который создает сеанс для пользователя. Эта конечная точка имеет три возможных ответа:

  • 200 ОК – если сессия создана успешно;

  • 422 Необрабатываемый контент — если для создания сеанса требуется дополнительная информация;

  • 500 Internal Server Error — если на серверной стороне произошла критическая ошибка.

ЧИТАТЬ   «Хезболла» выпустила ракеты по Израилю в «первом ответе» на убийство главного лидера своего союзника ХАМАС

Стандартная реализация Spring будет содержать классы Controller и Service (ненужные детали игнорируются):

@RestController
public class SessionApi {

    // ...

    public ResponseEntity<?> createSession(UserInfo userInfo) {
        SessionInfo sessionInfo = sessionService.createSession(userInfo);
        return new ResponseEntity<>(sessionInfo, HttpStatus.OK);
    }
}
@Service
public class SessionService {

    // ...

    public SessionInfo createSession(UserInfo userInfo) {
        // создаем сессию, а в случае критической ошибки выбрасываем исключение,
        // которое будет обработано в @ControllerAdvice
        return sessionInfo;
    }
}

Приведенный выше код хорошо обрабатывает первый (200) и последний (500) варианты ответа. Однако эта реализация не учитывает ответ 422. Возможные подходы к решению этой проблемы:

  • Немедленно вернуть ResponseEntity из сервиса с нужным кодом — превращает Controller в бесполезный класс слоя;

  • Выброс исключения в Service и его обработка в ControllerAdvice распределяет бизнес-логику между классами, поскольку 422 — это не критическая ошибка, а стандартный ответ;

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

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

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

public sealed interface CreateSessionResult permits SessionInfo, AdditionalInfoRequired {

}

Также потребуются реализации интерфейса, будь то DTO:

public record SessionInfo(/*поля пропущены*/) implements CreateSessionResult {

}
public record AdditionalInfoRequired(/*поля пропущены*/) implements CreateSessionResult {

}

Теперь давайте изменим тип возвращаемого значения в Service:

@Service
public class SessionService {

    // ...

    public CreateSessionResult createSession(UserInfo userInfo) {
        // в зависимости от ситуации результата может быть двух разных типов
        return someCondition
            ? sessionInfo
            : additionalInfoRequired;
    }
}

И, наконец, в контроллере мы сгенерируем соответствующий HTTP-ответ, используя сопоставление с образцом:

@RestController
public class SessionApi {

    // ...

    public ResponseEntity<? extends CreateSessionResult> createSession(UserInfo userInfo) {
        CreateSessionResult createSessionResult = sessionService.createSession(userInfo);
        return switch (createSessionResult) {
            case SessionInfo sessionInfo -> new ResponseEntity<>(sessionInfo, HttpStatus.OK);
            case AdditionalInfoRequired infoRequired -> new ResponseEntity<>(infoRequired, HttpStatus.UNPROCESSABLE_ENTITY);
        };
    }
}

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

ЧИТАТЬ   8 звезд 40+, чьи техники укладки хочется немедленно попробовать, чтобы выглядеть потрясающе

Что делать, если Java 21 нет в проекте

Самый близкий код можно получить в Java 17. В этой версии достаточно поменять переключатель в Контроллере:

@RestController
public class SessionApi {

    // ...

    public ResponseEntity<? extends CreateSessionResult> createSession(UserInfo userInfo) {
        CreateSessionResult createSessionResult = sessionService.createSession(userInfo);
        return switch (createSessionResult.getClass().getSimpleName()) {
            case "SessionInfo" -> new ResponseEntity<>(createSessionResult, HttpStatus.OK);
            case "AdditionalInfoRequired" -> new ResponseEntity<>(createSessionResult, HttpStatus.UNPROCESSABLE_ENTITY);
            default -> throw new RuntimeException("Это исключение никогда не произойдет");
        };
    }
}

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

Также в Java 17 вы можете включить сопоставление шаблонов с помощью настройки JVM. --enable-preview --source 17.

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


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

Source

От admin