Java

Обробка помилок та валідація

Обробка помилок та валідація

Вступ: Помилки — це частина домену

У попередніх матеріалах ми часто використовували throw new IllegalStateException(...). Але виключення — не єдиний спосіб обробки помилок, і не завжди найкращий.

Питання:

  • Що робити, якщо книга недоступна — це виняткова ситуація чи очікуваний сценарій?
  • Як не "проковтнути" помилку?
  • Як зробити API методу явним щодо можливих результатів?

Два підходи до помилок

1. Виключення (Exceptions)

// Традиційний підхід
public Order createOrder(String userId, String bookId) {
    User user = userRepository.findById(userId)
            .orElseThrow(() -> new UserNotFoundException(userId));

    Book book = bookRepository.findById(bookId)
            .orElseThrow(() -> new BookNotFoundException(bookId));

    if (!user.canBorrow()) {
        throw new CannotBorrowException("User has reached limit");
    }

    // ... створення замовлення
}

Проблеми:

  • Клієнт може забути обробити виняток
  • Неявно, які саме винятки може кинути метод
  • Контроль потоку через винятки — антипатерн

2. Result Pattern (Either)

// Функціональний підхід
public Result<Order, OrderError> createOrder(String userId, String bookId) {
    return userRepository.findById(userId)
            .map(user -> processOrder(user, bookId))
            .orElse(Result.failure(OrderError.userNotFound(userId)));
}

Переваги:

  • Явний контракт — метод завжди повертає результат
  • Компілятор "змушує" обробити обидва випадки
  • Ланцюгування операцій без try-catch

Result Pattern: Детальна реалізація

Базова структура Result

package com.library.common;

import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;

/**
 * Контейнер результату операції: успіх або помилка.
 * Натхненний Either з функціонального програмування.
 *
 * @param <T> тип успішного значення
 * @param <E> тип помилки
 */
public sealed interface Result<T, E> permits Result.Success, Result.Failure {

    // === Фабричні методи ===

    /**
     * Створює успішний результат.
     */
    static <T, E> Result<T, E> success(T value) {
        return new Success<>(value);
    }

    /**
     * Створює результат з помилкою.
     */
    static <T, E> Result<T, E> failure(E error) {
        return new Failure<>(error);
    }

    /**
     * Обгортає операцію, що може кинути виняток.
     */
    static <T> Result<T, Exception> of(Supplier<T> supplier) {
        try {
            return success(supplier.get());
        } catch (Exception e) {
            return failure(e);
        }
    }

    // === Перевірки ===

    boolean isSuccess();

    default boolean isFailure() {
        return !isSuccess();
    }

    // === Доступ до значень ===

    T getValue();

    E getError();

    /**
     * Повертає значення або значення за замовчуванням.
     */
    default T getOrElse(T defaultValue) {
        return isSuccess() ? getValue() : defaultValue;
    }

    /**
     * Повертає значення або обчислює його від помилки.
     */
    default T getOrElse(Function<E, T> fallback) {
        return isSuccess() ? getValue() : fallback.apply(getError());
    }

    /**
     * Повертає значення або кидає виняток.
     */
    default <X extends Throwable> T getOrThrow(Function<E, X> exceptionMapper) throws X {
        if (isSuccess()) {
            return getValue();
        }
        throw exceptionMapper.apply(getError());
    }

    // === Трансформації ===

    /**
     * Трансформує успішне значення.
     */
    default <U> Result<U, E> map(Function<T, U> mapper) {
        if (isSuccess()) {
            return success(mapper.apply(getValue()));
        }
        return failure(getError());
    }

    /**
     * Трансформує успішне значення в інший Result.
     */
    default <U> Result<U, E> flatMap(Function<T, Result<U, E>> mapper) {
        if (isSuccess()) {
            return mapper.apply(getValue());
        }
        return failure(getError());
    }

    /**
     * Трансформує помилку.
     */
    default <F> Result<T, F> mapError(Function<E, F> mapper) {
        if (isFailure()) {
            return failure(mapper.apply(getError()));
        }
        return success(getValue());
    }

    // === Побічні ефекти ===

    /**
     * Виконує дію для успішного значення.
     */
    default Result<T, E> onSuccess(Consumer<T> action) {
        if (isSuccess()) {
            action.accept(getValue());
        }
        return this;
    }

