Рубрики
Без рубрики

Игра с монадой или Как наслаждаться функциональным стилем в Java

Прагматичная Функциональная Java. С пометкой java, lang, для начинающих, учебное пособие.

В наши дни использование функционального стиля, ну, в некотором роде, модно. Есть много руководств/статей/постов/и т.д. с объяснением того, как использовать потоки Java 8, RxJava или Project Reactor. Хотя все эти объяснения полезны, они часто оставляют впечатление, что это единственные способы/подходы для функционального стиля в Java. Но это не так. Существует множество других функциональных техник. Один из них – Монады.

Как я уже упоминал ранее, Монада – это концепция функционального программирования. При правильном применении эта концепция позволяет писать выразительный, лаконичный, простой в тестировании код. Но (всегда есть “но”) правильное применение этой концепции требует несколько иного мышления.

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

В самом начале был код…

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

public UserProfileResponse getUserProfileHandler(final User.Id userId) {
    final User user = userService.findById(userId);
    if (user == null) {
        return UserProfileResponse.error(USER_NOT_FOUND);
    }

    final UserProfileDetails details = userProfileService.findById(userId);

    if (details == null) {
        return UserProfileResponse.of(user, UserProfileDetails.defaultDetails());
    }

    return UserProfileResponse.of(user, details);
}

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

Шаг 1

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

final User user = userService.findById(userId);

if (user == null) {
    return UserProfileResponse.error(USER_NOT_FOUND);
}

final UserProfileDetails details = userProfileService.findById(userId);

return UserProfileResponse.of(user, 
                              details == null 
                              ? UserProfileDetails.defaultDetails() 
                              : details);

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

Шаг 2

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

final User user = userService.findById(userId);

if (user == null) {
    return UserProfileResponse.error(USER_NOT_FOUND);
}

final Option details = Option.option(userProfileService.findById(userId));

return UserProfileResponse.of(user,
                              details.otherwiseGet(UserProfileDetails::defaultDetails));

Хорошо, выглядит несколько лучше. Читаемость восстановлена, но краткость немного пострадала.

Шаг 3

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

final Option user1 = Option.option(userService.findById(userId));

return user1.map(user -> {
    final Option details = Option.option(userProfileService.findById(userId));
    return UserProfileResponse.of(user, details.otherwiseGet(UserProfileDetails::defaultDetails));
})
            .otherwiseGet(() -> UserProfileResponse.error(USER_NOT_FOUND));

Эта версия имеет проблемы с отступами и определенно требует некоторой встраивания и других очисток. Я показываю это здесь с единственной целью, чтобы показать, вероятно, один из ключевых моментов при запуске использования монад: код, который требует значения от монады, перемещается без изменений в лямбда . Затем лямбда передается одному из методов отображения (для преобразования монады) или приложения (которые используются для создания побочных эффектов). Этот паттерн является общим для всех Монад, и, используя его, мы можем наслаждаться всей мощью Монад. Например, написать асинхронную обработку с помощью монад так же просто, как написать традиционный синхронный код.

Итак, давайте проведем заключительную уборку.

Шаг 4

Окончательные очистки, встраивание и т.д., Включая статический импорт для Option.option :

return option(userService.findById(userId))
        .map(user -> UserProfileResponse.of(user,
                                            option(userProfileService.findById(userId))
                                                    .otherwiseGet(UserProfileDetails::defaultDetails)))
        .otherwiseGet(() -> UserProfileResponse.error(USER_NOT_FOUND));

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

Я хотел бы видеть в комментариях отзывы об окончательной версии кода (удобочитаемость, выразительность и т.д.).

Оригинал: “https://dev.to/siy/playing-with-monad-or-how-to-enjoy-functional-style-in-java-ijb”