Перейти к содержимому

Практика 5. Протоколы и сменяемые зависимости

Цель

Спроектировать сервис, который зависит от небольших протоколов, и доказать сменяемость реализаций без наследования и приведения типов.

Структура проекта

oop-swift-practice-05/
├── README.md
└── Sources/
└── main.swift

Предметная область

Сервис оформляет заказ: проверяет данные, сохраняет заказ и отправляет уведомление. Реальных баз данных и сетевых запросов не требуется.

Задание 1. Модель

struct Order: Equatable {
let id: Int
let email: String
let total: Decimal
}

Определите инварианты модели и создайте init?, запрещающий неположительный id, пустой email и отрицательную сумму.

Задание 2. Контракты

Объявите протоколы:

protocol OrderValidating {
func validate(_ order: Order) -> Bool
}
protocol OrderSaving {
func save(_ order: Order) -> Bool
}
protocol OrderNotifying {
func sendConfirmation(for order: Order)
}

Объясните, почему это три протокола, а не один OrderManager.

Задание 3. Реализации в памяти

Создайте:

  • DefaultOrderValidator;
  • MemoryOrderRepository, сохраняющий заказы в массиве;
  • MemoryOrderNotifier, сохраняющий идентификаторы уведомлённых заказов.

Откройте наружу состояние только для чтения через private(set).

Задание 4. Сервис

final class CheckoutService {
private let validator: any OrderValidating
private let repository: any OrderSaving
private let notifier: any OrderNotifying
init(
validator: any OrderValidating,
repository: any OrderSaving,
notifier: any OrderNotifying
) {
fatalError("TODO")
}
func checkout(_ order: Order) -> Bool {
fatalError("TODO")
}
}

Алгоритм:

  1. отклонить заказ, не прошедший проверку;
  2. попытаться сохранить заказ;
  3. отправить уведомление только после успешного сохранения;
  4. вернуть итог операции.

Сервис не должен создавать конкретные зависимости внутри себя.

Задание 5. Подмены для проверки

Реализуйте управляемые подмены:

final class StubValidator: OrderValidating {
let result: Bool
init(result: Bool) { self.result = result }
func validate(_ order: Order) -> Bool { result }
}

Добавьте аналогичную подмену репозитория и spy-уведомитель, считающий вызовы. Проверьте не менее четырёх сценариев:

  • валидация не пройдена;
  • сохранение завершилось ошибкой;
  • успешное оформление;
  • повторное оформление того же заказа по правилам выбранной реализации.

Задание 6. Композиция протоколов

Создайте функцию диагностики, принимающую объект, который одновременно умеет сохранять заказ и предоставлять читаемое описание состояния. Введите отдельный небольшой протокол для описания и используйте композицию any A & B.

Дополнительное задание

Создайте ConsoleOrderNotifier, не меняя CheckoutService. Покажите в README, какую часть программы пришлось изменить для выбора другой реализации.

Требования к сдаче

  • ссылка на Git-репозиторий;
  • Swift 6, запуск описан в README;
  • минимум три узких протокола и две реализации хотя бы одного контракта;
  • минимум 15 проверок assert;
  • зависимости передаются через инициализатор;
  • отсутствуют глобальное изменяемое состояние и fatalError("TODO").

Критерии оценки

РазделДоля
Модель и инварианты15%
Качество протоколов20%
Реализации зависимостей20%
Сервис и порядок операций25%
Подмены, проверки и README20%

Вопросы для защиты

  1. Почему сервис хранит зависимости как протокольные типы?
  2. Что означает any OrderSaving?
  3. Почему протоколы разделены?
  4. Можно ли выполнить { get } изменяемым свойством?
  5. Когда метод протокола должен быть mutating?
  6. Чем stub отличается от spy в этой работе?
  7. Почему уведомление отправляется после сохранения?
  8. Что пришлось изменить при добавлении консольного уведомителя?
  9. Как протоколы помогают тестировать сервис?
  10. Когда отдельный протокол был бы избыточен?