Лекция 6. Расширения, protocol extensions и композиция поведения
1. Расширение существующего типа
extension добавляет типу новые вычисляемые свойства, методы, инициализаторы и
соответствия протоколам без изменения исходного объявления.
extension String { var trimmed: String { trimmingCharacters(in: .whitespacesAndNewlines) }}Расширение не создаёт подкласс и не оборачивает значение. Новый член становится частью интерфейса типа в текущем модуле.
2. Ограничения расширений
Расширение не может добавить хранимое свойство, потому что это изменило бы размер и расположение уже существующего типа.
extension Int { var isEven: Bool { self % 2 == 0 }}Вычисляемое свойство допустимо, поскольку не требует отдельного хранилища.
Расширения также не предназначены для переопределения существующих методов класса. Для изменяемого полиморфного поведения используют наследование, протокол или композицию.
3. Организация кода по соответствиям
Полезно отделять основную модель от реализаций вторичных контрактов.
struct Product { let id: Int let name: String}
extension Product: Equatable { static func == (lhs: Product, rhs: Product) -> Bool { lhs.id == rhs.id }}
extension Product: CustomStringConvertible { var description: String { "#\(id) \(name)" }}Так основное объявление показывает доменные данные, а расширения группируют интеграцию с другими частями программы.
4. Расширение протокола
Protocol extension предоставляет реализацию, доступную всем соответствующим типам.
protocol Summarizable { var title: String { get } var details: String { get }}
extension Summarizable { func summary() -> String { "\(title): \(details)" }}struct Task: Summarizable { let title: String let details: String}Task получает готовый summary() без базового класса.
5. Требование и дополнительный метод
Динамика вызова зависит от того, объявлен ли метод требованием протокола.
protocol Greeter { func greet() -> String}
extension Greeter { func greet() -> String { "Здравствуйте" } func debugName() -> String { "Greeter" }}
struct Student: Greeter { func greet() -> String { "Привет" } func debugName() -> String { "Student" }}Через any Greeter вызов greet() использует реализацию соответствующего типа,
поскольку метод входит в контракт. Дополнительный debugName() из extension не
является таким же полиморфным требованием. Если нужна сменяемая реализация, метод
следует объявить в протоколе.
6. Ограниченные расширения
Реализацию можно предоставить только типам с дополнительными свойствами.
extension Collection where Element: Numeric { var total: Element { reduce(.zero, +) }}Метод доступен коллекциям чисел, но не коллекциям произвольных элементов. Условия делают API точным и сохраняют статическую проверку типов.
7. Условное соответствие
Generic-тип может соответствовать протоколу только при условии, что его параметр также соответствует необходимому контракту.
struct Box<Value> { let value: Value}
extension Box: Equatable where Value: Equatable {}Box<Int> сравним, а Box со значением несравнимого типа не получает оператор
равенства.
8. Композиция поведения
Вместо глубокой иерархии объект получает небольшие компоненты и делегирует им работу.
protocol Pricing { func price(base: Decimal) -> Decimal}
struct RegularPricing: Pricing { func price(base: Decimal) -> Decimal { base }}
struct DiscountPricing: Pricing { let percent: Decimal
func price(base: Decimal) -> Decimal { base * (1 - percent / 100) }}
struct ProductOffer { let basePrice: Decimal let pricing: any Pricing
var finalPrice: Decimal { pricing.price(base: basePrice) }}Стратегия цены заменяется независимо от модели предложения. Не требуется создавать
DiscountProduct, SeasonalDiscountProduct и другие подклассы.
9. Поведение по умолчанию как примесь
Небольшие протоколы с реализациями по умолчанию напоминают примеси: тип собирает несколько возможностей без множественного наследования классов.
protocol Timestamped { var createdAt: Date { get }}
extension Timestamped { func age(reference: Date = Date()) -> TimeInterval { reference.timeIntervalSince(createdAt) }}Примесь не должна прятать важное состояние или создавать неочевидные зависимости.
10. Namespace через пустой тип
Расширения можно использовать для группировки фабрик и констант.
enum ValidationRules {}
extension ValidationRules { static let minimumPasswordLength = 8
static func validPassword(_ value: String) -> Bool { value.count >= minimumPasswordLength }}Пустой enum нельзя создать, поэтому он служит только пространством имён.
11. Retroactive conformance
Можно добавить существующему типу соответствие существующему протоколу, но такое решение требует осторожности. Два модуля могут по-разному трактовать равенство, сортировку или кодирование одного внешнего типа.
Если семантика неочевидна или тип и протокол принадлежат другим модулям, безопаснее создать собственную обёртку.
struct NormalizedEmail: Hashable { let value: String
init(_ raw: String) { value = raw.trimmingCharacters(in: .whitespaces).lowercased() }}12. Проектирование API расширений
Хорошее расширение:
- имеет ясную связь с исходным типом;
- не создаёт неожиданных побочных эффектов;
- использует точные ограничения;
- не засоряет глобальный API слишком общими именами;
- располагается рядом с ответственностью или соответствием протоколу.
Типичные ошибки
- попытка добавить хранимое состояние через extension;
- огромный файл расширений, не связанный с предметным смыслом;
- ожидание динамического переопределения дополнительного метода протокола;
- поведение по умолчанию скрывает обязательную операцию;
- глубокая иерархия сохраняется там, где достаточно сменяемой стратегии;
- внешнему типу добавляется спорное глобальное соответствие.
Вопросы для самопроверки
- Что можно добавить через extension?
- Почему нельзя добавить хранимое свойство?
- Зачем выносить соответствия протоколам в отдельные расширения?
- Что даёт реализация в protocol extension?
- Почему метод лучше объявить требованием протокола, если он должен быть полиморфным?
- Что делает ограниченное расширение?
- Что такое условное соответствие?
- Как композиция уменьшает глубину иерархии?
- Когда полезен пустой
enumкак namespace? - Почему retroactive conformance может быть спорным?
Итоги
- extension расширяет API типа без создания подкласса;
- хранимое состояние через extension добавить нельзя;
- protocol extension предоставляет общее поведение;
- полиморфно заменяемые методы следует включать в требования протокола;
- ограничения делают расширение доступным только подходящим типам;
- композиция поведения строится из небольших сменяемых компонентов;
- спорную семантику внешнего типа лучше выражать собственной обёрткой.