Статьи

Протоколно-ориентированное программирование в Swift 2

С выпуском Swift 2 Apple добавила ряд новых функций и возможностей в язык программирования Swift. Однако одним из самых важных был пересмотр протоколов. Улучшенная функциональность, доступная в протоколах Swift, позволяет создавать новый тип программирования — протоколно-ориентированное программирование. Это в отличие от более распространенного объектно-ориентированного стиля программирования, к которому многие из нас привыкли.

В этом уроке я собираюсь показать вам основы протоколно-ориентированного программирования в Swift и его отличие от объектно-ориентированного программирования.

Это руководство требует, чтобы вы работали с Xcode 7 или выше, что включает поддержку версии 2 языка программирования Swift.

Если вы еще не знакомы с протоколами, они являются способом расширения функциональности существующего класса или структуры. Протокол может рассматриваться как проект или интерфейс, который определяет набор свойств и методов. Класс или структура, соответствующие протоколу, необходимы для заполнения этих свойств и методов значениями и реализациями соответственно.

Следует также отметить, что любое из этих свойств и методов может быть обозначено как необязательное, что означает, что для их реализации не требуются соответствующие типы. Определение протокола и соответствие класса в Swift может выглядеть так:

01
02
03
04
05
06
07
08
09
10
11
protocol Welcome {
    var welcomeMessage: String { get set }
    optional func welcome()
}
 
class Welcomer: Welcome {
    var welcomeMessage = «Hello World!»
    func welcome() {
        print(welcomeMessage)
    }
}

Для начала откройте Xcode и создайте новую игровую площадку для iOS или OS X. Как только Xcode создаст игровую площадку, замените ее содержимое следующим:

01
02
03
04
05
06
07
08
09
10
11
protocol Drivable {
    var topSpeed: Int { get }
}
 
protocol Reversible {
    var reverseSpeed: Int { get }
}
 
protocol Transport {
    var seatCount: Int { get }
}

Мы определяем три протокола, каждый из которых содержит свойство. Далее мы создаем структуру, которая соответствует этим трем протоколам. Добавьте следующий код на игровую площадку:

1
2
3
4
5
struct Car: Drivable, Reversible, Transport {
    var topSpeed = 150
    var reverseSpeed = 20
    var seatCount = 5
}

Возможно, вы заметили, что вместо создания класса, соответствующего этим протоколам, мы создали структуру. Мы делаем это, чтобы избежать одной из типичных проблем, присущих объектно-ориентированному программированию, ссылкам на объекты .

Представьте, например, что у вас есть два объекта, A и B. A создает некоторые данные самостоятельно и сохраняет ссылку на эти данные. А затем делится этими данными с B по ссылке, что означает, что оба объекта имеют ссылку на один и тот же объект. Без знания A B каким-то образом изменяет данные.

Хотя это может показаться не большой проблемой, может случиться так, что А не ожидал, что данные будут изменены. Объект А может найти данные, с которыми он не знает, как обращаться или обрабатывать. Это общий риск ссылок на объекты.

В Swift структуры передаются по значению, а не по ссылке . Это означает, что в вышеприведенном примере, если данные, созданные A, были упакованы в виде структуры вместо объекта и переданы B, данные будут скопированы вместо общего доступа по ссылке. Это приведет к тому, что и А, и В будут иметь собственную уникальную копию одного и того же фрагмента данных. Внесенное B изменение не повлияет на копию, управляемую A.

Drivable компонентов Drivable , Reversible и Transport на отдельные протоколы также обеспечивает более высокий уровень настройки, чем традиционное наследование классов. Если вы читали мой первый учебник о новой платформе GameplayKit в iOS 9, то эта ориентированная на протокол модель очень похожа на структуру сущностей и компонентов, используемую в платформе GameplayKit.

Принимая этот подход, пользовательские типы данных могут наследовать функциональность от нескольких источников, а не от одного суперкласса. Учитывая то, что мы получили, мы могли бы создать следующие классы:

  • класс с компонентами Drivable и Reversible протоколов
  • класс с компонентами Drivable и Transportable протоколов
  • класс с компонентами Reversible и Transportable протоколов

При объектно-ориентированном программировании наиболее логичным способом создания этих трех классов будет наследование от одного суперкласса, который содержит компоненты всех трех протоколов. Этот подход, однако, приводит к тому, что суперкласс является более сложным, чем необходимо, и каждый из подклассов наследует больше функциональных возможностей, чем необходимо.

Все, что я показал вам до сих пор, стало возможным в Swift с момента его выпуска в 2014 году. Те же концепции, ориентированные на протоколы, могли даже применяться к протоколам Objective-C. Однако из-за ограничений, которые существовали в протоколах, настоящее программирование, ориентированное на протокол, было невозможно до тех пор, пока в язык Swift не было добавлено несколько ключевых функций. Одна из наиболее важных из этих функций — это расширения протокола. в том числе условные расширения .

Во-первых, давайте расширим протокол Drivable и добавим функцию, чтобы определить, является ли конкретный Drivable быстрее другого. Добавьте на свою игровую площадку следующее:

01
02
03
04
05
06
07
08
09
10
extension Drivable {
    func isFasterThan(item: Drivable) -> Bool {
        return self.topSpeed > item.topSpeed
    }
}
 
let sedan = Car()
let sportsCar = Car(topSpeed: 250, reverseSpeed: 25, seatCount: 2)
 
