Почему традиционная архитектура не справляется с современными задачами?
Вы когда-нибудь сталкивались с ситуацией, когда простое добавление новой фичи в 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)
}