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

Что такое контракт?

3.0 Senior🔥 51 комментариев
#SOLID и паттерны проектирования

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

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

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

Контракт в программировании: определение и практическое применение

Контракт (contract) — это одна из самых важных концепций в разработке. Это соглашение между компонентами код о том, что они ожидают друг от друга и что гарантируют. Давайте разберемся подробно.

Определение контракта

Контракт состоит из трех частей:

1. Предусловие (Precondition) — что должно быть истинно ДО вызова

public int divide(int a, int b) {
    // Предусловие: b != 0
    // Если это не так, результат не определен
    return a / b;
}

// Корректный вызов соблюдает контракт
divide(10, 2);   // OK, b != 0

// Нарушение контракта
divide(10, 0);   // Предусловие нарушено!

2. Постусловие (Postcondition) — что гарантируется ПОСЛЕ вызова

public String getUserEmail(Long userId) {
    // Постусловие: возвращается валидный email
    // если пользователь существует
    // или null если не существует
    return userRepository.findById(userId)
        .map(User::getEmail)
        .orElse(null);
}

3. Инвариант (Invariant) — что всегда остается истинным

public class BankAccount {
    private double balance;  // Инвариант: balance >= 0
    
    public void deposit(double amount) {
        // После этого метода: balance > 0
        balance += amount;
    }
    
    public void withdraw(double amount) {
        if (amount > balance) {
            throw new IllegalArgumentException("Insufficient funds");
        }
        // Инвариант сохраняется: balance >= 0
        balance -= amount;
    }
}

Контракт в методах

Пример с явным документированием контракта:

public class PaymentService {
    /**
     * Обрабатывает платеж
     * 
     * Предусловия:
     * - amount > 0
     * - account должен существовать
     * - account должен быть активным
     * - баланс >= amount
     * 
     * Постусловия:
     * - Платеж обработан и сохранен в БД
     * - Email отправлен пользователю
     * - Баланс уменьшен на amount
     * 
     * Исключения:
     * - IllegalArgumentException если amount <= 0
     * - AccountNotFoundException если аккаунта нет
     * - InsufficientFundsException если мало денег
     */
    public void processPayment(Long accountId, double amount) 
            throws AccountNotFoundException, InsufficientFundsException {
        
        // Проверка предусловий
        if (amount <= 0) {
            throw new IllegalArgumentException("Amount must be positive");
        }
        
        Account account = accountRepository.findById(accountId)
            .orElseThrow(() -> new AccountNotFoundException("Account not found"));
        
        if (!account.isActive()) {
            throw new IllegalArgumentException("Account is not active");
        }
        
        if (account.getBalance() < amount) {
            throw new InsufficientFundsException("Insufficient funds");
        }
        
        // Выполнение операции (постусловия должны быть выполнены)
        account.setBalance(account.getBalance() - amount);
        accountRepository.save(account);
        
        Payment payment = new Payment(accountId, amount);
        paymentRepository.save(payment);
        
        emailService.sendConfirmation(account.getEmail(), amount);
    }
}

Контракт в интерфейсах

Interface как контракт:

// Контракт: что должен реализовать любой Repository
public interface Repository<T, ID> {
    /**
     * Сохраняет сущность
     * 
     * Предусловие: entity != null
     * Постусловие: сущность сохранена и имеет ID
     */
    T save(T entity);
    
    /**
     * Получает сущность по ID
     * 
     * Предусловие: id != null
     * Постусловие: возвращает Optional с сущностью или пусто
     */
    Optional<T> findById(ID id);
}

// Реализация должна следовать контракту
public class UserRepository implements Repository<User, Long> {
    @Override
    public User save(User entity) {
        if (entity == null) {
            throw new IllegalArgumentException("Entity cannot be null");
        }
        // Сохранить в БД
        return entity;
    }
    
    @Override
    public Optional<User> findById(Long id) {
        if (id == null) {
            throw new IllegalArgumentException("ID cannot be null");
        }
        // Найти в БД
        return Optional.empty();
    }
}

Design by Contract (DbC)

Этот подход разработан Бертраном Мейером:

