Главное Авторские колонки Вакансии Образование
arrow-right Created with Sketch. Максим Алиев 1 137 0 В избр. Сохранено
Авторизуйтесь
Вход с паролем

Протокольно-ориентированное программирование в iOS

В этой статье я бы хотел поговорить о протокольно-ориентированном программировании в iOS, которое стало возможным с выходом Swift 2.0.
Мнение автора может не совпадать с мнением редакции

Коротко о протоколах в 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

0
В избр. Сохранено
Авторизуйтесь
Вход с паролем