Jak pisać czysty kod w Angularze? Czyli o typowych błędach programistów.

O czystym kodzie powiedziano już wiele. Większość programistów, nawet tych z minimalnym doświadczeniem, zna słynne książki oraz filmy Uncle Bob’a, w których autor na wiele różnych sposobów stara się wytłumaczyć słuszność idei Clean Code. Jednak mimo to, że jest to wiedza powszechnie znana i raczej nikt nie ma wątpliwości, że kod powinien być pisany w sposób jasny i zrozumiały, to spotykamy się często z projektami w których coś chyba poszło w pewnym momencie w nieodpowiednią stronę.

Pisząc w Angularze także powinniśmy się trzymać pewnych ustalonych z góry standardów, aby efekt naszej pracy nie odrzucał innych developerów, którzy pracują obecnie z nami lub przejmą nasz kod w przyszłości. W praktyce jednak wygląda to tak, że ilu developerów w projekcie, tyle opinii na temat czystego kodu. Na szczęście pracując z Angularem nie musimy toczyć batalii o to, czyja racja jest bardziej prawdziwa, ponieważ twórcy tego framework’a przyjęli jeden oficjalny Style Guide. Godząc się na wykorzystanie Angulara w naszym projekcie, powinniśmy z automatu zgodzić się z powyższymi regułami (czasami możemy je lekko zmodyfikować w zależności od czynników od nas niezależnych). Bez względu na poziom naszej wiedzy i percepcji w rozumieniu kodu pisanego musimy pamiętać o jednej ważnej rzeczy! Programista kod tworzy nie dla siebie, ale dla innych! Kod zawsze jest i będzie o wiele razy więcej czytany, niż pisany. Co więcej, będzie czytany przez innych, więc piszmy kod z myślą o tych osobach!

Czemu zatem ma służyć ten artykuł?

Oficjalny Style Guide jest publicznie dostępny na oficjalnej stronie Angulara, więc po co powielać te informacje tutaj? Otóż przez ostatnie kilka lat, kiedy pracowałem w projektach angularowych z wieloma różnymi developerami, rzadko kiedy dało się zauważyć, aby ktoś do tych wyznaczonych standardów się stosował. Powodów było wielu: jedni o nim nigdy nie słyszeli, inni niby coś tam słyszeli, ale widzieli, że w różnych tutorialach tego się nie stosowało. Jeszcze inni termin style guide kojarzyli tylko z linterami i zakładali, że jeśli jest takowy w projekcie jest, to oni już mogą tworzyć swój wesoły kod, bo przecież „lint przechodzi”. Kiedy rozmawiam z mało doświadczonym developerem o Angularze i z jego strony pada pytanie: „Od czego zaczać?”. Zawsze kieruję go pod ten link, ponieważ moim zdaniem, kryje się tam fundament angularowego Clean Code’u. Wszystkich Wam serdecznie polecam sumienne przejrzenie Style Guide z oficjalnej strony, zaś poniżej przedstawię te reguły, które może nie tyle co uważam za najważniejsze (wszystkie są bardzo ważne!!!), co najczęściej spotykane.

1. Praca z plikami w projekcie

Kiedy chcecie dodać do projektu nowy byt, bez względu czy to będzie: module, service, component, pipe, model czy reducer, to zawsze każdy z nich powinien posiadać swój osobny plik. Korzyści są oczywiste. Pliki posiadają mniejszą liczbę wierszy kodu, przez co są łatwiejsze w zrozumieniu oraz utrzymaniu. W przypadku pracy zespołowej nad projektem, łatwiej mergować zmiany w niezależnych plikach, aniżeli w jednym wielkim pliku. Zasady mówią, że powinniśmy zastanowić się w sytuacji, kiedy kod jest obszerniejszy niż 400 linii. To prawda, maksymalna liczba linii kodu w danym pliku może nie jest zawsze taka oczywista, ale jeśli przekracza te 400, to jest to więcej niż pewne, że gdzieś coś można wydzielić.

Innym aspektem tutaj jest lazy loading. Osadzenie pojedynczego komponentu w danym pliku i użycie domyślnego eksportu stwarza możliwość lazy loadingu dla tego komponentu, niezależnie od innych bytów w projekcie np. wydzielanie klas dla routingu na każdy feature module.

2. Spójne nazwy plików i poszczególnych bytów

Zasada jest taka, że każdy plik powinien być nazywany według wzorca {feature}.{type}.ts, a odpowiednia konwencja nazewnictwa danego bytu w Angularze to kebab-case. Dla nazwy klas natomiast powinno używać się konwencji upper camel-case (zwanej czasami Pascal case) z odpowiednim sufiksem świadczącym o typie bytu danej klasy (Component, Directive, Module, Pipe, Model lub Service). Spójrzmy na poniższy przykład zastosowania tej zasady.

Ponadto pliki testów unitowych powinny trzymać się konwencji {feature}.{type}.spec.ts, zaś pliki testów end-to-end {feature}.{type}.e2e-spec.ts

3. Poprawny selektor komponentu