// Аналогия с реальным контрактом
public interface CarRentalContract {
    /**
     * ОБЯЗАТЕЛЬСТВА КЛИЕНТА (Precondition):
     * - Иметь водительские права
     * - Оплатить залог
     * - Вернуть машину в срок
     * 
     * ГАРАНТИИ КОМПАНИИ (Postcondition):
     * - Предоставить чистую машину
     * - Машина в хорошем состоянии
     * - 24/7 поддержка
     * 
     * УСЛОВИЯ (Invariant):
     * - Машина застрахована
     * - Бак заполнен
     */
    Car rentCar(Driver driver, int days, double deposit);
}

Контракт API

REST API контракт:

@RestController
@RequestMapping("/api/v1/users")
public class UserController {
    /**
     * GET /api/v1/users/{id}
     * 
     * Предусловия:
     * - id должен быть валидным UUID или числом
     * - пользователь должен быть аутентифицирован
     * 
     * Постусловия:
     * - Если пользователь существует: 200 OK с JSON
     * - Если не существует: 404 Not Found
     * - Если неавторизован: 401 Unauthorized
     * 
     * Response body:
     * {
     *   "id": "long",
     *   "email": "string",
     *   "name": "string",
     *   "createdAt": "ISO 8601 datetime"
     * }
     */
    @GetMapping("/{id}")
    public ResponseEntity<UserDTO> getUser(@PathVariable Long id) {
        User user = userRepository.findById(id)
            .orElseThrow(() -> new UserNotFoundException("User not found"));
        return ResponseEntity.ok(userMapper.toDTO(user));
    }
}

Контракт в многопоточности

Контракт для thread-safe кода:

public class ThreadSafeCounter {
    // Инвариант: value всегда integer
    private int value = 0;
    
    /**
     * Предусловие: нет (может быть вызван из любого потока)
     * Постусловие: value увеличен на 1 (атомарно)
     * Инвариант: операция thread-safe
     */
    public synchronized void increment() {
        value++;  // Синхронизация гарантирует атомарность
    }
    
    public synchronized int getValue() {
        return value;
    }
}

Нарушение контракта: примеры багов

// ❌ Нарушение контракта — возвращается null
public String getUserEmail(Long id) {
    // Контракт обещал: "возвращает email"
    // Но иногда возвращает null
    User user = userRepository.findById(id).orElse(null);
    return user.getEmail();  // NullPointerException!
}

// ✅ Правильно — явно обрабатываем случай
public String getUserEmail(Long id) {
    return userRepository.findById(id)
        .map(User::getEmail)
        .orElse(null);  // Явно предусмотрено в контракте
}

// ❌ Нарушение контракта — побочный эффект
public List<User> getActiveUsers() {
    // Контракт обещал только вернуть пользователей
    // Но вы удаляете неактивных!
    List<User> users = userRepository.findAll();
    userRepository.deleteInactive();  // Побочный эффект!
    return users.stream().filter(User::isActive).collect(toList());
}

// ✅ Правильно — только чтение
public List<User> getActiveUsers() {
    return userRepository.findByActive(true);
}

Best Practices контрактов

// 1. Документируйте контракты в JavaDoc
/**
 * Сохраняет пользователя
 * @param user не должен быть null
 * @return сохраненный пользователь с установленным ID
 * @throws IllegalArgumentException если user null
 * @throws DataIntegrityViolationException если email уже существует
 */
public User save(User user) { }

// 2. Используйте @NotNull, @Nullable для явности
public User findById(@NotNull Long id) {
    // Контракт: id не может быть null
}

public Optional<User> findByEmail(@Nullable String email) {
    // Контракт: email может быть null
}

// 3. Выбрасывайте исключения при нарушении
if (entity == null) {
    throw new IllegalArgumentException("Entity cannot be null");
}

// 4. Используйте Objects.requireNonNull()
public void process(String data) {
    this.data = Objects.requireNonNull(data, "Data cannot be null");
}

Вывод

Контракт — это соглашение о:

  • Предусловиях (что должно быть истинно перед вызовом)
  • Постусловиях (что гарантируется после вызова)
  • Инвариантах (что всегда остается истинным)

Правильное определение и соблюдение контрактов:

  • Упрощает понимание кода
  • Предотвращает баги
  • Делает API предсказуемым
  • Облегчает тестирование
  • Улучшает надежность всей системы

Соблюдение контрактов — признак опытного разработчика и критично для production code'а.

Что такое контракт? | PrepBro