CQRS и Event Sourcing в Swift: Архитектура для сложных проектов

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Почему традиционная архитектура не справляется с современными задачами?

Вы когда-нибудь сталкивались с ситуацией, когда простое добавление новой фичи в Swift-приложение превращается в кошмар? Баги сыплются, данные теряются, а логика разрастается до невероятных размеров? Знакомо? Добро пожаловать в мир, где CQRS и Event Sourcing становятся вашими лучшими друзьями.

Эти паттерны — не просто модные слова. Это способ мышления, который кардинально меняет подход к построению приложений на Swift. И сегодня мы разберем их на реальных примерах. Без воды, без заумных теорий — только практика.

Что такое CQRS? Разделяй и властвуй

CQRS (Command Query Responsibility Segregation) — это принцип, который говорит: "Хватит мешать чтение и запись в одном классе". Звучит просто, но последствия огромны.

В классической CRUD (Create, Read, Update, Delete) архитектуре вы используете одну модель для всего. Например, User. Он и сохраняется, и читается, и обновляется. А что если для чтения нужны одни поля, а для записи — другие? Начинаются костыли: опциональные поля, флаги, маппинги...

CQRS предлагает разделить ответственность:

  • Команды (Commands) — изменяют состояние. Они мутируют данные, но ничего не возвращают.
  • Запросы (Queries) — читают данные. Они не меняют состояние, только возвращают результат.

В Swift это может выглядеть так:

// Команда — только мутация
struct CreateUserCommand {
    let id: UUID
    let name: String
    let email: String
}

// Запрос — только чтение
struct GetUserQuery {
    let userId: UUID
}

// Обработчик команды
class CreateUserCommandHandler {
    func handle(_ command: CreateUserCommand) {
        // Логика создания пользователя
        print("Создан пользователь: \(command.name)")
    }
}

// Обработчик запроса
class GetUserQueryHandler {
    func handle(_ query: GetUserQuery) -> User? {
        // Логика получения пользователя
        return User(id: query.userId, name: "Алексей", email: "alex@example.com")
    }
}

Видите? Никакого смешения. Каждый класс делает ровно одну вещь. Это упрощает тестирование, поддержку и масштабирование.

Event Sourcing: Храним историю, а не состояние

Теперь переходим к Event Sourcing — паттерну, который хранит не текущее состояние объекта, а всю последовательность событий, которые к нему привели.

Представьте банковский счет. Вместо того чтобы хранить баланс (1000 рублей), вы храните список событий: "Открытие счета", "Пополнение на 500", "Снятие 200", "Пополнение на 700". Чтобы узнать текущий баланс, вы просто воспроизводите все события. Это дает невероятные возможности: аудит, откат изменений, аналитика.

Реализуем это на Swift:

// Событие
protocol Event {}

struct AccountCreated: Event {
    let accountId: UUID
    let initialBalance: Double
}

struct MoneyDeposited: Event {
    let accountId: UUID
    let amount: Double
}

struct MoneyWithdrawn: Event {
    let accountId: UUID
    let amount: Double
}

// Агрегат — восстанавливает состояние из событий
class BankAccount {
    private(set) var balance: Double = 0
    private(set) var isActive: Bool = false
    private(set) var accountId: UUID?

    func apply(_ event: Event) {
        switch event {
        case let created as AccountCreated:
            accountId = created.accountId
            balance = created.initialBalance
            isActive = true
        case let deposited as MoneyDeposited:
            balance += deposited.amount
        case let withdrawn as MoneyWithdrawn:
            balance -= withdrawn.amount
        default:
            break
        }
    }

    // Восстановление из истории событий
    static func load(from history: [Event]) -> BankAccount {
        let account = BankAccount()
        for event in history {
            account.apply(event)
        }
        return account
    }
}

Круто, правда? Теперь у вас есть полная история изменений. Никаких "как так получилось, что баланс отрицательный?" — вы всегда можете воспроизвести всю цепочку.

Объединяем CQRS и Event Sourcing в Swift

А теперь самое интересное — комбинация обоих паттернов. CQRS отвечает за разделение команд и запросов, а Event Sourcing — за хранение истории. Вместе они образуют мощную архитектуру.

Давайте построим простую систему управления заказами для интернет-магазина на Swift.

1. Определяем события

// События заказа
struct OrderCreated: Event {
    let orderId: UUID
    let customerId: UUID
    let items: [OrderItem]
    let createdAt: Date
}

struct OrderItemAdded: Event {
    let orderId: UUID
    let item: OrderItem
}

struct OrderItemRemoved: Event {
    let orderId: UUID
    let itemId: UUID
}

struct OrderSubmitted: Event {
    let orderId: UUID
    let submittedAt: Date
}

struct OrderCancelled: Event {
    let orderId: UUID
    let reason: String
}

2. Агрегат заказа

class OrderAggregate {
    private(set) var orderId: UUID?
    private(set) var customerId: UUID?
    private(set) var items: [OrderItem] = []
    private(set) var status: OrderStatus = .draft
    private(set) var createdAt: Date?
    private(set) var submittedAt: Date?

    enum OrderStatus {
        case draft, submitted, cancelled
    }

    func apply(_ event: Event) {
        switch event {
        case let created as OrderCreated:
            orderId = created.orderId
            customerId = created.customerId
            items = created.items
            status = .draft
            createdAt = created.createdAt
        case let added as OrderItemAdded:
            items.append(added.item)
        case let removed as OrderItemRemoved:
            items.removeAll { $0.id == removed.itemId }
        case let submitted as OrderSubmitted:
            status = .submitted
            submittedAt = submitted.submittedAt
        case let cancelled as OrderCancelled:
            status = .cancelled
        default:
            break
        }
    }

    // Команды
    func addItem(_ item: OrderItem) -> Event {
        guard status == .draft else {
            fatalError("Нельзя добавить товар в оформленный заказ")
        }
        return OrderItemAdded(orderId: orderId!, item: item)
    }

    func submit() -> Event {
        guard status == .draft else {
            fatalError("Заказ уже оформлен или отменен")
        }
        return OrderSubmitted(orderId: orderId!, submittedAt: Date())
    }

    static func load(from history: [Event]) -> OrderAggregate {
        let order = OrderAggregate()
        for event in history {
            order.apply(event)
        }
        return order
    }
}

3. Команды и обработчики

// Команды
struct CreateOrderCommand: Command {
    let customerId: UUID
    let items: [OrderItem]
}

struct AddItemCommand: Command {
    let orderId: UUID
    let item: OrderItem
}

struct SubmitOrderCommand: Command {
    let orderId: UUID
}

// Обработчик команд
class OrderCommandHandler {
    private let eventStore: EventStore

    init(eventStore: EventStore) {
        self.eventStore = eventStore
    }

    func handle(_ command: CreateOrderCommand) throws {
        let orderId = UUID()
        let event = OrderCreated(
            orderId: orderId,
            customerId: command.customerId,
            items: command.items,
            createdAt: Date()
        )
        try eventStore.save(event, for: orderId)
    }                

Блоги

Рекомендаций по книгам