Лекция 5. Протоколы как средство абстракции
1. Абстракция через контракт
Протокол описывает набор требований, которым может соответствовать класс, структура или перечисление. Он отвечает на вопрос «что умеет значение», не фиксируя способ хранения данных и конкретную реализацию.
protocol Printable { func render() -> String}
struct Receipt: Printable { let number: Int
func render() -> String { "Чек №\(number)" }}Соответствие указывается после двоеточия. Один тип может соответствовать нескольким протоколам.
2. Требования к свойствам
Протокол не объявляет хранилище. Он задаёт доступность свойства.
protocol IdentifiableEntity { var id: Int { get } var title: String { get set }}get требует возможность чтения, get set — чтения и изменения. Требование можно
выполнить хранимым или вычисляемым свойством.
struct Course: IdentifiableEntity { let id: Int var title: String}Протокол не должен требовать изменения, если клиенту достаточно чтения.
3. Требования к методам
protocol Validator { func isValid(_ value: String) -> Bool}
struct EmailValidator: Validator { func isValid(_ value: String) -> Bool { value.contains("@") && !value.hasPrefix("@") }}Клиент зависит от Validator, поэтому конкретную стратегию можно заменить без
изменения клиентского кода.
4. Изменяющие методы и mutating
Структура должна явно сообщить, что метод меняет self или его свойства.
protocol Resettable { mutating func reset()}
struct Counter: Resettable { private(set) var value = 0
mutating func increment() { value += 1 } mutating func reset() { value = 0 }}Класс выполняет такое требование без слова mutating, поскольку имеет ссылочную
семантику.
5. Требования к инициализаторам
protocol Named { init(name: String)}
final class User: Named { let name: String
required init(name: String) { self.name = name }}В нефинальном классе требуется required: любой возможный подкласс должен сохранить
возможность создания через контракт протокола.
6. Протокол как тип
Несколько разных реализаций можно хранить за общей абстракцией.
struct ConsoleLogger: Logger { func log(_ message: String) { print(message) }}
final class MemoryLogger: Logger { private(set) var messages: [String] = [] func log(_ message: String) { messages.append(message) }}
protocol Logger { func log(_ message: String)}
let loggers: [any Logger] = [ConsoleLogger(), MemoryLogger()]В современном Swift any Logger подчёркивает existential-контейнер: внутри может
находиться значение любого типа, соответствующего протоколу.
7. Зависимость от протокола
final class RegistrationService { private let validator: any Validator private let logger: any Logger
init(validator: any Validator, logger: any Logger) { self.validator = validator self.logger = logger }
func register(email: String) -> Bool { guard validator.isValid(email) else { logger.log("Некорректный email") return false } logger.log("Пользователь зарегистрирован") return true }}Сервис не создаёт зависимости внутри себя. Реализации передаются снаружи, поэтому поведение можно заменять и проверять изолированно.
8. Композиция протоколов
Если параметру нужны несколько независимых возможностей, требования объединяются.
protocol Storable { func save()}
func persistAndPrint(_ value: any Storable & Printable) { value.save() print(value.render())}Композиция не создаёт новый именованный протокол. Она локально описывает нужный клиенту набор возможностей.
9. Наследование протоколов
protocol Describable { var description: String { get }}
protocol PersistableEntity: IdentifiableEntity, Describable { func save()}Наследование протоколов объединяет контракты без наследования состояния или готовой реализации класса.
10. Протоколы стандартной библиотеки
Equatable, Hashable, Comparable, Codable, Sequence и другие протоколы
встраивают пользовательские типы в стандартные алгоритмы Swift.
struct Product: Hashable { let id: Int let name: String}
let unique: Set<Product> = [ Product(id: 1, name: "Книга"), Product(id: 1, name: "Книга")]Синтезированная реализация удобна, но равенство должно соответствовать смыслу модели.
Иногда идентичность продукта определяется только id, и реализацию нужно написать
явно.
11. any и some
any Protocol скрывает конкретный тип и допускает разные реализации в одной
переменной. some Protocol скрывает тип от вызывающего кода, но функция каждый раз
возвращает один и тот же конкретный тип.
func makeValidator() -> some Validator { EmailValidator()}some сохраняет статическую специализацию. any удобен для хранения сменяемых
реализаций. Подробно ограничения рассматриваются вместе с generics.
12. Узкие протоколы
Большой протокол заставляет реализации поддерживать ненужные операции. Контракт полезно формировать вокруг потребностей клиента.
protocol UserReading { func user(id: Int) -> User?}
protocol UserWriting { func save(_ user: User)}Сервис чтения зависит только от UserReading, а не от полного репозитория.
Типичные ошибки
- протокол копирует все методы конкретного класса;
- контракт содержит несвязанные ответственности;
- клиент зависит от
any Protocol, хотя ему достаточно generic-параметра; get setтребуется без необходимости;- реализация создаётся внутри сервиса и не может быть заменена;
- протокол вводится без второго сценария или реальной границы зависимостей.
Вопросы для самопроверки
- Что описывает протокол?
- Чем требование свойства
{ get }отличается от{ get set }? - Зачем структуре
mutating? - Почему инициализатор протокола может потребовать
required? - Что означает запись
any Logger? - Как протокол уменьшает связанность сервиса?
- Что делает композиция протоколов?
- Чем наследование протоколов отличается от наследования классов?
- В чём базовое различие
anyиsome? - Почему узкий протокол обычно удобнее большого?
Итоги
- протокол задаёт контракт без привязки к хранению и реализации;
- соответствовать протоколу могут разные категории типов;
- протоколы позволяют подменять зависимости;
anyхранит значение неизвестного соответствующего типа;- композиция объединяет несколько контрактов;
- стандартные протоколы подключают модель к возможностям языка;
- контракт следует делать минимальным и ориентированным на клиента.