    /**
     * Виконує дію для помилки.
     */
    default Result<T, E> onFailure(Consumer<E> action) {
        if (isFailure()) {
            action.accept(getError());
        }
        return this;
    }

    /**
     * Pattern matching для обробки обох випадків.
     */
    default <U> U fold(Function<T, U> onSuccess, Function<E, U> onFailure) {
        return isSuccess() ? onSuccess.apply(getValue()) : onFailure.apply(getError());
    }

    // === Реалізації ===

    record Success<T, E>(T value) implements Result<T, E> {
        public Success {
            Objects.requireNonNull(value, "Success value cannot be null");
        }

        @Override
        public boolean isSuccess() { return true; }

        @Override
        public T getValue() { return value; }

        @Override
        public E getError() {
            throw new IllegalStateException("No error in Success");
        }
    }

    record Failure<T, E>(E error) implements Result<T, E> {
        public Failure {
            Objects.requireNonNull(error, "Error cannot be null");
        }

        @Override
        public boolean isSuccess() { return false; }

        @Override
        public T getValue() {
            throw new IllegalStateException("No value in Failure");
        }

        @Override
        public E getError() { return error; }
    }
}

Ключові моменти:

  • Рядок 15: sealed interface — тільки Success та Failure можуть реалізовувати
  • Рядок 34-41: Метод of() для обгортання небезпечних операцій
  • Рядок 85-95: map та flatMap для ланцюгування
  • Рядок 122-124: fold для обробки обох випадків

Типізовані помилки

package com.library.domain.error;

/**
 * Sealed hierarchy помилок замовлення.
 */
public sealed interface OrderError {

    String getMessage();

    record UserNotFound(String userId) implements OrderError {
        @Override
        public String getMessage() {
            return "User not found: " + userId;
        }
    }

    record BookNotFound(String bookId) implements OrderError {
        @Override
        public String getMessage() {
            return "Book not found: " + bookId;
        }
    }

    record BookNotAvailable(String bookId, String title) implements OrderError {
        @Override
        public String getMessage() {
            return "Book '" + title + "' is not available";
        }
    }

    record UserCannotBorrow(String userId, String reason) implements OrderError {
        @Override
        public String getMessage() {
            return "User cannot borrow: " + reason;
        }
    }

    record InsufficientFunds(java.math.BigDecimal required, java.math.BigDecimal available)
            implements OrderError {
        @Override
        public String getMessage() {
            return String.format("Insufficient funds: required %s, available %s",
                    required, available);
        }
    }

    // Фабричні методи для зручності
    static OrderError userNotFound(String userId) {
        return new UserNotFound(userId);
    }

    static OrderError bookNotFound(String bookId) {
        return new BookNotFound(bookId);
    }

    static OrderError bookNotAvailable(String bookId, String title) {
        return new BookNotAvailable(bookId, title);
    }

    static OrderError userCannotBorrow(String userId, String reason) {
        return new UserCannotBorrow(userId, reason);
    }
}

Використання Result у сервісі

package com.library.application;

import com.library.common.Result;
import com.library.domain.error.OrderError;
import com.library.domain.model.*;
import com.library.domain.model.order.Order;

/**
 * Application Service з використанням Result Pattern.
 */
public class OrderApplicationService {

    private final BookRepository bookRepository;
    private final UserRepository userRepository;
    private final OrderRepository orderRepository;

    public OrderApplicationService(BookRepository bookRepository,
                                    UserRepository userRepository,
                                    OrderRepository orderRepository) {
        this.bookRepository = bookRepository;
        this.userRepository = userRepository;
        this.orderRepository = orderRepository;
    }

    /**
     * Створює замовлення.
     * Повертає Result замість кидання винятків.
     */
    public Result<Order, OrderError> createOrder(String userId, String bookId) {
        // Пошук користувача
        return findUser(userId)
                .flatMap(user -> findBook(bookId)
                        .flatMap(book -> validateAndCreate(user, book)));
    }

    private Result<User, OrderError> findUser(String userId) {
        return userRepository.findById(userId)
                .map(Result::<User, OrderError>success)
                .orElseGet(() -> Result.failure(OrderError.userNotFound(userId)));
    }

    private Result<Book, OrderError> findBook(String bookId) {
        return bookRepository.findById(bookId)
                .map(Result::<Book, OrderError>success)
                .orElseGet(() -> Result.failure(OrderError.bookNotFound(bookId)));
    }

    private Result<Order, OrderError> validateAndCreate(User user, Book book) {
        // Перевірка користувача
        if (!user.canBorrow()) {
            String reason = user.getOverdueBooks() > 0
                    ? "Has overdue books"
                    : "Reached borrow limit";
            return Result.failure(OrderError.userCannotBorrow(user.getId(), reason));
        }

        // Перевірка книги
        if (!book.isAvailable()) {
            return Result.failure(OrderError.bookNotAvailable(book.getId(), book.getTitle()));
        }

        // Створення замовлення
        Order order = Order.create(user, book);

        // Збереження
        bookRepository.save(book);
        userRepository.save(user);
        orderRepository.save(order);

        return Result.success(order);
    }
}

Обробка результату в CLI

package com.library.presentation.cli;

import com.library.application.OrderApplicationService;
import com.library.common.Result;

public class OrderMenu {

    private final OrderApplicationService orderService;

    public OrderMenu(OrderApplicationService orderService) {
        this.orderService = orderService;
    }

    public void handleCreateOrder(String userId, String bookId) {
        orderService.createOrder(userId, bookId)
                .onSuccess(order -> {
                    System.out.println("✅ Замовлення створено!");
                    System.out.println("   ID: " + order.getId());
                    System.out.println("   Книга: " + order.getBook().getTitle());
                })
                .onFailure(error -> {
                    System.out.println("❌ Помилка: " + error.getMessage());
                });

        // Або через fold:
        String message = orderService.createOrder(userId, bookId)
                .fold(
                        order -> "Замовлення " + order.getId() + " створено",
                        error -> "Помилка: " + error.getMessage()
                );
        System.out.println(message);
    }
}

Guard Clauses: Захисні умови

Проблема: Глибока вкладеність

// ❌ Антипатерн: Arrow Code / Deep Nesting
public void processOrder(Order order) {
    if (order != null) {
        if (order.getUser() != null) {
            if (order.getBook() != null) {
                if (order.isActive()) {
                    if (order.canModify()) {
                        // Нарешті логіка...
                    } else {
                        throw new IllegalStateException("Cannot modify");
                    }
                } else {
                    throw new IllegalStateException("Order not active");
                }
            } else {
                throw new IllegalArgumentException("Book is null");
            }
        } else {
            throw new IllegalArgumentException("User is null");
        }
    } else {
        throw new IllegalArgumentException("Order is null");
    }
}

Рішення: Guard Clauses

Guard Clause — умова, яка перевіряє аргумент і негайно виходить з методу (через return або throw).

// ✅ Guard Clauses: плоска структура
public void processOrder(Order order) {
    // Guards на початку
    if (order == null) {
        throw new IllegalArgumentException("Order cannot be null");
    }
    if (order.getUser() == null) {
        throw new IllegalArgumentException("Order user cannot be null");
    }
    if (order.getBook() == null) {
        throw new IllegalArgumentException("Order book cannot be null");
    }
    if (!order.isActive()) {
        throw new IllegalStateException("Order is not active");
    }
    if (!order.canModify()) {
        throw new IllegalStateException("Order cannot be modified");
    }

    // Основна логіка — без вкладеності
    performOrderProcessing(order);
}

Утилітний клас Preconditions

package com.library.common;

import java.util.Collection;
import java.util.Objects;

/**
 * Утилітний клас для захисних умов.
 * Натхненний Guava Preconditions.
 */
public final class Preconditions {

    private Preconditions() {}

    // === Null checks ===

    /**
     * Перевіряє, що об'єкт не null.
     */
    public static <T> T requireNonNull(T obj, String paramName) {
        if (obj == null) {
            throw new IllegalArgumentException(paramName + " cannot be null");
        }
        return obj;
    }

    // === String checks ===

    /**
     * Перевіряє, що рядок не null і не порожній.
     */
    public static String requireNonBlank(String str, String paramName) {
        requireNonNull(str, paramName);
        if (str.isBlank()) {
            throw new IllegalArgumentException(paramName + " cannot be blank");
        }
        return str;
    }

    // === Number checks ===

    /**
     * Перевіряє, що число додатне.
     */
    public static int requirePositive(int value, String paramName) {
        if (value <= 0) {
            throw new IllegalArgumentException(paramName + " must be positive, got: " + value);
        }
        return value;
    }

    /**
     * Перевіряє, що число невід'ємне.
     */
    public static int requireNonNegative(int value, String paramName) {
        if (value < 0) {
            throw new IllegalArgumentException(paramName + " cannot be negative, got: " + value);
        }
        return value;
    }

    /**
     * Перевіряє, що число в діапазоні.
     */
    public static int requireInRange(int value, int min, int max, String paramName) {
        if (value < min || value > max) {
            throw new IllegalArgumentException(
                    String.format("%s must be between %d and %d, got: %d",
                            paramName, min, max, value));
        }
        return value;
    }

    // === Collection checks ===

    /**
     * Перевіряє, що колекція не порожня.
     */
    public static <T extends Collection<?>> T requireNonEmpty(T collection, String paramName) {
        requireNonNull(collection, paramName);
        if (collection.isEmpty()) {
            throw new IllegalArgumentException(paramName + " cannot be empty");
        }
        return collection;
    }

    // === State checks ===

    /**
     * Перевіряє умову стану.
     */
    public static void checkState(boolean condition, String message) {
        if (!condition) {
            throw new IllegalStateException(message);
        }
    }

    /**
     * Перевіряє умову стану з форматуванням.
     */
    public static void checkState(boolean condition, String format, Object... args) {
        if (!condition) {
            throw new IllegalStateException(String.format(format, args));
        }
    }

    /**
     * Перевіряє умову аргументу.
     */
    public static void checkArgument(boolean condition, String message) {
        if (!condition) {
            throw new IllegalArgumentException(message);
        }
    }
}

Використання Preconditions у доменних об'єктах

package com.library.domain.model;

import static com.library.common.Preconditions.*;

public final class Book {

    private final String id;
    private final String isbn;
    private String title;
    private String author;
    private int availableCopies;
    private final int totalCopies;

    private Book(String id, String isbn, String title, String author, int totalCopies) {
        this.id = id;
        this.isbn = isbn;
        this.title = title;
        this.author = author;
        this.totalCopies = totalCopies;
        this.availableCopies = totalCopies;
    }

    public static Book create(String id, String isbn, String title,
                               String author, int totalCopies) {
        // Guards з зрозумілими повідомленнями
        requireNonBlank(id, "id");
        requireNonBlank(isbn, "isbn");
        requireNonBlank(title, "title");
        requireNonBlank(author, "author");
        requirePositive(totalCopies, "totalCopies");

        checkArgument(isbn.length() == 13, "ISBN must be 13 characters");

        return new Book(id, isbn, title, author, totalCopies);
    }

    public void borrow() {
        checkState(isAvailable(), "Cannot borrow: no copies available");
        availableCopies--;
    }

    public void returnCopy() {
        checkState(availableCopies < totalCopies,
                "Cannot return: all %d copies already returned", totalCopies);
        availableCopies++;
    }

    // ...
}

Ієрархія бізнес-виключень

Коли виключення доречні?

Виключення доречні для:

  • Справжніх виняткових ситуацій (помилка I/O, недоступність сервісу)
  • Порушення інваріантів (програмна помилка)
  • Ситуацій, де відновлення неможливе

Result доречний для:

  • Очікуваних альтернативних сценаріїв
  • Бізнес-помилок (книга недоступна, ліміт досягнуто)
  • API, де клієнт має обробити обидва випадки

Реалізація ієрархії

package com.library.domain.exception;

/**
 * Базовий виняток для всіх бізнес-помилок.
 */
public abstract class BusinessException extends RuntimeException {

    private final String errorCode;

    protected BusinessException(String errorCode, String message) {
        super(message);
        this.errorCode = errorCode;
    }

    protected BusinessException(String errorCode, String message, Throwable cause) {
        super(message, cause);
        this.errorCode = errorCode;
    }

    public String getErrorCode() {
        return errorCode;
    }
}
package com.library.domain.exception;

/**
 * Виняток, коли сутність не знайдена.
 */
