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

Практика 6. Расширения и композиция поведения

Цель

Применить расширения для организации API, реализовать поведение по умолчанию в protocol extensions и заменить разрастающуюся иерархию композицией стратегий.

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

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

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

Нужно рассчитать итоговую стоимость заказа. На цену независимо влияют скидка и стоимость доставки. Решение должно позволять комбинировать стратегии без создания подкласса для каждого сочетания.

Задание 1. Доменные модели

struct CartItem: Equatable {
let name: String
let unitPrice: Decimal
let quantity: Int
}
struct Cart {
private(set) var items: [CartItem] = []
}

Добавьте проверяемые способы создания. Цена неотрицательная, количество положительное, название после удаления пробелов не пустое.

Задание 2. Расширения модели

В отдельных extensions реализуйте:

  • CartItem.subtotal как вычисляемое свойство;
  • Cart.add(_:);
  • Cart.subtotal;
  • соответствие CartItem протоколу CustomStringConvertible.

Не добавляйте глобальные функции и не дублируйте формулу стоимости.

Задание 3. Ограниченное расширение

Добавьте коллекциям элементов CartItem вычисляемую общую стоимость.

extension Collection where Element == CartItem {
var total: Decimal {
fatalError("TODO")
}
}

Проверьте работу для массива и для ArraySlice<CartItem>.

Задание 4. Стратегии скидки

protocol DiscountPolicy {
func discount(for subtotal: Decimal) -> Decimal
}
extension DiscountPolicy {
func finalPrice(for subtotal: Decimal) -> Decimal {
max(0, subtotal - discount(for: subtotal))
}
}

Реализуйте:

  • NoDiscount;
  • PercentDiscount с проверкой диапазона 0...100;
  • ThresholdDiscount, предоставляющий фиксированную скидку после достижения порога.

Задание 5. Стратегии доставки

Создайте протокол ShippingPolicy с методом расчёта доставки. Реализуйте:

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

Все входные параметры должны сохранять корректные инварианты.

Задание 6. Композиция

struct CheckoutCalculator {
let discountPolicy: any DiscountPolicy
let shippingPolicy: any ShippingPolicy
func total(for cart: Cart) -> Decimal {
fatalError("TODO")
}
}

Алгоритм:

  1. получить промежуточную стоимость корзины;
  2. применить скидку;
  3. рассчитать доставку по цене после скидки;
  4. вернуть сумму цены после скидки и доставки.

Создайте не менее шести комбинаций стратегий. Для добавления новой комбинации нельзя создавать новый класс калькулятора.

Задание 7. Требование и метод extension

Добавьте в DiscountPolicy требование label. Предоставьте значение по умолчанию в extension, затем переопределите его в одной стратегии. Покажите вызов через any DiscountPolicy.

После этого создайте дополнительный метод только в extension и экспериментально сравните диспетчеризацию. Вывод объясните в README.

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

Создайте OrderSummary и добавьте ему условное соответствие Equatable, если все содержащиеся значения сравнимы. Либо реализуйте generic-обёртку Box<Value> с условными соответствиями Equatable и CustomStringConvertible.

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

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

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

РазделДоля
Модели и инварианты15%
Расширения и ограниченный API20%
Стратегии скидки15%
Стратегии доставки15%
Композиция и комбинации25%
Проверки и объяснение диспетчеризации10%

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

  1. Почему subtotal является вычисляемым свойством?
  2. Почему extension не может добавить хранимое свойство?
  3. Когда ограниченное расширение доступно коллекции?
  4. Что предоставляет protocol extension?
  5. Почему label объявлен требованием протокола?
  6. Как выбирается реализация метода, существующего только в extension?
  7. Почему скидка и доставка представлены отдельными стратегиями?
  8. Сколько подклассов понадобилось бы для всех комбинаций поведения?
  9. Как добавить новую стратегию, не изменяя калькулятор?
  10. Когда наследование было бы уместнее композиции?