programowanieProgramista Swift middle

Co to jest kompozycja protokołów (protocol composition) w Swift, jak to działa i do czego jest potrzebna? Jakie są pułapki przy używaniu kilku protokołów jednocześnie?

Zdaj rozmowy kwalifikacyjne z asystentem AI Hintsage

Odpowiedź.

W Swift doświadczenie wielu języków OOP zostało uogólnione i udoskonalone poprzez możliwość łączenia (kompozycji), a nie tylko dziedziczenia protokołów. Kompozycja protokołów pozwala zadeklarować zmienną, parametr funkcji lub typ ogólny z wymogiem zgodności z od razu wieloma protokołami. Ten mechanizm jest niezwykle przydatny, gdy konieczne jest pracowanie z obiektami, które mają zachowanie wielu kontraktów (interfejsów), przy elastycznym unikaniu wad dziedziczenia wielokrotnego. Problem, który rozwiązuje kompozycja, to konieczność wyrażenia "obiekt musi spełniać grupę wymagań", a nie tylko jedno.

W rozwiązaniu Swift używany jest specjalny składnia: łączenie protokołów znakiem & (ampersand), na przykład protocolA & protocolB. Pod maską realizowana jest kontrola w czasie wykonywania (np. przy rzutowaniu typów i w kontekstach ogólnych). To minimalizuje liczbę typów i elastycznie wdraża wzorzec "podziału obowiązków".

Przykład kodu:

protocol Drawable { func draw() } protocol Movable { func move() } struct Sprite: Drawable, Movable { func draw() { print("Sprite rysuje") } func move() { print("Sprite porusza się") } } func animate(object: Drawable & Movable) { object.draw() object.move() } let s = Sprite() animate(object: s)

Kluczowe cechy:

  • Pozwala elastycznie wyrażać kompozycję zachowań bez hierarchii dziedziczenia
  • Gwarantuje spełnienie wszystkich kontraktów jednocześnie
  • Kompatybilny z parametrami ogólnymi i aliasami typów

Pytania z haczykiem.

Czy można utworzyć zmienną typu tylko protocolA & protocolB, nie wiążąc się z określoną strukturą lub klasą?

Tak, można zadeklarować zmienną jako zgodną od razu z wieloma protokołami, na przykład:

var obj: protocolA & protocolB

Ale ważne: takie zmienne mogą odnosić się tylko do obiektów (a nie typów wartości), jeśli w kompozycji przynajmniej jeden protokół jest ograniczony do typów klasowych (protocol: AnyObject).

Czy można włączyć do kompozycji typ klasowy, na przykład SomeClass & Drawable?

Tak, ale z niuansami: kompozycja typu SomeClass & Protocol wymaga, aby wartości były koniecznie instancjami tej klasy (lub jej dziedziców), implementującymi protokół. Takie podejście stosuje się do ograniczenia typów ogólnych.

Czy można używać kompozycji protokołów jako typu typów skojarzonych w rozszerzeniach protokołów?

Tak, ale są ograniczenia: nie można zadeklarować associatedtype jako kompozycję, ale można użyć where w rozszerzeniu do ograniczenia kompozycji protokołów, na przykład, rozszerzenie stosujące się tylko do typów, które są zgodne z wieloma protokołami.

Typowe błędy i antywzorce

  • Użycie kompozycji z ośmioma lub dziewięcioma protokołami: to oznaka przeciążenia architektury i złego podziału obowiązków
  • Rzutowanie typów wartości (struct) do zmiennej kompozycji protokołów z ograniczeniem AnyObject — zawsze spowoduje błąd
  • Używanie tej samej kompozycji w różnych częściach aplikacji bez typealias: utrudnia czytelność

Przykład z życia

Negatywny przypadek

W projekcie zrealizowano 5 podobnych protokołów — Drawable, Movable, Resizable, Colorable, Animatable. Wszędzie stosowano kompozycję Drawable & Movable & Resizable & Colorable & Animatable. Typowe błędy towarzyszyły skomplikowanym błędom z powodu tego, że część bytów nie realizowała jednego z kontraktów.

Zalety:

  • Nie wymaga głębokiego dziedziczenia
  • Łatwo dodać lub usunąć funkcjonalność

Wady:

  • Trudno śledzić niespójności
  • Trudne testowanie
  • Zła czytelność deklaracji

Pozytywny przypadek

Zamiast skomplikowanej kompozycji wydzielono dwa główne protokoły (na przykład, Actor i Viewable), zrobiono typealias dla kompozycji "DynamicEntity" i wszędzie używano go. Wyraźnie rozdzielono obszary odpowiedzialności.

Zalety:

  • Kod jest łatwiejszy do odczytania i utrzymania
  • Testy wyraźnie wydzielają zachowanie dla DynamicEntity
  • Szybka modyfikacja listy wymagań

Wady:

  • Wymaga przebudowy architektury
  • Czasami trzeba podzielić istniejące klasy, aby spełnić wymagania