Na World Wide Developers Conference 2015 Apple ogłosiło, że język Swift, wtedy zbliżający się do wersji 2.0, będzie pierwszym językiem programowania umożliwiającym oparcie architektury aplikacji w całości o protokoły. Oczywiście te dostępne są od dawna w innych językach (mogą kryć się pod inną nazwą), jednak Apple wzbogaciło je o rozszerzenia (protocol extensions), które otwierają przed programistami zupełnie nowe możliwości.

 

Protocol-oriented programming zachęca nas do tworzenia architektury w oparciu o protokoły (abstrakcje), zamiast polegać na konkretnych implementacjach.

 

Moim skromnym zdaniem jedną z największych zalet korzystania z protocol-oriented programming (jak to sensownie przetłumaczyć – programowanie na protokołach???) jest automatyczne spełnianie ostatniej z zasad SOLID, czyli zasady odwrócenia odpowiedzialności. Korzystając z protokołów nie musimy skupiać się konkretnych implementacjach, ponieważ wystarczy nam gwarancja, że będziemy mieli dostęp do danej funkcji lub właściwości obiektu. Dodatkowo nie będziemy się ograniczać tylko do samych klas (typów referencyjnych), bo w Swift również struktury (typy wartościowe) mogą wykorzystywać protokoły. Najlepszym argumentem przemawiającym za strukturami jest fakt, że zawsze będziemy otrzymywali unikalną kopię obiektu. W wielowątkowym środowisku, w którym pracują dzisiejsze aplikacje jest to szczególnie ważne. Struktury dają nam gwarancję, że poszczególne wątki nie będą wykonywały jednocześnie operacji na tym samy obiekcie.

Opierając nasz kod na abstrakcjach tworzymy luźno powiązane moduły, które będzie nam znacznie łatwiej testować, między innymi dzięki zastosowaniu wzorca Dependency Injection. Polega on na „wstrzykiwaniu” zależności do obiektu (określanego jako System Under Test), na przykład podczas pisania testów jednostkowych. Najlepiej jednak będzie jak zobaczycie konkretny przykład.

Załóżmy, że mamy typową sytuację, w której chcemy przy starcie naszego widoku głównego (UIViewController) pobrać jakieś dane z serwera. Dla uproszczenia przykładu nie będę stosował żadnej architektury, jak MVC lub MVVM. Tak będzie wyglądał kod oparty o konkretną implementację zamiast abstrakcji:

 

 

Problem polega na tym, że pisząc testy dla obiektu MainVC jesteśmy zmuszeni korzystać z „konkretnej” implementacji obiektu DataService, co znacznie utrudni nam zadanie. Podczas testowania chcemy tylko sprawdzić zachowanie obiektu MainVC, więc dużo łatwiej byłoby „symulować” działanie obiektu DataService. Zmieńmy trochę nasz kod, tak aby korzystał z abstrakcji oraz wstrzykiwania zależności:

 

 

Teraz podczas pisania testów dla obiektu MainVC możemy przypisać do pola dataService dowolny obiekt spełniający wymagania protokołu DataServiceProtocol. Tak będzie wyglądał przykładowy test (korzystam z bibliotek Quick + Nimble):

 

 

Powyższy przykład jest oczywiście banalny, ale służy on tylko zaprezentowaniu koncepcji.

Jak widzicie w miejsce konkretnej implementacji możemy sobie wstawić tzw. „Mocka”, czyli obiekt jedynie symulujący zachowanie obiektu DataService. Funkcja getDataFromWeb(withUrl:) będzie zwracała na sztywno zdefiniowane wartości. Dzięki takiemu rozwiązaniu nasze testy mogą skupić się całkowicie na obiekcie MainVC. Oczywiście w osobnym pakiecie testów wskazane jest przetestowanie właściwej implementacji DataService 🤓.

 

 

Ale to już było…

 

 

Oczywiście zaprezentowana powyżej koncepcja nie jest niczym nowym i z powodzeniem można ją zastosować również w takich językach jak Java, czy Objective-C. Swift idzie jednak o krok dalej i umożliwia wykorzystanie protokołów jako podstawy dla architektury całej aplikacji.

W programowaniu obiektowym projektowanie obiektów zaczynamy od utworzenia klasy bazowej (super klasy), po której będą dziedziczyły obiekty pochodne. Obiekty oraz ich wzajemne relacje są podstawą architektury. W programowaniu opartym na protokołach pracę rozpoczynamy od utworzenia… protokołu 🤣. Rozwiązanie to opiera się na trzech filarach – rozszerzeniach protokołów, dziedziczeniu protokołów oraz kompozycji protokołów.

Rozszerzenia protokołów – pozwalają nam na określenie sposobu zachowania dla poszczególnych pól oraz metod. Dzięki temu możemy zdefiniować pewne wspólne zachowania dla obiektów wykorzystujących dany protokół. Dzięki ich wykorzystaniu mamy możliwość tworzeniu bardziej „ekspresyjnego” kodu, który już samą konstrukcją będzie mówił nam jakie jest jego przeznaczenie. W takiej na przykład Javie nie mamy takich możliwości. W przykładzie wykorzystamy profesję, którą pewnie trochę kojarzycie 👍:

 

 

Każdy szanujący się programista musi pić regularnie kawę (taki banał powtarzany w niektórych anegdotach 😜), dlatego został wyposażony w odpowiednią funkcję oraz pole, w którym będzie mógł notować ilość wypitych kaw. W rozszerzeniu protokołu dodaliśmy domyślną implementację metodydrinkCoffe(numberOfCups:), która będzie dziedziczona przez wszystkie obiekty wykorzystujące protokół Programmer. Jeżeli dany obiekt będzie chciał jednak nadpisać domyślną implementację to nie będzie z tym żadnego problemu. Tak wygląd przykładowe wykorzystanie rozszerzenia:

 

 

Zwróćcie uwagę na fakt, że klasa JuniorProgrammer nie definiuje nigdzie metody drinkCoffe(numberOfCups:), mimo że ta jest wymagana przez protokół. Dzieje się tak właśnie dlatego, że w rozszerzeniu umieściliśmy jej domyślną implementację. Bardzo przydatna rzecz 😉.

 

Dziedziczeniu protokołów – koncepcja podobna do dziedziczenia w podejściu obiektowym. Główna różnica polega na tym, że dany protokół nie będzie dziedziczył funkcjonalności obiektu bazowego a jedynie jego wymagania. Za pomocą rozszerzeń możemy jednak określać domyślne implementacje, tak ja to zrobiliśmy powyżej. Do naszego przykładu dodamy nowego programistę:

 

 

Programista backend’u przejmuje naturalnie wszystkie cechy podstawowego „modelu” (w tym domyślną implementację metody drinkCoffe(numberOfCups:)), jednocześnie dodając funkcję specyficzną dla jego specjalizacji, czyli programowanie w języku JavaScript.

 

Kompozycja protokołów – w przypadku Swift’a obiekt może dziedziczyć po więcej niż jednym protokole, co jest niemożliwe w przypadku klas. Dzięki temu możemy tworzyć małe, specjalistyczne protokoły, zamiast dużych i ogólnych. Pomaga to w zachowaniu zasady Interface Segregation z zestawu SOLID. Musimy jednak uważać, aby nie tworzyć zbyt małych protokołów, które utrudnią nam utrzymanie kodu. Kontynuując nasz przykład z programistą. Załóżmy, że mamy osobę, która potrafi pisać kod backend’u, jak również aplikacje na Androida. Z łatwością możemy rozbudować nasze początkowe założenia:

 

 

W ten prosty sposób możemy tworzyć bardzo złożone kontrakty dla naszych obiektów. Klasa SuperProgrammer z automatu przejmuje implementacje metod useJavaScript() oraz useKotlin(). Powyższy przykład pokazuje, z jaką łatwością za pomocą protokołów możemy dodawać nowe funkcjonalności do naszych obiektów.

 

 

Type constraints

 

 

