← Назад к вопросам

Как будешь писать паттерн монада

2.7 Senior🔥 91 комментариев
#Stream API и функциональное программирование

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Ответ

Монада (Monad) — это паттерн из функционального программирования, который позволяет элегантно работать с операциями, которые могут выдавать результат или ошибку. В Java эта концепция реализуется несколькими способами, самый практичный из которых — Optional.

Что такое монада

Монада — это контейнер, который:

  1. Оборачивает значение (может быть или может не быть)
  2. Предоставляет метод bind (flatMap в Java) для цепочки операций
  3. Обработает ошибки автоматически без проверки на каждом шаге

Паттерн 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);

Правила монады

  1. Identity (левая): M(a).flatMap(f) == f(a)
  2. Identity (правая): M(a).flatMap(M) == M(a)
  3. Ассоциативность: M(a).flatMap(f).flatMap(g) == M(a).flatMap(x => f(x).flatMap(g))

Это значит, что монада предсказуема и композируема.

Вывод

Монада в Java:

  • Optional — встроенная монада для работы с null
  • Result/Either — для элегантной обработки ошибок
  • Stream — монада для работы с коллекциями
  • CompletableFuture — монада для асинхронных операций

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

Как будешь писать паттерн монада | PrepBro