$ sudo teach IT

Зачем это нужно

Представьте, что вы строите поведение через классы: базовый класс Animal, от него FlyingAnimal, от него SwimmingFlyingAnimal... И тут появляется утка, которая плавает и летает, но не вписывается ни в одну ветку дерева, потому что у Swift нет множественного наследования классов. С протоколами эта проблема не возникает: вместо одного жёсткого дерева вы собираете поведение из независимых кусочков, как из конструктора. Нужна утке способность летать — добавьте протокол Flyable. Нужна способность плавать — добавьте Swimmable. Никакой общей иерархии для этого придумывать не надо.

Более того, в предыдущем уроке вы уже видели, что протокол может не только требовать метод, но и сам предлагать его реализацию через extension. Именно на этом и держится подход, который в документации Apple называется protocol-oriented programming (POP, программирование через протоколы) — способ строить поведение композицией протоколов и их реализациями по умолчанию вместо иерархии классов.

Реализация по умолчанию как основа

Когда несколько типов, соответствующих протоколу, реализуют требование одинаково, вы описываете это один раз в extension протокола — и все конформные типы получают метод бесплатно.

protocol Greeter {
    var name: String { get }
    func greet() -> String
}

extension Greeter {
    func greet() -> String {
        return "Привет, я \(name)."
    }
}

struct Employee: Greeter {
    let name: String
}

let e = Employee(name: "Аня")
print(e.greet()) // Привет, я Аня.

Employee соответствует протоколу Greeter, но нигде не пишет тело greet() — оно взято из extension. Если завтра появится ещё десять типов с таким же name, ни один из них не должен повторять этот код.

Собственная реализация вместо стандартной

Реализация по умолчанию — это именно значение по умолчанию, а не единственный вариант. Тип может объявить требование протокола сам, и тогда используется именно эта версия, а не та, что лежит в extension.

struct Manager: Greeter {
    let name: String

    func greet() -> String {
        return "Здравствуйте, я \(name), и я тут главный."
    }
}

let m = Manager(name: "Олег")
print(m.greet()) // Здравствуйте, я Олег, и я тут главный.

Manager — тоже Greeter, но у него своя формулировка. Компилятор выбирает реализацию из самого типа, а к общей заготовке из extension обращается только если тип ничего своего не предложил.

Композиция протоколов: A & B

Часто нужен не «какой-то один протокол», а сразу несколько сразу. Для этого в объявлении типа параметра или переменной протоколы перечисляют через & — это и называют композицией протоколов.

protocol Worker {
    var task: String { get }
    func work() -> String
}

extension Worker {
    func work() -> String {
        return "Сейчас я делаю: \(task)."
    }
}

struct Intern: Greeter, Worker {
    let name: String
    let task: String
}

func describe(_ person: Greeter & Worker) -> String {
    return "\(person.greet()) \(person.work())"
}

Параметр person: Greeter & Worker принимает любой тип, который соответствует ОБОИМ протоколам сразу — неважно, структура это, класс или перечисление. Не пришлось заводить общий базовый класс и не пришлось придумывать третий протокол-надстройку: два независимых требования просто складываются вместе там, где они нужны вместе.

Ограничение where Self в расширении протокола

Иногда реализация по умолчанию имеет смысл не для всех типов протокола, а только для тех, что дополнительно соответствуют ещё одному протоколу. Для этого у расширения указывают условие where Self: ДругойПротокол.

protocol Reportable {
    func report() -> String
}

extension Reportable where Self: Worker {
    func report() -> String {
        return "Отчёт: \(work())"
    }
}

Такая реализация report() появится только у типов, которые соответствуют одновременно Reportable и Worker: только для них внутри extension доступен метод work(). Тип, который подписался под Reportable, но не под Worker, эту заготовку не получит и обязан написать report() сам.

Частые ошибки

  • Путать протокол с классом: у протокола нет своего хранимого состояния, только требования к тому, что должно быть у конформного типа. Хранимые свойства по-прежнему объявляются в struct, class или enum.
  • Думать, что если метод есть в extension, то он есть у любого протокола — реализация по умолчанию появляется только у типов, которые формально объявили соответствие этому протоколу (через : ИмяПротокола).
  • Путать порядок выбора: сначала компилятор смотрит, объявил ли сам тип требование протокола; если да — используется реализация типа, если нет — реализация из extension.
  • Плодить лишние протоколы там, где хватило бы одного с двумя требованиями. Композиция A & B нужна, когда поведения действительно независимы и переиспользуются раздельно, а не когда их всегда используют вместе.

Резюме

  • Protocol-oriented programming — сборка поведения из протоколов и их реализаций по умолчанию вместо жёсткой иерархии классов.
  • Реализация в extension протокола становится реализацией по умолчанию для всех конформных типов.
  • Тип может переопределить требование протокола собственной реализацией — она выигрывает у заготовки из extension.
  • Композиция A & B задаёт тип, который соответствует сразу нескольким протоколам, без общего базового класса.
  • Условие where Self: ДругойПротокол ограничивает реализацию по умолчанию только теми типами, что соответствуют дополнительному протоколу.

Проверьте себя

4 вопроса

Сводка о сотруднике из двух протоколов

Даны два протокола с реализациями по умолчанию в extension: Greeter (умеет здороваться через greet()) и Worker (умеет рассказывать о своей задаче через work()). Часть типов используют реализацию по умолчанию как есть, часть — переопределяют один из методов собственной версией.

Реализуйте функцию func summarize(_ person: Greeter & Worker) -> String, которая принимает любое значение, соответствующее сразу обоим протоколам, и возвращает строку вида "РЕЗУЛЬТАТ_GREET РЕЗУЛЬТАТ_WORK" — то есть результат вызова greet(), затем один пробел, затем результат вызова work(). Функция не должна знать заранее, какая именно реализация (по умолчанию или собственная) сработает у конкретного типа — просто вызовите оба метода через параметр.