sedan.isFasterThan(sportsCar)

Вы можете видеть, что когда код игровой площадки выполняется, он выводит значение false   в качестве вашего sedan автомобиль имеет topSpeed 150 по умолчанию, что меньше, чем у sportsCar .

Расширение выхода

Возможно, вы заметили, что мы предоставили определение функции, а не объявление функции. Это кажется странным, потому что протоколы должны содержать только объявления. Правильно? Это еще одна очень важная особенность расширений протокола в Swift 2 — поведение по умолчанию . Расширяя протокол, вы можете обеспечить реализацию по умолчанию для функций и вычисляемых свойств, чтобы классы, соответствующие протоколу, не обязаны это делать.

Далее мы собираемся определить другое Drivable протокола Drivable , но на этот раз мы определим его только для типов значений, которые также соответствуют протоколу Reversible . Это расширение затем будет содержать функцию, которая определяет, какой объект имеет лучший диапазон скоростей. Мы можем достичь этого с помощью следующего кода:

1
2
3
4
5
6
7
extension Drivable where Self: Reversible {
    func hasLargerRangeThan(item: Self) -> Bool {
        return (self.topSpeed + self.reverseSpeed) > (item.topSpeed + item.reverseSpeed)
    }
}
 
sportsCar.hasLargerRangeThan(sedan)

Ключевое слово Self , написанное с большой буквы «S», используется для представления класса или структуры, соответствующей протоколу. В приведенном выше примере ключевое слово Self представляет структуру Car .

После запуска кода детской площадки, Xcode выведет результаты на боковой панели справа, как показано ниже. Обратите внимание, что у sportsCar большая дальность, чем у sedan .

Условное расширение вывода

Хотя определение и расширение ваших собственных протоколов может быть очень полезным, истинная сила расширений протоколов проявляется при работе со стандартной библиотекой Swift. Это позволяет добавлять свойства или функции к существующим протоколам, таким как CollectionType (используется для таких вещей, как массивы и словари) и Equatable (возможность определять, равны ли два объекта или нет). С помощью условных расширений протокола вы также можете предоставить очень специфические функциональные возможности для конкретного типа объекта, соответствующего протоколу.

На нашей площадке мы собираемся расширить протокол CollectionType и создать два метода: один для получения средней максимальной скорости автомобилей в массиве Car а другой — для средней скорости движения задним ходом. Добавьте следующий код на вашу игровую площадку:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
extension CollectionType where Self.Generator.Element: Drivable {
    func averageTopSpeed() -> Int {
        var total = 0, count = 0
        for item in self {
            total += item.topSpeed
            count++
        }
        return (total/count)
    }
}
 
func averageReverseSpeed<T: CollectionType where T.Generator.Element: Reversible>(items: T) -> Int {
    var total = 0, count = 0
    for item in items {
        total += item.reverseSpeed
        count++
    }
    return (total/count)
}
 
let cars = [Car(), sedan, sportsCar]
cars.averageTopSpeed()
averageReverseSpeed(cars)

Расширение протокола, которое определяет метод averageTopSpeed использует преимущества условных расширений в Swift 2. В отличие от averageReverseSpeed функция averageReverseSpeed мы определяем непосредственно под ней, является еще одним способом достижения аналогичного результата с использованием averageReverseSpeed Swift. Лично я предпочитаю более чистое расширение протокола CollectionType , но это зависит от личных предпочтений.

В обеих функциях мы перебираем массив, суммируем общую сумму и затем возвращаем среднее значение. Обратите внимание, что мы вручную сохраняем количество элементов в массиве, потому что при работе с CollectionType а не с обычными элементами типа Array , свойство count является Self.Index.Distance типа Self.Index.Distance а не Int .

Как только ваша игровая площадка выполнит весь этот код, вы увидите среднюю максимальную скорость вывода 183 и среднюю скорость реверса 21 .

Стандартные расширения библиотеки

Несмотря на то, что протоколно-ориентированное программирование является очень эффективным и масштабируемым способом управления вашим кодом в Swift, все еще есть совершенно веские причины для использования классов при разработке в Swift:

Большинство SDK для iOS, watchOS и tvOS написаны на Objective-C с использованием объектно-ориентированного подхода. Если вам нужно взаимодействовать с любым из API, включенных в эти SDK, вы вынуждены использовать классы, определенные в этих SDK.

Компилятор Swift оптимизирует время жизни объектов в зависимости от того, когда и где они используются. Стабильность объектов на основе классов означает, что ваши ссылки на другие файлы и элементы будут оставаться согласованными.

Временами ссылки на объекты — это именно то, что вам нужно, например, если вы вводите информацию в конкретный объект, такой как графический рендер. Использование классов с неявным совместным использованием важно в подобных ситуациях, потому что вы должны быть уверены, что средство рендеринга, которому вы отправляете данные, все равно будет таким же, как и раньше.

Надеемся, что к концу этого урока вы увидите потенциал программирования, ориентированного на протоколы, в Swift и как его можно использовать для оптимизации и расширения вашего кода. Хотя эта новая методология кодирования не полностью заменит объектно-ориентированное программирование, она приносит ряд очень полезных, новых возможностей.

От поведения по умолчанию до расширений протокола, протоколно-ориентированное программирование в Swift будет принято многими будущими API и полностью изменит наш взгляд на разработку программного обеспечения.

Как всегда, обязательно оставляйте свои комментарии и отзывы в комментариях ниже.