Apple dało nam również możliwość nakładania pewnych ograniczeń na protokoły oraz ich rozszerzenia. Możemy na przykład ograniczyć wykorzystanie naszego protokołu w ramach obiektu UIViewController lub uogólniając, w ramach samych klas (w takim przypadku struktura nie będzie mogła z niego skorzystać):

 

 

Możemy także zastosować taką konstrukcję:

 

 

Rozszerzenie będzie dostępne tylko dla obiektów, które w pierwszej kolejności będą spełniały wymogi protokołu AndroidProgrammer, ale jednocześnie będą korzystały z protokołu BackendProgrammer. Nakładając konkretne ograniczenia, uzyskaliśmy automatycznie dostęp do wszystkich właściwości protokołu BackendProgrammer. Oznacza to, że w powyższym rozszerzeniu moglibyśmy z użyć funkcji useJavaScript() jeżeli zaszłaby taka potrzeba.

 

 

Sposoby wywołania rozszerzeń protokołów

 

 

Na koniec coś dla miłośników detali i fanów maksymalnej optymalizacji 🤩. Podczas korzystania z pól oraz metod zdefiniowanych w protokole możemy mieć do czynienia z jednym z dwóch sposobów wywołania – statycznym (static dispatch) lub dynamicznym (dynamic dispatch). Jest to związane z faktem, że niektóre pola oraz metody mogą być nadpisywane przez obiekty pochodne:

Dynamic Dispatch – kompilator nie ma pojęcia, która właściwość (pole) lub metoda zostanie wykorzystana po uruchomieniu programu i musi wykonać dodatkowe operacje, aby to sprawdzić. Dzieje się tak dlatego, że Swift daje nam możliwość nadpisywania pól oraz metod w obiektach pochodnych. Oznacza to, że kompilator musi sprawdzić, czy chcemy skorzystać z implementacji obiektu bazowego, czy może raczej obiektu pochodnego.

Static Dispatch – sytuacja odwrotna. Kompilator jest pewien, która właściwość (pole) lub metoda zostanie wykorzystana podczas działania programu. Można w ten sposób zaoszczędzić odrobinę zasobów, podkręcając tym samym wydajność. Użycie operatora final w definicji klasy, da kompilatorowi do zrozumienia, że nie zostaną na jej podstawie utworzone żadne obiekty pochodne. Oznacza to, że kompilator nie musi zastanawiać się, którą z implementacji wybrać. To tylko jedna z możliwości. Podobnie zachowuje się operator static.

Wróćmy jednak do samych protokołów. Jeżeli dany obiekt tworzy własną implementację pola lub metody, które zostały zdefiniowane jedynie w rozszerzeniu danego protokołu, to mamy do czynienia ze static dispatch. Oznacza to, że wybór konkretnej implementacji będzie zależał od typu wykorzystanej zmiennej. Trochę zakręcone, ale będzie to lepiej widać na przykładzie:

 

 

Jak sami widzicie, pomimo faktu, że korzystaliśmy z tej samej instancji dwa razy, to wynik operacji różnił się w zależności od tego jaki typ został przypisany do zmiennej. Taki mały haczyk, na który warto zwrócić uwagę. A przy okazji można lepiej zrozumieć jaka jest różnica pomiędzy static a dynamic dispatch 😎.

 

 

Słowo na drogę

 

Myślę, że za pomocą tego artykułu udało mi się przybliżyć Wam trochę inne oblicze języka Swift, który stara się być czymś więcej niż tylko kolejnym językiem obiektowym. Protokoły mogą okazać się w codziennej pracy bardzo przydatnym narzędziem. Opierając na nich architekturę aplikacji (przynajmniej w jakimś stopniu), tworzymy luźniej powiązane komponenty, które w większym stopniu polegać będą na abstrakcjach. Protokoły pozwalają nam również spojrzeć na nasz kod pod trochę inny kątem, poszerzając naszą wiedzę o nowy paradygmat 🤓. Do usłyszenia 🧐.

 

 

Link do zdjęcia tytułowego.


 

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *