Serwisy są nieodłączną częścią aplikacji Angularowych, ale czy powinny nią być? Dlaczego nazywamy obiekt serwisem i jakie niesie to konsekwencje? W niniejszym artykule postaram się zgłębić temat nazywania obiektów, oraz zadać pytanie – czym grozi brak precyzji w określaniu ich odpowiedzialności, oraz jak mu zapobiec.
Chwilę po tym, gdy wpadł mi do głowy pomysł na ten artykuł, pojawiło się RFC dotyczące poprawy styleguide’ów Angulara. Wśród różnych wskazań znalazło się również to dotyczące zaprzestania używania sufiksu „service” w nazwach klas i plików. Jak tłumaczy autor: „The term 'service’ (…) does not add any meaningful information to explain what a class does.”
Po gorącej dyskusji zdecydowano, aby pozostawić to zagadnienie deweloperom, i nie uwzględniać zasad dotyczących sufiksów w styleguide’ach, jednakże oficjalna dokumentacja i przykłady mają ich nie używać. Link
Zacznijmy jednak nasze rozważania od obecnej konwencji nazywania obiektów w Angularze.
Jakie rodzaje klas i obiektów występują w typowej aplikacji Angularowej?
Projektując aplikację w Angularze, opieramy się na kilku, kilkunastu podstawowych typach obiektów. Między innymi są to: dyrektywy (w tym komponenty), pipe’y, guard’y, resolver’y czy interceptor’y. Te obiekty mają mniej lub bardziej precyzyjnie zdefiniowaną rolę. Upraszczając – pipe’y transformują dane w ramach szablonu, guard’y sprawiają, że niektóre ścieżki routingu są dostępne tylko pod określonymi warunkami, resolver’y dostarczają dane do widoku, zanim zostanie on wyświetlony, a interceptor’y modyfikują wychodzące i przychodzące requesty.
Komponenty zaś są bazowym elementem każdej aplikacji, definiując widok i jego logikę. Ponieważ definicja komponentu jest dość pojemna, dla lepszego rozumienia, za co dany komponent odpowiada, często określamy, czy jest to smart, czy dumb komponent.
A jak to jest z serwisami?
Określenie, czym jest serwis, nie jest tak proste. Kojarzą nam się one głównie z komunikacją z API, implementacją logiki biznesowej czy zarządzaniem stanem, a więc generalnie – delegowaniem zadań z komponentów i udostępnianiem danych oraz logiki między nimi.
W praktyce, serwisem określamy niemal każdy obiekt i klasę, które są owrapowane dekoratorem Injectable oraz mają w nazwie sufiks „Service”. Dekorator Injectable oznacza klasę jako dostępną do provide’owania i wstrzykiwania jako zależność. A więc nazywając obiekt serwisem stwierdzamy jedynie, że jest to jakiś obiekt – zależność.
Z pewnością spotkałeś się jednak z klasami dostępnymi do provide’owania, których nie nazywamy serwisami – przykładowo wspomniane już interceptory, guardy czy resolvery. A więc serwisem nazywamy takie obiekty – zależności, którym zwyczajnie nie nadaliśmy specyficznej nazwy.
Taki stan rzeczy może skłaniać nas do refleksji, że pojęcie serwisu może być wytrychem, którym programiści Angulara omijają potrzebę jasnego zdefiniowania odpowiedzialności obiektu oraz jego nazwy.
Side note – o podejściu funkcyjnym
Warto wspomnieć, że nowsze wersje Angulara wprowadzają funkcyjne podejście, np. do definiowania guardów i resolverów. Ja natomiast w ich kontekście wciąż mówię o klasach i obiektach. Jednak podejście funkcyjne zupełnie nie wpływa na rozważania o odpowiedzialności obiektów, dlatego będę trzymał się obiektowej nomenklatury.
Delegowanie odpowiedzialności
Jak już wspomniałem, do serwisów delegujemy odpowiedzialność z komponentów. A delegowanie jest kluczowe dla poprawy jakości kodu. Istotne jest jednak, komu tę odpowiedzialność powierzamy. Bez jasno zdefiniowanego przeznaczenia obiektu, do którego delegujemy, kod może szybko wymknąć się spod kontroli. Dlatego warto delegować do klas i obiektów o precyzyjnie określonej roli. A wybór nazwy pomaga stwierdzić, jakie zadanie obiekt pełni i w jaki sposób inne obiekty powinny się z nim komunikować.
Zastanówmy się teraz, jak zasady programowania obiektowego mogą pomóc nam wybrać lepszą nazwę.
Skąd wziąć właściwą nazwę? Sięgnijmy do OOP!
Programowanie obiektowe nie ogranicza się do zdefiniowania, czym są klasy i obiekty, czym jest polimorfizm i enkapsulacja – formułuje również dobre praktyki, jak się z nimi obchodzić (np. SOLID) oraz wzorce projektowe, które stanowią przepis na rozwiązanie często spotykanych problemów.
I to właśnie wzorce projektowe pomogą nam w określeniu nazwy i odpowiedzialności obiektu więc przyjrzyjmy się im bliżej.
Czym są wzorce projektowe?
Prosto mówiąc, sposobami na rozwiązanie często spotykanych problemów w systemach informatycznych. Mówiąc o problemach, nie mam na myśli błędów, lecz zagadnień, które oprogramowanie rozwiązuje. Takim problemem/zagadnieniem w dziedzinie frontend developmentu jest abstrakcja powtarzalnych operacji na żądaniach, takich jak np. dodawanie nagłówków, a sposobem rozwiązania jest zastosowanie obiektu o szczególnym zastosowaniu i implementacji – interceptora. Każdy z wymienionych na początku artykułu rodzajów obiektów również można by opisać w postaci wzorca projektowego.
Aby bardziej świadomie używać pojęcia wzorców projektowych, przytoczę krótko ich historię oraz zasady ich definiowania.
Historia wzorców projektowych
Koncept wzorców projektowych w informatyce został zaproponowany przez tzw. Gang Czworga w ich kultowej książce Design Patterns: Elements of Reusable Object-Oriented Software. Te wzorce są uniwersalnymi rozwiązaniami często spotykanych problemów projektowych i od dekad inspirują programistów do budowania elastycznego, łatwego w utrzymaniu kodu.
Definicja wzorca
Aby jednoznacznie opisać wzorzec, powinniśmy uwzględnić:
- Nazwę
- Opis problemu, który rozwiązuje
- Rozwiązanie (jakie klasy, obiekty, interfejsy i zależności między nimi w nim występują, zwykle nazywając precyzyjnie te składniki)
- Konsekwencje, które wiążą się z jego zastosowaniem
Wróćmy jednak do Angulara i zastanówmy się jak możemy wykorzystać je w praktyce.
Wzorce specyficzne dla domeny aplikacji
Oprócz powszechnie znanych wzorców obiektowych, wzorce są również definiowane przez frameworki czy biblioteki. Angular wprowadza i dostarcza programiście koncepcje, takie jak komponenty, pipe’y czy dyrektywy, które określają sposób budowania w nim aplikacji. Jeśli będziemy budować aplikację w sposób przemyślany, właściwie stosując wzorce angularowe, oraz wykorzystując powszechnie znane wzorce projektowe, mamy już połowę sukcesu.
Druga połowa pochodzi z rozpoznania powtarzalnych problemów specyficznych dla naszej aplikacji oraz zdefiniowania wzorców, którymi będziemy je rozwiązywać. I choć Angular jest dość zopiniowanym frameworkiem, projektując aplikację wciąż napotkamy wiele powtarzalnych problemów, które nie są rozwiązywane out-of-the-box i które warto rozwiązać w sposób usystematyzowany. Dlatego jako zaangażowani deweloperzy powinniśmy dążyć do definiowania wzorców specyficznych dla aplikacji.
Przyjrzyjmy się kilku powszechnie spotykanym zagadnieniom, które zasługują na usystematyzowane podejście.
Praktyczne zagadnienia wymagające uwagi
Komunikacja z API
Jest to typowe zadanie, które delegujemy do serwisów, aby abstrahować od szczegółowej implementacji zapytania oraz umożliwić współdzielenie tej implementacji. Jeśli ograniczymy odpowiedzialność takiego obiektu tylko do tego, wyłączając z niego przechowywanie danych, obsługę błędów czy inne efekty uboczne, mamy przepis na obiekt o jasnej odpowiedzialności, który ja lubię nazywać API Fasadą. Np. klasę stanowiącą fasadę interakcji z API produktów moglibyśmy nazwać ProductsApiFacade.
Zarządzanie stanem
Jestem fanem używania bibliotek do zarządzania stanem, a ostatnio szczególnie ngrx signal store’a. Jeśli zerkniemy na jego dokumentację, plik, w którym go definiujemy, nie nazywa się „service”, ale „store”. Podobnie, gdy korzystamy z „tradycyjnego” store’a opartego o Redux – każdy z typów obiektów ma tam swoją nazwę – reducer, selector, action czy effect – i tymi nazwami są opisane również poszczególne pliki. Dlaczego zatem, gdy implementujemy customowy state management, przysłowiowy serwis z subject’em, to czasem nadal nazywamy go serwisem, a nie store’m?
A więc niezależnie od sposobu implementacji store’a, w klasie (i pliku który ją zawiera) powinniśmy uwzględnić człon “store”, np. ProductsStore. A później starannie przestrzegać tego, by odpowiedzialność obiektu Store’a ograniczała się do bycia store’m.
Wynoszenie logiki z komponentu – „prezenter” jako antyprzykład
Przerośnięte komponenty są trudne w utrzymaniu. Stąd możemy wpaść na pomysł wyniesienia części jego logiki do pomocniczego obiektu. Niektórzy deweloperzy zaczęli zatem stosować wzorzec komponentu z prezenterem. Jednym z popularniejszych i najlepiej udokumentowanych źródeł na ten temat jest seria artykułów Larsa Nielsena o architekturze MVP w angularze link.
Osobiście mam dwa duże problemy z tym podejściem.
Po pierwsze – Angular opiera się na architekturze komponentowej, nie na MVP. We wspomnianym artykule prezenterem nazwany został obiekt do którego delegujemy złożoną logikę prezentacyjną komponentu. Pomimo próby imitacji MVP, definicja ta nie jest spójna z oryginalną. A używanie powszechnie znanej nazwy w nowym kontekście, może być mylące.
Po drugie – nazwa prezentera nie określa dokładnie za co dany obiekt odpowiada – jest zupełnie jak serwis, z tą różnicą, że ściśle powiązany z komponentem prezentacyjnym.
Zamiast delegowania całej “złożonej logiki” do „prezentera”, powinniśmy delegować konkretne zadania (np. obsługę formularzy) do obiektów zorientowanych na konkretne zadanie.
Jak wdrażać standardy?
Gdy już rozpoznamy problemy i zaproponujemy sposób na ich ustrukturyzowanie, trzeba sprawić, że cały zespół zacznie wdrażać je w życie.
Po pierwsze, możemy prowadzić dokumentację projektowych wzorców. Dokumentacja to podstawa – i choć bywa uciążliwa w utrzymywaniu, pozwala nam uporządkować koncepcje, których używamy, ułatwia wdrażanie nowych członków zespołu oraz odwoływanie się do zasad w ramach code review. Nie musimy opisywać wzorców w sposób bardzo sformalizowany – w ramach prób i błędów możemy znaleźć taką konwencję, która będzie zwięzła i czytelna dla wszystkich.
Po drugie, komunikacja. Jeśli pracujesz w większym zespole frontendowym, warto wprowadzić okresowe spotkania przewidziane na dyskusję podejść stosowanych w projekcie – zarówno pain points’ów, z którymi się spotykamy, jak i propozycji usprawnień, które mogą im zaradzić.
Po trzecie, konsekwencja. Czyli weryfikowanie zmian w code review. A jeśli widzisz, że inni członkowie zespołu mają problem z wdrażaniem zasad w życie, sprawdź, czy ich definicja jest dla wszystkich jasna i mają do niej łatwy dostęp.
Podsumowanie
W powyższym artykule starałem się argumentować o zaprzestaniu używania serwisów na rzecz obiektów o jasno zdefiniowanym przeznaczeniu i precyzyjnej nazwie. Próbowałem podkreślić, że nieprecyzyjna nazwa obiektu może prowadzić do traktowania jego odpowiedzialności nazbyt swobodnie. A także, że rozwiązaniem może być wykorzystywanie powszechnie znanych wzorców projektowych oraz tworzenie własnych wzorców odpowiadających na problemy specyficzne dla danego projektu. Zaś po ich zdefiniowaniu bardzo ważne jest skuteczne wdrożenie ich w projekt poprzez tworzenie dokumentacji, komunikację wewnątrz zespołu oraz konsekwencję w egzekwowaniu ustalonych zasad.