Протокольно-ориентированное программирование в iOS
Коротко о протоколах в Swift.
Протоколы позволяют задать некоторый интерфейс, которому должен удовлетворять класс, поддерживающий этот протокол. Для примера:
protocol Pet { func sound() -> String }
Протоколы сами не могут содержать реализацию методов, поэтому мы создаем класс, который и будет реализовывать метод sound():
class Cat: Pet { func sound() -> String { return "Мяу" } } class Dog: Pet { func sound() -> String { return "Гав" } }
Далее можно создать экземпляры этих классов и вызвать метод sound():
let cat = Cat() let dog = Dog() cat.sound() // "Мяу" dog.sound() // "Гав"
При этом можно иметь экземпляр типа Pet и также иметь возможность вызвать этот метод:
let pet: Pet = cat pet.sound() // "Мяу"
Переменная pet будет содержать экземпляр типа Cat, но для нас это объект Pet, делающий то, что предписано протоколом. В этом и заключается их сила: они позволяют скрыть внутреннюю структуру объекта. Код заботиться только о том, чтобы объект выполнял то, что требует протокол.
Расширения протоколов.
Расширения в Swift появились давно (category в Objective-C), они задаются с использованием ключевого слова extension. Но их можно было использовать только с классами. Начиная со Swift 2.0, стало возможным расширять протоколы:
protocol Pet { func sound() -> String } extension Pet { func sound() -> String { return "Звук" } }
Теперь метод sound() получил реализацию по умолчанию:
class Cat: Pet { } let cat = Cat() cat.sound() // "Звук"
Pet в данном случае называют mixin, или trait, потому что это кусок функционала, который добавляется, или смешивается (mix), с другим классом. Этот метод все еще можно переопределить:
class Cat: Pet { func sound() -> String { return "Мяу" } } let cat = Cat() cat.sound() // "Мяу"
Все это, на первый взгляд, не сильно отличается от наследования, когда у нас есть реализации методов по умолчанию в базовом классе и мы их наследуем в производном:
class Cat: BaseClass { ... }
Но большая разница в том, что Cat - не наследник Pet. Допустим, Cat наследуется от базового класса BaseClass:
class Cat: BaseClass, Pet { ... }
Теперь Cat содержит свойства и методы из BaseClass плюс методы из протокола Pet. Но мы можем наследоваться только от одного класса (в отличие от С++), и при этом поддерживать множество протоколов:
class Cat: BaseClass, Pet, Animal, Cute { ... }
Наследование позволяет писать переиспользуемый код, но расширения протоколов делают это без наследования. Ниже - пример того, как могут быть полезны расширения протоколов.
Пусть имеется следующая иерархия классов:
class Object class Machine: Object class Tower: Object class Catapult: Machine class HellTower: Tower
Допустим, HellTower может стрелять огненными шарами, а Catapult - камнями. Но что, если нужно добавить возможность Catapult тоже стрелять огненными шарами? Самый простой и плохой вариант - скопипастить код из класса HellTower. Другой вариант - поместить код, отвечающий за возможность атаковать огненными шарами, в базовый класс Object. Но это в конце концов может привести к тому, что базовый класс будет отвечать за огромную часть функционала и его будет тяжело поддерживать. На мой взгляд, лучшим решением в данном случае будет использование расширений протоколов. Можно выделить возможность стрелять огненными шарами в отдельный протокол, затем предоставить реализацию по умолчанию:
protocol FireballsAttackTrait { ... }
Тогда Catapult и HellTower будут владеть таким поведением, или чертой (trait):
class Catapult: Machine, FireballsAttackTrait class HellTower: Tower, FireballsAttackTrait
Таким образом, проблема легко решилась без наследования. Можно создать множество разных поведений: BulletAttackTrait, ArrowAttackTrait и т. д., и присваивать их игровым объектам по мере необходимости.
Этот пример показывает, как протокольно-ориентированное программирование может упростить наследование. Теперь иерархия классов выглядит более легкой и гибкой.
Хороший пример фрэймворка, использующего миксины:
https://github.com/AliSoftware/Reusable
--
Максим Алиев
iOS-разработчик MobileDev