Historia pytania:
Abstrakcja protokołów pojawiła się w Swift w kontraście do klasycznego dziedziczenia obiektów w OOP. Jeśli w Objective-C i innych językach OOP dominował podejście dziedziczenia "od ogółu do szczegółu", to Swift od samego początku promował protokoły jako główny sposób osiągania abstrakcji, podkreślając kompozycję ponad dziedziczeniem.
Problem:
Klasyczne dziedziczenie zakłada sztywną hierarchię: drzewo podklas z obowiązkowym rozszerzeniem za pomocą override. To ogranicza elastyczność, prowadzi do "kruchych" kodów, trudnego refaktoryzowania i rozrostu podstawowych klas przodków. Ponadto Swift nie wspiera wielokrotnego dziedziczenia klas — co oznacza, że ponowne wykorzystanie funkcjonalności jest możliwe tylko za pomocą innych mechanizmów.
Rozwiązanie:
Abstrakcja protokołów pozwala na deklarowanie "zestawu wymagań", które typ musi zrealizować. Protokoły można rozszerzać (extension) w celu wprowadzenia ogólnej logiki, co przybliża je do koncepcji "miksinów":
Przykład kodu:
protocol Drawable { func draw() } extension Drawable { func draw() { print("Default drawing") } } struct Circle: Drawable {} let c = Circle() c.draw() // Wyświetli "Default drawing"
Kluczowe cechy:
Jaka jest różnica między rozszerzeniem protokołu a zwykłą implementacją w klasie?
Rozszerzenie protokołu przez extension dodaje domyślną implementację tylko w przypadkach, gdy użytkownik nie zaimplementował tej metody w swoim typie. Jeśli metoda jest jawnie zaimplementowana w typie, wywołana zostanie właśnie ona.
Przykład:
protocol Demo { func foo() } extension Demo { func foo() { print("default") } } struct X: Demo { func foo() { print("custom") } } X().foo() // "custom"
Co się stanie, jeśli typ implementujący protokół i jego rozszerzenie zostanie wywołany jako dane protokołu?
Jeśli protokół deklaruje metodę jako obowiązkową (wymaganie), używana jest implementacja konkretnego typu nawet przy rzutowaniu na typ protokołu. Jednak jeśli w rozszerzeniu dodane zostanie nowe (wcześniej nie zadeklarowane w protokole) właściwość, będzie ona dostępna tylko przez rozszerzenie, a nie przez typ protokołu.
Czy można przechowywać w tablicy instancje różnych structów implementujących jeden protokół?
Tak — dzięki "typom egzystencjalnym" (np. [Drawable]) można przechowywać heterogeniczne kolekcje:
struct Tri: Drawable { func draw() { print("Triangle") } } let arr: [Drawable] = [Circle(), Tri()] arr.forEach { $0.draw() }
W firmie istniał podstawowy superklasa Shape, od którego dziedziczyły wszystkie figury (Circle, Square, Polygon). Podstawowa klasa rosła, ponieważ każdą nową figurę trzeba było wspierać przez override. Rozszerzanie systemu stawało się coraz trudniejsze — każdy nowy typ łamał ABI i zmuszał do przepisania istniejącego kodu.
Zalety:
Wady:
Zaczęto używać kilku protokołów: Drawable, Colorable, Animatable. Teraz każdą figurę łatwo jest uczynić jednocześnie "animowaną i kolorową", nie zmieniając innych struktur. Nowe funkcjonalności dodawane są przez rozszerzenia.
Zalety:
Wady: