Как будешь писать паттерн монада
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Ответ
Монада (Monad) — это паттерн из функционального программирования, который позволяет элегантно работать с операциями, которые могут выдавать результат или ошибку. В Java эта концепция реализуется несколькими способами, самый практичный из которых — Optional.
Что такое монада
Монада — это контейнер, который:
- Оборачивает значение (может быть или может не быть)
- Предоставляет метод bind (flatMap в Java) для цепочки операций
- Обработает ошибки автоматически без проверки на каждом шаге
Паттерн Monada в Java: Optional
Пример без монады (императивный стиль):
// Старый способ — много проверок
User user = userRepository.findById(userId);
if (user != null) {
UserDTO dto = mapper.map(user);
if (dto != null) {
Optional<String> result = cache.get(dto.getEmail());
if (result != null) {
System.out.println(result);
}
}
}
С использованием монады (Optional):
// Функциональный стиль — чисто и элегантно
userRepository.findById(userId)
.map(user -> mapper.map(user))
.flatMap(dto -> cache.get(dto.getEmail()))
.ifPresent(System.out::println);
Полная реализация паттерна Monad
1. Простой пример с Optional
import java.util.Optional;
public class MonadExample {
public static void main(String[] args) {
// Создаем Optional с значением
Optional<String> value = Optional.of("Hello World");
// map — применяет функцию, если значение есть
Optional<Integer> length = value.map(String::length);
System.out.println(length.orElse(0)); // 11
// flatMap — применяет функцию, которая возвращает Optional
Optional<Integer> firstWordLength = value
.flatMap(v -> Optional.of(v.split(" ")[0]))
.map(String::length);
System.out.println(firstWordLength.orElse(0)); // 5
// ifPresent — выполняет действие, если значение есть
value.ifPresent(v -> System.out.println("Value: " + v));
// orElse — возвращает значение или дефолт
System.out.println(value.orElse("default"));
}
}
2. Пример с пользователем и базой данных
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private EmailService emailService;
// Паттерн: найти пользователя, валидировать, отправить письмо
public void sendWelcomeEmail(String email) {
userRepository.findByEmail(email)
.filter(user -> user.isActive()) // фильтруем неактивных
.filter(user -> !user.isEmailVerified()) // фильтруем верифицированных
.map(user -> createEmailMessage(user)) // трансформируем
.ifPresent(message -> emailService.send(message));
}
private EmailMessage createEmailMessage(User user) {
return new EmailMessage(user.getEmail(), "Welcome!");
}
}
Собственная реализация Monad
Для понимания, как работает монада под капотом:
// Интерфейс монады
public interface Monad<T> {
// unit/return — оборачивает значение
static <T> Monad<T> unit(T value) {
return new SimpleMonad<>(value);
}
// bind/flatMap — цепочка операций
<R> Monad<R> flatMap(Function<T, Monad<R>> function);
// map — преобразование
<R> Monad<R> map(Function<T, R> function);
// get — извлечение значения
T get();
}
// Простая реализация
public class SimpleMonad<T> implements Monad<T> {
private final T value;
public SimpleMonad(T value) {
this.value = value;
}
@Override
public <R> Monad<R> flatMap(Function<T, Monad<R>> function) {
// Применяем функцию к значению и возвращаем результат
// (функция возвращает Monad)
return function.apply(value);
}
@Override
public <R> Monad<R> map(Function<T, R> function) {
// Применяем функцию и оборачиваем результат в новую монаду
return new SimpleMonad<>(function.apply(value));
}
@Override
public T get() {
return value;
}
}
// Использование
public class CustomMonadExample {
public static void main(String[] args) {
Monad<String> monad = SimpleMonad.unit("Hello")
.map(s -> s + " World")
.flatMap(s -> SimpleMonad.unit(s.length()));
System.out.println(monad.get()); // 11
}
}
Монада Result/Either (для обработки ошибок)
Проблема: Optional не может передать информацию об ошибке.
// Без монады: нужны проверки
user = getUserFromDB(id);
if (user == null) {
logger.error("User not found");
return null;
}
if (!user.isActive()) {
logger.error("User is not active");
return null;
}
return user;
// С монадой Result можно красиво обработать ошибки
Реализация Result монады:
public abstract class Result<T> {
public static class Success<T> extends Result<T> {
public final T value;
public Success(T value) { this.value = value; }
}
public static class Failure<T> extends Result<T> {
public final String error;
public Failure(String error) { this.error = error; }
}
// map — трансформация значения в Success
public abstract <R> Result<R> map(Function<T, R> function);
// flatMap — цепочка операций, которые могут быть Success или Failure
public abstract <R> Result<R> flatMap(Function<T, Result<R>> function);
// Вспомогательные методы
public abstract T getOrElse(T defaultValue);
public abstract void ifSuccess(Consumer<T> action);
public abstract void ifFailure(Consumer<String> action);
}
public class Success<T> extends Result<T> {
private final T value;
public Success(T value) { this.value = value; }
@Override
public <R> Result<R> map(Function<T, R> function) {
try {
return new Success<>(function.apply(value));
} catch (Exception e) {
return new Failure<>(e.getMessage());
}
}
@Override
public <R> Result<R> flatMap(Function<T, Result<R>> function) {
try {
return function.apply(value);
} catch (Exception e) {
return new Failure<>(e.getMessage());
}
}
@Override
public T getOrElse(T defaultValue) { return value; }
@Override
public void ifSuccess(Consumer<T> action) { action.accept(value); }
@Override
public void ifFailure(Consumer<String> action) { /* ничего */ }
}
public class Failure<T> extends Result<T> {
private final String error;
public Failure(String error) { this.error = error; }
@Override
public <R> Result<R> map(Function<T, R> function) {
return new Failure<>(error); // пропускаем трансформацию
}
@Override
public <R> Result<R> flatMap(Function<T, Result<R>> function) {
return new Failure<>(error); // пропускаем операцию
}
@Override
public T getOrElse(T defaultValue) { return defaultValue; }
@Override
public void ifSuccess(Consumer<T> action) { /* ничего */ }
@Override
public void ifFailure(Consumer<String> action) { action.accept(error); }
}
// Использование Result монады
@Service
public class UserService {
public Result<User> findAndValidateUser(Long id) {
return findUserInDB(id)
.flatMap(user -> validateUser(user))
.flatMap(user -> checkIfActive(user));
}
private Result<User> findUserInDB(Long id) {
User user = repository.findById(id).orElse(null);
if (user == null) {
return new Failure<>("User not found");
}
return new Success<>(user);
}
private Result<User> validateUser(User user) {
if (user.getEmail() == null || user.getEmail().isEmpty()) {
return new Failure<>("Email is required");
}
return new Success<>(user);
}
private Result<User> checkIfActive(User user) {
if (!user.isActive()) {
return new Failure<>("User is not active");
}
return new Success<>(user);
}
}
// Использование
Result<User> result = userService.findAndValidateUser(123L);
result.ifSuccess(user -> System.out.println("User: " + user.getName()));
result.ifFailure(error -> System.out.println("Error: " + error));
Практический пример: цепочка операций с обработкой ошибок
@Service
public class OrderService {
public Result<OrderDTO> processOrder(Long orderId) {
return findOrder(orderId)
.flatMap(order -> validateOrder(order))
.flatMap(order -> checkInventory(order))
.flatMap(order -> processPayment(order))
.flatMap(order -> shipOrder(order))
.map(order -> convertToDTO(order));
}
private Result<Order> findOrder(Long id) {
return repository.findById(id)
.map(Result::<Order>new Success)
.orElseGet(() -> new Failure<>("Order not found"));
}
private Result<Order> validateOrder(Order order) {
if (order.getItems().isEmpty()) {
return new Failure<>("Order is empty");
}
return new Success<>(order);
}
private Result<Order> checkInventory(Order order) {
for (OrderItem item : order.getItems()) {
if (inventoryService.getStock(item.getProduct()) < item.getQuantity()) {
return new Failure<>("Not enough stock");
}
}
return new Success<>(order);
}
private Result<Order> processPayment(Order order) {
try {
paymentService.charge(order.getTotal());
return new Success<>(order);
} catch (PaymentException e) {
return new Failure<>("Payment failed: " + e.getMessage());
}
}
private Result<Order> shipOrder(Order order) {
order.setStatus(OrderStatus.SHIPPED);
repository.save(order);
return new Success<>(order);
}
private OrderDTO convertToDTO(Order order) {
return new OrderDTO(order.getId(), order.getStatus());
}
}
// Использование
public class OrderController {
@PostMapping("/orders/{id}/process")
public ResponseEntity<?> processOrder(@PathVariable Long id) {
Result<OrderDTO> result = orderService.processOrder(id);
if (result instanceof Success) {
return ResponseEntity.ok(((Success<OrderDTO>) result).value);
} else {
String error = ((Failure<OrderDTO>) result).error;
return ResponseEntity.badRequest().body(Map.of("error", error));
}
}
}
Java 8+ альтернативы
Stream API (монада List)
List<String> result = users
.stream()
.filter(user -> user.getAge() > 18)
.map(User::getEmail)
.collect(Collectors.toList());
CompletableFuture (монада для асинхронных операций)
CompletableFuture<String> future = CompletableFuture
.supplyAsync(() -> getUserFromDB(id))
.thenApply(user -> user.getName())
.thenApply(String::toUpperCase);
future.thenAccept(System.out::println);
Правила монады
- Identity (левая):
M(a).flatMap(f) == f(a) - Identity (правая):
M(a).flatMap(M) == M(a) - Ассоциативность:
M(a).flatMap(f).flatMap(g) == M(a).flatMap(x => f(x).flatMap(g))
Это значит, что монада предсказуема и композируема.
Вывод
Монада в Java:
- Optional — встроенная монада для работы с null
- Result/Either — для элегантной обработки ошибок
- Stream — монада для работы с коллекциями
- CompletableFuture — монада для асинхронных операций
Основная идея: цепочка операций без проверок на каждом шаге, потому что монада сама обрабатывает успех/ошибку.