public class EntityNotFoundException extends BusinessException {

    private final String entityType;
    private final String entityId;

    public EntityNotFoundException(String entityType, String entityId) {
        super(
                "ENTITY_NOT_FOUND",
                String.format("%s with id '%s' not found", entityType, entityId)
        );
        this.entityType = entityType;
        this.entityId = entityId;
    }

    public String getEntityType() { return entityType; }
    public String getEntityId() { return entityId; }
}
package com.library.domain.exception;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * Виняток при помилках валідації.
 */
public class ValidationException extends BusinessException {

    private final List<ValidationError> errors;

    public ValidationException(String message) {
        super("VALIDATION_ERROR", message);
        this.errors = Collections.emptyList();
    }

    public ValidationException(List<ValidationError> errors) {
        super("VALIDATION_ERROR", formatErrors(errors));
        this.errors = new ArrayList<>(errors);
    }

    public List<ValidationError> getErrors() {
        return Collections.unmodifiableList(errors);
    }

    private static String formatErrors(List<ValidationError> errors) {
        return errors.stream()
                .map(ValidationError::toString)
                .reduce((a, b) -> a + "; " + b)
                .orElse("Validation failed");
    }

    /**
     * Record для окремої помилки валідації.
     */
    public record ValidationError(String field, String message) {
        @Override
        public String toString() {
            return field + ": " + message;
        }
    }
}
package com.library.domain.exception;

/**
 * Виняток при конфлікті стану.
 */
public class ConflictException extends BusinessException {

    public ConflictException(String message) {
        super("CONFLICT", message);
    }

    public static ConflictException duplicateEntity(String entityType, String field, Object value) {
        return new ConflictException(
                String.format("%s with %s '%s' already exists", entityType, field, value)
        );
    }

    public static ConflictException invalidStateTransition(String from, String to) {
        return new ConflictException(
                String.format("Cannot transition from %s to %s", from, to)
        );
    }
}
package com.library.domain.exception;

/**
 * Виняток при порушенні бізнес-правила.
 */
public class BusinessRuleViolationException extends BusinessException {

    private final String ruleName;

    public BusinessRuleViolationException(String ruleName, String message) {
        super("BUSINESS_RULE_VIOLATION", message);
        this.ruleName = ruleName;
    }

    public String getRuleName() {
        return ruleName;
    }

    public static BusinessRuleViolationException borrowLimitExceeded(int limit) {
        return new BusinessRuleViolationException(
                "BORROW_LIMIT",
                String.format("Borrow limit of %d books exceeded", limit)
        );
    }

    public static BusinessRuleViolationException hasOverdueBooks(int count) {
        return new BusinessRuleViolationException(
                "OVERDUE_BOOKS",
                String.format("Cannot borrow: %d overdue book(s)", count)
        );
    }
}

Обробка виключень у CLI

package com.library.presentation.cli;

import com.library.domain.exception.*;

public class ExceptionHandler {

    public void handle(Exception e) {
        switch (e) {
            case EntityNotFoundException ex ->
                System.err.printf("❌ Не знайдено: %s%n", ex.getMessage());

            case ValidationException ex -> {
                System.err.println("❌ Помилки валідації:");
                ex.getErrors().forEach(error ->
                    System.err.printf("   - %s: %s%n", error.field(), error.message())
                );
            }

            case ConflictException ex ->
                System.err.printf("❌ Конфлікт: %s%n", ex.getMessage());

            case BusinessRuleViolationException ex ->
                System.err.printf("❌ Порушення правила '%s': %s%n",
                        ex.getRuleName(), ex.getMessage());

            case BusinessException ex ->
                System.err.printf("❌ Помилка [%s]: %s%n",
                        ex.getErrorCode(), ex.getMessage());

            default ->
                System.err.printf("❌ Невідома помилка: %s%n", e.getMessage());
        }
    }
}

Валідація: Де і як?

Рівні валідації

Loading diagram...
graph TB
    subgraph "Presentation"
        A[CLI Input Validation<br/>Формат, наявність]
    end

    subgraph "Application"
        B[Service Validation<br/>Бізнес-правила]
    end

    subgraph "Domain"
        C[Invariant Validation<br/>Конструктори, методи]
    end

    A --> B --> C

    style A fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style B fill:#f59e0b,stroke:#b45309,color:#ffffff
    style C fill:#22c55e,stroke:#15803d,color:#ffffff
