Czy architektura na frontendzie jest potrzebna? Devdebata
– Idealna architektura niweluje ryzyko i wytycza klarowne cele oraz ścieżki do stworzenia niezawodnego systemu informatycznego, natomiast wiąże się z ogromnymi kosztami „in principio”, które nie zawsze mogą się zwrócić – powiedział nam Marcin Milewicz, Senior Frontend Developer w Xebia. Zaprosiliśmy trzech frontendowców do devdebaty i zapytaliśmy ich o to, w jaki sposób pochodzą do opracowywania architektury na potrzeby frontendu.
W devdebacie udział wzięli:
- Maciej Krawczyk. Senior Frontend Developer w Xebia. Z programowaniem związany odkąd pamięta. Zaczynał od tworzenia prostych gier w HTMLu i Flashu, z czasem jako freelancer realizował kompletne serwisy www. W projektach komercyjnych jako full-stack developer uczestniczy od około 10 lat, głównie w branży fin-tech. Obecnie realizuje się w roli Frontend Tech Leada w jednym z projektów w Xebia. Prywatnie ojciec dwójki bąbelków, fan Gwiezdnych Wojen i MCU. Resztki wolnego czasu marnuje na graniu w Xboxa.
- Marcin Milewicz. Senior Frontend Developer w Xebia. Programista z pasji od ponad półtorej dekady, profesjonalnie przeszło 10 lat. Po godzinach OSS kontrybutor, Toastmaster, miłośnik podróży, gór oraz psychologii pozytywnej. Silnie nastawiony na innowacje technologiczne i rozwój osobisty.
- Damian Kowalski. Frontend Developer/Team Leader w Xebia. Damian jako Frontend Developer posiada ponad 10-letnie doświadczenie w pracy nad wszelkiej maści aplikacjami. Obecnie dba o jakość frontendu w wielu projektach, a także aktualizuje radar frontendowy w PGS Software – lubi być na bieżąco z tym, co zmienia się na rynku. Poza tym gra amatorsko w piłkę nożną, interesuję się VRem i rynkiem Crypto.
Spis treści
Czy architektura na froncie jest potrzebna?
Maciek Krawczyk:
W moim odczuciu odpowiedź na to pytanie brzmi: to zależy. Wyjaśnijmy sobie może na początek, czym jest w ogóle architektura aplikacji. W wielkim skrócie jest to zbiór reguł i zasad dotyczących organizacji kodu w projekcie – plików, klas, funkcji, modułów, obiektów itp. To czy powinniśmy poświęcać czas na wypracowanie odpowiedniej architektury dla naszej aplikacji zależy głównie od jej rozmiarów oraz cyklu życia. Dla małych projektów, o których zapomnimy po miesiącu od implementacji, nie ma większego sensu palić czasu na over-engineering. Projekt taki zazwyczaj ma powstać szybko i wypełnić określone zadanie.
Wyjątek mogą stanowić projekty niekomercyjne, pisane “dla siebie”, których celem jest samodoskonalenie i zdobywanie praktycznych umiejętności projektowania aplikacji. Sprawa wygląda inaczej w przypadku większych, rozwojowych projektów, które w miarę upływu czasu będą się rozrastać o nowe funkcjonalności. Opracowanie architektury przed rozpoczęciem implementacji oszczędzi nam czasu i nerwów podczas wprowadzania zmian oraz dodawania nowych funkcjonalności w przyszłości. Wybór architektury w takim przypadku jest równie ważny jak np. wybór języka programowania czy frameworka. Dobrze zaprojektowana aplikacja, z odpowiednio dobraną architekturą, umożliwi nam łatwiejszą rozbudowę i dodawanie nowych elementów bez wielkiej ingerencji w już istniejące.
Damian Kowalski:
To zależy od skali projektu i wymagań biznesowych. Dla prostego PoC często liczy się czas dostarczenia i walidacji założeń, po czym całość może trafić do kosza. W takich przypadkach spędzanie godzin, czy nawet dni, nad wymyślaniem architektury, która mogłaby skalować się wraz ze wzrostem zespołu jest moim zdaniem wyrzucaniem pieniędzy w błoto. Jeśli natomiast z góry wiemy, że projekt będzie dostarczany przez wiele zespołów, myślę że warto na początku zastanowić się nad podziałem kodu np. wykorzystując DDD i oddzielając współdzielone komponenty do osobnej biblioteki komponentów. Przy tym podejściu opłaci się wspólnie ustalić reguły, które wszyscy powinni respektować.
Wszystko warto robić z głową tak, żeby ostatecznie usprawnić pracę i zadbać o jakość. Jestem wielkim fanem podejścia YAGNI – z reguły wymyślanie rozwiązań na X lat do przodu się nie sprawdza, bo operujemy na bardzo dynamicznie rozwijającym się kawałku oprogramowania, dlatego trzeba dostosowywać podejście do wielkości projektu.
Marcin Milewicz:
Gdy definiujemy architekturę aplikacji frontendowej powinniśmy nie tylko myśleć o wyzwaniach technicznych lub funkcjonalnych, ale także o kosztach, możliwościach wykonania, ryzyku, oczekiwanej jakości czy harmonogramie prac przy rozwoju aplikacji. Idealna architektura niweluje ryzyko i wytycza klarowne cele oraz ścieżki do stworzenia niezawodnego systemu informatycznego, natomiast wiąże się z ogromnymi kosztami „in principio”, które nie zawsze mogą się zwrócić. Nieważne czy projektujemy architekturę dla całego systemy informatycznego, czy tylko dla części frontendowej, zawsze powinniśmy szukać złotego środka.
Złoty środek określi nam proporcje pomiędzy wkładem czasu przeznaczonym na dokładne zaprojektowanie, a późniejszymi zyskami z tego wynikającymi. Zanim wybierzemy konkretne technologie i zasady projektowe, warto ustalić jakie są nasze driver’y architektoniczne. Pozwoli nam to zrozumieć istotę problemu, który aplikacja ma rozwiązać oraz ocenić jakie są nasze możliwości, aby można było ten problem rozwiązać w optimum najmniejszego kosztu i najmniejszego ryzyka.
Jakimi regułami powinniśmy się kierować przy tworzeniu aplikacji frontend?
Maciek Krawczyk:
W aplikacjach opartych o gotowy framework (np. Angular), struktura czy architektura może być w jakimś stopniu narzucona lub przynajmniej zasugerowana zarówno przez sam framework, jak i przez ogólnie przyjęte przez społeczność zasady tworzenia w nim aplikacji. Natomiast w przypadku projektów w czystym JS-ie mamy dużo większą dowolność, jeżeli chodzi o podejmowanie architektonicznych decyzji. Często oznacza to nie lada wyzwanie, jednak wcale nie musi wiązać się z wymyślaniem koła na nowo. Istnieje mnóstwo gotowych wzorców, z których można skorzystać oraz narzędzi, które je wspierają.
Przykład? Nasza aplikacja składać się będzie z wielu komponentów umieszczonych na wspólnym dashboardzie. Możemy wybrać Micro Frontends. Będzie raczej niewielka, skupiona wokół core’owej logiki biznesowej? Możemy wybrać model hexagonalny i DDD. Wszystko zależy od wymagań danego projektu.
Damian Kowalski:
Dla mnie minimum to ESLint i Prettier używające reguł przyjętych przez środowisko np. zaproponowane przez Airbnb. Jeśli zdecydujemy się na wybór frameworka, a w znakomitej większości przypadków tak właśnie będzie, to należy stosować się do zasad przyjętych w danym ekosystemie. Najgorsze co możemy zrobić to np. dodawać dependency injection do projektu na React, bo takie mamy przyzwyczajenia w pracy z Angularem. Ważne jest też zapewnienie odpowiedniej jakości i tu oczywiście należy pomyśleć o testach. Według mnie najlepiej sprawdzają się testy integracyjne i e2e, ponieważ są najbliżej tego jak końcowy użytkownik korzysta z naszej aplikacji. Kiedyś testy te były w mniejszości, z powodu wysokiego “kosztu” ich implementacji i utrzymania. Dzięki takim narzędziom jak np. Cypress jest to nieaktualne.
Marcin Milewicz:
Reguły projektowe powinny określić język programowania oraz jego założenia. Aktualnie nie widzę żadnego powodu, aby nie używać Typescript’a w przypadku nowoczesnych rozwiązań frontendowych. Doświadczenie w pracy produkcyjnej w językach takich jak JavaScript, Java, Python oraz Typescript przekonało mnie, że używanie języków typowanych, a w szczególności silnie typowanych jest zarówno bezpieczniejsze, jak i efektywniejsze w dłuższej perspektywie utrzymywania aplikacji. Konfiguracja Typescript’a pozwala na wiele kompromisów w późniejszym jego użytkowaniu, dlatego warto już na samym początku ustalić pewne założenia. Jako minimum zawsze polecam minimum trzymanie się trybu „strict”.
W różnych projektach, w których miałem okazję uczestniczyć, developerzy często zastanawiali się nad prawidłowym podejściem w strukturyzacji kodu za pomocą umiejscowienia ich w folderach. Widziałem bardzo wiele podejść odnośnie struktury folderów w projektach. Jedne były lepsze, drugie gorsze, a sam temat „złotego podziału” przewijał się bardzo często. Zauważyłem też, że często młodsi developerzy zbyt bardzo przywiązują się do samej struktury katalogów w projekcie i utożsamiają ją z odpowiednim podziałem modułowym aplikacji. Niestety nie jest to do końca tożsame, bo struktura architektury modułowej w aplikacji frontendowej to nie tylko same umiejscowienie kodu w katalogach, ale także wzajemne relacje modułów.
Określenie wzajemnej relacji modułów możemy identyfikować poprzez wzajemne importy poszczególnych części kodu. Uważam, że tutaj należy położyć największy nacisk przy projektowaniu założeń projektowych. To w jaki sposób moduły wzajemnie z siebie korzystają określa nam ich niezależność, sprzężenia, a także stabilność.
Co z ostatnio popularnym podejściem Micro Frontends?
Maciek Krawczyk:
Idea microfrontends polega na podziale aplikacji SPA na szereg mniejszych komponentów, z których każdy stanowi osobną, niezależną aplikację i umieszczeniu ich na jednej stronie obok siebie. Może to trochę przypominać architekturę mikroserwisów stosowaną w przypadku aplikacji serwerowych. Projekt, w którym aktualnie uczestniczę w ramach Xebia (dawniej PGS Software) niejako wpisuje się w tę definicję, więc można uznać, że stanowi przykład aplikacji opartej o Micro Frontends. Aplikacja składa się z kilku instancji, z których każda umieszczona jest na stronie wewnątrz osobnego iframe’a, natomiast komunikacja pomiędzy nimi odbywa się za pomocą mechanizmu window.postMessage().
Taki układ wymagał stworzenia wielu mechanizmów służących m. in. do inicjalizacji poszczególnych komponentów, wymiany danych pomiędzy nimi oraz synchronizacji stanu. Ponieważ jestem wielkim fanem biblioteki RxJS oraz reactive programmingu, wiele z nich opiera się o te właśnie mechanizmy. Jako przykład można wymienić między innymi współdzieloną szynę danych, która zamienia wiadomości wysłane za pomocą window.postMessage() na strumień z RxJS czy naszą własną, prostą implementację obserwowalnego store’a wzorowanego nieco na Reduxie, który również korzysta ze wspomnianej szyny jako strumienia akcji.
Damian Kowalski:
Mikroserwisy na froncie? Tak, to da się zrobić. W moim projekcie korzystamy z tego podejścia, gdzie każdy zespół (a jest ich kilkanaście) ma swoje repozytorium z komponentami. Całość jest później sklejana w 3 głównych repozytoriach. W przypadku Micro Frontends najlepiej sprawdza się podział wertykalny aplikacji – najczęściej jest to podział na “strony”. I to sprawdza się całkiem nieźle. Należy jednak pamiętać, że w porównaniu z backendem naszym środowiskiem uruchomieniowym jest przeglądarka (w przypadku aplikacji webowych oczywiście), co narzuca ograniczenia inne niż te w przypadku backendu.
Są gotowe rozwiązania np. single-spa.js.org, frint.js.org, mosaic9.org, które rozwiązują problemy skalowalności i wydajności – każde z nich w trochę inny sposób. Nie powstał jednak jeszcze żaden “silver bullet” i pewnie nigdy nie powstanie. Należy odpowiedzieć sobie na pytanie czy na pewno jest nam takie podejście potrzebne i czy ma szansę przyspieszyć pracę. Moim zdaniem warto tym tematem się zainteresować dla naprawdę dużych projektów.
Marcin Milewicz:
Koncepcja microfrontends rozwiązująca pewny zbiór problemów, ale także niesie za sobą pewne wady i ograniczenia. Osobiście miałem okazję do projektowania i wdrażania od zera architektury microfrontendsowej, zatem na własnej skórze mogłem przekonać się o korzyściach, ale i wadach takiego podejścia. Mówiąc o podejściu microfrontendowym mówimy o czymś więcej, aniżeli o technicznym rozwiązaniu. To także podejście do dopasowywania zespołów wokół poszczególnych microfrontendów i określania jasnych odpowiedzialności za poszczególne części aplikacji. Dla mnie istotne jest to, aby pamiętać, że microfrontendsy mają nam pomóc rozwiązać problemy, a nie dołożyć nowe.
Jeśli w naszej organizacji mamy realizować projekt w kilku/kilkunastu niezależnych zespołach to jest to jakiś znak, że możemy rozważać takie podejście. Przy takim modelu wytwarzania oprogramowania koncepcja autonomicznych microfrontendów może rozwiązać wiele problemów i nakładów komunikacyjnych. Podejście microfrontendsy to rozległy temat, na który można byłoby dużo mówić i pisać, dlatego po więcej moich przemyśleń zapraszam na Youtube i Githuba, gdzie znajdziecie moje prezentacje oraz przykładowe koncepcje w tym temacie.
- youtube.com/watch?v=eNA2WaNarww
- youtube.com/watch?v=sNdX8CmRq5A
- github.com/marcinmilewicz/microfrontendly
- github.com/marcinmilewicz/nx-microfrontends-angular-with-react
Mono czy multirepo?
Maciek Krawczyk:
Jedną z kluczowych decyzji jakie musi podjąć zespół projektowy stanowi wybór pomiędzy trzymaniem całego kodu w jednym repozytorium a rozbiciem go na wiele osobnych repozytoriów np. po jednym dla każdego modułu. Oba podejścia mają jak zwykle swoje wady i zalety, a to na którą opcję powinniśmy się zdecydować zależy m. in. od rozmiaru projektu, jego złożoności oraz struktury zespołu, który ten projekt będzie rozwijać.
W przypadku małych zespołów rozsądnym jest umieszczenie całego projektu “klasycznie” w jednym repozytorium. Zapewnia to każdemu z członków zespołu łatwy dostęp do całego kodu, nie ma problemu z utrzymywaniem zależności a procesy CI oraz deploymentu również nie muszą być bardzo skomplikowane.
Problem zaczyna się pojawiać, kiedy nasz projekt rozrośnie się do sporych rozmiarów. Ilość plików i podfolderów zaczyna przytłaczać, indeksowanie plików w IDE trwa wieczność a liczba zależności w projekcie przyprawia o zawrót głowy. W tym przypadku najprawdopodobniej lepiej sprawdzi się podejście, w którym projekt podzielony jest na wiele repozytoriów, np. poprzez wydzielenie poszczególnych modułów do osobnych paczek. Wprowadza to jawną separację modułów, które z racji tego, że są mniejsze są również łatwiejsze w utrzymaniu. Dodatkowym plusem jest możliwość ich utrzymywania przez osobne zespoły.
Damian Kowalski:
Często jest tak, że architektura w projekcie jest odwzorowaniem struktury firmy. Jeśli biznes chce podziału na oddzielne zespoły tworzące osobne ficzery i robiące osobny, niezależny od nikogo deployment to często pada na multirepo. Jednak frontend jest o tyle odmienny od mikroserwisów backendowych, że na końcu i tak musimy te osobne klocki poukładać w jedną, uruchamianą w przeglądarce aplikację. Z mojego doświadczenia wynika, że to sklejanie rodzi najwięcej problemów, chociażby dlatego, że musimy wziąć pod uwagę performance. Opowieści o używaniu w jednej apce wielu frameworków, często w różnych wersjach można włożyć między bajki. No chyba, że robimy apkę używaną wewnętrznie i totalnie nie obchodzą nas czasy ładowania czy SEO.
Moim zdaniem dla dużych projektów frontendowych bardziej sprawdza się Monorepo i rozwiązania jak np. Nx, gdzie te sklejanie aplikacji w jedną działa out of the box. Jednocześnie wciąż robimy deployment tylko tych kawałków kodu, które się zmieniły. Monorepo sprawdza się też dobrze w przypadku, kiedy i frontend i backend używają tego samego języka np. TypeScript. W tym przypadku możemy w łatwy sposób wydzielić interfejsy, które będą współdzielone między frontendem i backendem zapewniając tym samym spójność kontraktu i minimalizując ryzyko błędów wynikających z integracji BE/FE. Polecam zapoznać się jak z tymi wyzwaniami radzi sobie Nx.
Marcin Milewicz:
Nie wiem dlaczego, ale podjęcie decyzji o tym czy chcemy iść w mono lub w multirepo zawsze elektryzuje wielu developerów. Wielokrotnie spotykałem osoby, które w zaparte szły w jedno lub drugie rozwiązanie. Zwykle obie strony miały rację i dawały dużo argumentów za daną strategią w magazynowaniu kodu. Sam jestem bardziej zwolennikiem rozwiązań monorepo, ale nie twierdzę, że to uniwersalne rozwiązanie. Sądzę, że warto w przypadku wyboru strategii utrzymywania kodu cofnąć się do naszych założeń architektonicznych i spróbować wyobrazić sobie jak będzie wyglądała praca w projekcie.
Warto wziąć pod uwagę strategię wersjonowania, liczbę osób w zespole, liczbę zespołów, odpowiedzialność zespołów, a także jaka będzie granularność poszczególnych modułów aplikacji oraz z jak wieloma modułami będą pracować pojedyncze osoby. Łatwiejszy setup środowiska, wyraźne rozdzielenie funkcjonalności oraz wersjonowanie to często argumenty do wyboru multirepo. Obecnie jednak mamy wiele narzędzi, które pozwalają osiągnąć te rezultaty w koncepcji monorepo. Sam miałem do czynienia z Lerną czy NX’em. Moim zdaniem szczególnie NX zasługuje na przeanalizowanie go jako potencjalnego rozwiązania wad monorepo przy jednoczesnym zachowaniu i uwypukleniu zalet tej koncepcji.
Oczywiście NX’a nie możemy traktować jako mitycznej „srebrnej kuli”, bo prawdopodobnie w przypadku prostych i małych aplikacji nie przyniesie on nam korzyści, a może podwyższyć próg wejścia do projektu. Jeśli weźmiemy pod uwagę aplikację podzieloną np. na kilkaset dużych modułów to to także koncepcja monorepo może okazać się nietrafiona. Wielokrotnie powtarza się, że Google kod swoich tysięcy projektów trzyma w monorepo i jest to skalowalne. Należy jednak pamiętać, że Google ma swój system wersjonowania. W przypadku, kiedy my używamy GITa to przy kilkuset dużych modułach może być już on mało wydajny do swobodnej pracy. Określenie sposobu trzymania kodu w repozytorium to także jasne wypracowanie zasad projektowych.
Praca w monorepo będzie o wiele bardziej wydajna jeśli będziemy mogli przyjąć politykę „pojedynczej wersji” (Single Version Policy) oraz częstych release’ów. Przy multirepo możemy natomiast stworzyć w łatwy sposób niezależny system wersjonowania (np semver) dla poszczególnych modułów, chociaż wyżej wymienione narzędzia pozwalają nam także realizować taką strategię dla monorepo. Kwestia wersjonowania to zupełnie odrębny i duży temat, którego nie chcę tutaj poruszać, ale należy o tym pamiętać przy wyborze pomiędzy mono i multirepo.
Projektując architekturę i określając takie szczegóły jak sposób magazynowania kodu warto brać pod uwagę organizację pracy oraz interesariuszy, którzy z naszego kodu będą korzystać. Czasem przy projektowaniu wczesnej architektury systemu informatycznego tworzy się mapę interesariuszy z której tutaj można skorzystać. Na jej podstawie możemy określić, kto i z jakiej części całego kodu aplikacji będzie korzystał. Jeśli okaże się, że poszczególne zespoły czy jednostki interesariuszy są w pełni autonomiczne w kontrybucji swojego kodu, zaś poszczególne moduły są używane przez nich wzajemnie już na poziomie zbudowanych artefaktów, to koncepcja monorepo może okazać się zupełnie niepotrzebna.
Zdjęcie główne artykułu pochodzi z unsplash.com.