Selektor komponentu, czyli nazwa po jakiej Twój komponent będzie identyfikowany przez DOM ma bardzo duże znaczenie. Po pierwsze selektor powinien używać wcześniej już wspomnianej konwencji kebab-case. Po drugie, jego nazwa powinna być poprzedzona odpowiednim prefiksem. Prefiks powinien nawiązywać do obrębu funkcjonalności w jakiej dany komponent się znajduję. Np. jeśli chcemy stworzyć komponent Product, reprezentujący dany przedmiot w obrębie funkcjonalności modułu odnoszącego się do wyświetlania rekomendowanych produktów, to powinien on posiadać selektor recommendation-product, przy założeniu, że nazwa tej funkcjonalności to Recommendation. Jeśli w tej samej aplikacji chcemy posiadać komponent listy użytkowników, który natomiast zostanie użyty w funkcjonalności panelu admina to analogicznie jego selektor powinien nazywać się admin-users. Zasada ta ma na celu uniknięcie przypadkowych kolizji nazw dla komponentów stworzonych w obrębie różnych funkcjonalności. Pomyślmy, co stałoby się w przypadku, jeśli posiadalibyśmy komponent z selektorem „users” w obrębie panelu admina, a następnie chcielibyśmy dodać komponent listy użytkowników dla nowego modułu wyświetlającego użytkowników o podobnych zainteresowaniach jak nasze? Doszłoby do kolizji nazw, ponieważ dwa różne komponenty musiałyby być identyfikowane w DOMie jako users, a taka sytuacja jest niemożliwa. Poniżej przykład prawidłowego nazewnictwa.

W powyższym przykładzie, założyliśmy, że nasza funkcjonalność nazywa się Recommendation lub Admin. W takim przypadku stosunkowo łatwo użyć pełnej nazwy jako prefiksu. Jeśli natomiast nazwa naszej funkcjonalności składałaby się z wielu wyrazów, np. The Unique Recommendation System, zaleca się użycie skrótu np turs-users, turs-product. Analogiczną zasadę powinniśmy stosować także dla dyrektyw.

4. Interfejsy

Interfejsy, które pojawiły się w TypeScript’cie sprawiają Javascriptowcą bardzo dużo problemów. Nagminnie spotykałem się z sytuacją ich niepoprawnego zastosowania jak i nieodpowiedniego nazywania. Łatwo się domyślić, że wynika to z faktu, że  interfejsy nie były obecne w AngularJS. Wielu programistów AngularJS jak i czystego JS’a w momencie kiedy zaczęło swoją przygodę z Angularem po prostu nie zrozumiało do końca idei stosowania słowa kluczowego interface, ale to już zupełnie inny temat. Najważniejsze jest, aby zapamiętać:

  • Interfejsy nazywamy według konwencji upper-camel-case i nigdy nie poprzedzamy samej nazwy prefiksem „I”. Używanie nazw typu: IProduct, IUser, IItem jest po prostu zbędne. Zupełnie poprawnie będzie jak użyjemy prostych nazw, analogicznie: Product, User, Item. Także oficjalny Style Guide języka TypeScript przestrzega nas przed tym.
  • Warto rozważyć zastosowanie interfejsu w przypadku modelu danych!
  • Zwykle częściej poprawnym podejściem będzie użycie wzorca kompozycji has-a, aniżeli relacji is-a.
  • Dla angularowych bytów (components, directives, pipes) stosujemy klasy, a nie interfejsy.

5. Delegowanie złożonej logiki biznesowej z komponentu do serwisu

Często spotykałem się z kodem, gdzie komponenty i dyrektywy miały po kilkaset, a nawet tysięcy(!) linii kodu. Taka sytuacja skutecznie uniemożliwia szybkie zapoznanie się z intencjami autora kodu. Przeładowane różnymi funkcjonalnościami klasy burzą pierwszą z elementarnych zasad SOLID, Single Responsibility Principle. Jak już wspomniałem na początku, kod nie powinien być pisany „dla autora od autora”, ale dla każdego potencjalnego użytkownika tego kodu. Naprawdę, nawet doświadczony programista, który widzi kilkaset linii kodu, może się przerazić. Czas potrzebny na naprawę najmniejszego błędu rośnie, a pieniądze klienta zamawiającego od nas produkt szybko się wyczerpują. Jest to z pewnością sytuacja, której chciałbyś uniknąć. Jeśli komponent wyświetla listę produktów pobranych z zewnętrznego API to kod odpowiedzialny za wykonanie request’u do o endpoint’u nie powinien znajdować się bezpośrednio w nim. Komponent powinien skorzystać z angularowego serwisu (bądź innej abstrakcji), zaś w tym serwisie powinna znajdować się wspomniana implementacja. Podobnie, jeśli komponent dostarcza możliwość sortowania na trzy różne sposoby listy produktów, to sama logika sortowania także powinna znaleźć się poza nim. W samym zaś komponencie powinno się tylko i wyłącznie wykonywać metody sortujące, które wystawia nam odseparowana abstrakcja.

Zalet takiego podejścia jest oczywiście więcej. Kiedy zastanawiamy się czy warto wydzielić daną logikę biznesową do osobnego serwisu to powinniśmy sobie zadać pytanie. Czy komponent powinien martwić się o wykonanie danej funkcjonalności? Np. jeśli pobieramy dane z zewnętrznego API to odpowiedź jest prosta. Komponent powinien martwić się jak dane zaprezentować, a nie skąd i jak je pobrać. Komponent te dane powinien otrzymać, zaś nie jest istotne dla niego w jaki sposób one zostaną dostarczone.

Unikajmy także wielkich serwisów robiących wszystko. Zakładając, że mamy stworzyć komponent prezentujący tabelkę z różnymi danymi, które można sortować, zaznaczać, usuwać lub edytować, to dobrym podejściem nie będzie stworzenie jednego product-grid.service.ts. Każda funkcjonalność to tak naprawdę osobna odpowiedzialność. Lepszym podejściem będzie stworzenie kilku serwisów takich jak: product-sort.service.ts, select-product.service.ts, delete-product.service.ts czy edit-service.product.ts. Doskonale wiem, że w niektórych językach/frameworkach stosuje się podejście do obsługi jednego typu abstrakcji w jednym miejscu np. Active Record Pattern, ale nie w Angularze! Pamiętajmy jeszcze raz, że kod piszemy nie dla siebie, a dla innych. Specjalista Ruby on Rails, Django czy ActiveJDBC zrozumie podejście stosowane we wzorcu Active Record, ale specjalista Angulara niekoniecznie. Nie musi! Specjalista Angulara musi znać wzorce stosowane w Angularze (reactive programming, data store, MVVM). Zatem unikajmy zapożyczeń technik rozwiązywania problemów z innych framework’ów, bo Angular nimi nie jest. Założenie, że w przyszłości nasz angularowy kod będzie utrzymywał i rozwijał ktoś inny niż specjalista Angulara, jest co najmniej dziwne.

Kolejna wymierną korzyść zauważa się także podczas testowania. Testowanie jest łatwiejsze, pliki zawierające testy nie mają tysiąca linijek kodu, a dla odseparowanych zależności można łatwo stworzyć mock’i w zależności od charakteru testów.

6. Poprawne użycie dyrektyw

Programiści którzy migrowali z AngularJS’a do Angulara muszą doskonale wiedzieć co to jest dyrektywa. No właśnie, muszą? Otóż, nie jest to do końca jasne. Podczas rozmów z developerami AngularJS często łapałem ich na tym, że nie widzą podstawowej różnicy pomiędzy bytami directive, a component w AngularJS. AngularJS w wersji 1.5 wprowadził możliwość tworzenia abstrakcji component, która realizowała podejście tworzenia własnych elementów DOM. Mniej lub więcej odzwierciedla on typ component’u w Angularze. Co była jednak zanim wersja AngularJS 1.5 została wydana? W starszych wersjach AngularJS, aby stworzyć własny element DOM pełniący funkcję niezależnego komponentu, używano właśnie dyrektyw i było to jak najbardziej poprawne podejście. Wielu programistów jednak przespało moment wypuszczenia wersji 1.5 i nie do końca zapoznali się z podstawową różnicą w zastosowaniu obu elementów framework’a. Wielu programistów nie miała szans na zapoznanie się z tym, ponieważ w wypuszczonych przez nich na produkcję aplikacjach nawet nie było mowy o migracji do nowszej wersji. W projektach w których taka migracja miała miejsce okazywało się, że nie ma sensu już przepisywać wszystkich dyrektyw na świeżo wprowadzone komponenty. Trochę to skomplikowane i zagmatwane, ale niestety taki AngularJS był i cieszmy się, że teraz już używamy nowego Angulara, który odciął się w każdy możliwy sposób od długu technologicznego jego starszego brata.

Jaka jest Zatem Różnica pomiędzy dyrektywą, a komponentem?

Różnica jest bardzo prosta i zawiera się w jednym zdaniu. Używamy dyrektywy, kiedy chcemy zaimplementować logikę prezentacyjną, która nie wymaga powiązania templatki html. Warto zaznaczyć, że pojedynczy element, może posiadać nawet kilka dyrektyw. Spójrzmy na sytuację, gdy potrzebujemy zaimplementować generyczne rozwiązanie dla sytuacji, kiedy użytkownik najedzie kursorem myszki na odpowiedni element.

Podsumowując nasz Angular Clean Code,

powyżej opisałem takie przypadki z którymi nagminnie spotykałem się w mojej karierze pracując z różnymi projektami angularowymi. Oczywiście nie są to wszystkie reguły jakie powinniśmy znać pracując z kodem aplikacji angularowej. Jeszcze raz zachęcam gorąco do zapoznania się i nauczenia tego co mówi nam oficjalny przewodnik na oficjalnej stronie Angular.io. Jeśli „nie czujecie” niektórych zasad albo coś wydaje Wam się dziwne to zachęcam do kontaktu. Zawsze w ramach możliwości czasowych postaram się coś sensownego odpowiedzieć!

Dodaj komentarz

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