РівеньЩо перевіряємоПриклади
PresentationФормат вводуEmail має @, рік — число
ApplicationБізнес-контекстКористувач існує, книга доступна
DomainІнваріантиtotalCopies > 0, isbn валідний

Validator Pattern

package com.library.domain.validation;

import com.library.domain.exception.ValidationException;
import com.library.domain.exception.ValidationException.ValidationError;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;

/**
 * Builder для валідації з накопиченням помилок.
 */
public final class Validator<T> {

    private final T value;
    private final List<ValidationError> errors = new ArrayList<>();

    private Validator(T value) {
        this.value = value;
    }

    public static <T> Validator<T> of(T value) {
        return new Validator<>(value);
    }

    /**
     * Додає правило валідації.
     */
    public Validator<T> validate(Predicate<T> predicate, String field, String message) {
        if (!predicate.test(value)) {
            errors.add(new ValidationError(field, message));
        }
        return this;
    }

    /**
     * Додає правило для поля.
     */
    public <V> Validator<T> validateField(V fieldValue, String field,
                                           Predicate<V> predicate, String message) {
        if (!predicate.test(fieldValue)) {
            errors.add(new ValidationError(field, message));
        }
        return this;
    }

    /**
     * Перевіряє, чи є помилки.
     */
    public boolean hasErrors() {
        return !errors.isEmpty();
    }

    /**
     * Повертає список помилок.
     */
    public List<ValidationError> getErrors() {
        return List.copyOf(errors);
    }

    /**
     * Повертає значення або кидає ValidationException.
     */
    public T getOrThrow() {
        if (hasErrors()) {
            throw new ValidationException(errors);
        }
        return value;
    }

    /**
     * Повертає Result замість виключення.
     */
    public Result<T, List<ValidationError>> toResult() {
        if (hasErrors()) {
            return Result.failure(errors);
        }
        return Result.success(value);
    }
}

Використання Validator

package com.library.application;

import com.library.domain.validation.Validator;

public class UserRegistrationService {

    public record RegistrationRequest(
            String name,
            String email,
            String password,
            int age
    ) {}

    public Result<User, List<ValidationError>> register(RegistrationRequest request) {
        return Validator.of(request)
                .validateField(request.name(), "name",
                        name -> name != null && !name.isBlank(),
                        "Name is required")
                .validateField(request.name(), "name",
                        name -> name == null || name.length() <= 100,
                        "Name must be at most 100 characters")
                .validateField(request.email(), "email",
                        email -> email != null && email.contains("@"),
                        "Valid email is required")
                .validateField(request.password(), "password",
                        pwd -> pwd != null && pwd.length() >= 8,
                        "Password must be at least 8 characters")
                .validateField(request.age(), "age",
                        age -> age >= 18 && age <= 120,
                        "Age must be between 18 and 120")
                .toResult()
                .flatMap(this::createUser);
    }

    private Result<User, List<ValidationError>> createUser(RegistrationRequest request) {
        // Перевірка унікальності email
        if (userRepository.findByEmail(request.email()).isPresent()) {
            return Result.failure(List.of(
                    new ValidationError("email", "Email already registered")
            ));
        }

        User user = User.create(
                generateId(),
                request.name(),
                Email.of(request.email()),
                UserType.REGULAR
        );

        userRepository.save(user);
        return Result.success(user);
    }
}

Підсумки

Result Pattern

Явний контракт: метод завжди повертає результат. Компілятор змушує обробити обидва випадки.

Guard Clauses

Захисні умови на початку методу. Плоска структура замість глибокої вкладеності.

Ієрархія виключень

Типізовані помилки: EntityNotFoundException, ValidationException, ConflictException.

Validator Pattern

Накопичення помилок замість fail-fast. Зручний builder API.

Рекомендації

СитуаціяРекомендація
Очікувана альтернативаResult<T, E>
Програмна помилкаIllegalArgumentException, IllegalStateException
Помилка I/O, мережіRuntimeException або Result
Валідація з кількома полямиValidator з накопиченням
API для зовнішніх клієнтівResult — явний контракт

Корисні посилання