Programistą można zostać na wiele sposobów - większość zaczyna w domowym zaciszu, ucząc się danego języka lub technologii, ciesząc się tym i po prostu starając się doprowadzić swoje pomysły do realizacji. Podczas tego procesu człowiek poznaje kolejne konstrukcje językowe, coraz bardziej złożone zagadnienia i możliwości oferowane przez daną technologię, które przekładają się na lepsze i szybsze "osiągnięcia" w danej dziedzinie. To ważne. Natomiast bez odpowiedniego mentora lub możliwości pracy zawodowej czasami zapomina się o innej rzeczy, a mianowicie architekturze oprogramowania i efektywnej konstrukcji systemów IT.
Nie zrozumcie mnie źle - te zagadnienia również są często omawiane w tutorialach. Natomiast dzieje się to często przy okazji, gdyż są po prostu potrzebne do wykonania poszczególnych ćwiczeń (w przeciwieństwie np. do standardu kodowania, który niezbędny na tym poziomie nie jest). W zależności od jakości tego tłumaczenia, mróżne wzorce ogą być bardziej przyswajane przez studenta lub pomijane, gdy nie zostanie zasygnalizowane, że właśnie została przedstawiona koncepcja ważna nie tylko z punktu widzenia danej technologii, ale ogólnie sposobu projektowania jakiegokolwiek oprogramowania i dalszego rozwoju programisty. Tego typu zagadnienia są potem omawiane na studiach lub podczas kursów i zdarza się, że w umyśle studenta pojawia się myśl "hej, znam to".
I mimo że architektura oprogramowania to znacznie szersze pojęcie, a sposoby organizacji poszczególnych komponentów systemów IT są dzisiaj bardziej złożone, to aby je zrozumieć, warto cofnąć się do podstaw. I właśnie o takich fundamentalnych pojęciach architektonicznych dzisiaj porozmawiamy z myślą o osobach, które dopiero wchodzą w ten świat na poważnie, a wcześniej nie miały okazji np. samodzielnie zaprojektować żadnego oprogramowania. Co, oczywiście, nie jest żadnym wstydem, gdyż czasem robi to ktoś za nas lub framework sam narzuca pewien sposób myślenia. Tylko czy w tym przypadku wiemy dokładnie, dlaczego robimy to tak, jak robimy?
Architektura klient-serwer
Cóż, starsi stażem programiści po tym rozdziale zorientują się, że naprawdę będziemy wyjaśniać same podstawy. Zachęcam ich nie do porzucenia lektury, ale wręcz czegoś przeciwnego - przygotowania sobie herbaty, przeczytania dalszej części tekstu i zaproponowania w komentarzach innych fundamentalnych pojęć z zakresu architektury oprogramowania, które ich zdaniem powinien znać adept programowania.
Wyobraźmy sobie sytuację, w której korzystamy z aplikacji webowej służącej do komunikowania się z innymi użytkownikami - może być to serwis społecznościowy, komunikator internetowy czy nawet system komentarzy pod artykułami. Jedna osoba pisze wiadomość, którą może odczytać inny konkretny użytkownik lub wszyscy, a także na nią odpisać. Oczywiście, to nie jest tak, że informacja jest przesyłana bezpośrednio do przeglądarki innych użytkowników. Podobnie jak nawet w całkiem realnej poczcie lub firmie kurierskiej, istnieje pośrednik, który koordynuje dane przesyłane pomiędzy odbiorcami. Tym pośrednikiem jest serwer, natomiast odbiorcami - klienty (tak - w tym przypadku "klienty", a nie "klienci", podobnie jak w IT mamy "agenty", a nie "agentów"). Mówimy zatem o architekturze klient-serwer, w której wielu klientów może korzystać z jednego (choć nie zawsze) serwera, pełniącego rolę składowiska danych i synchronizatora komunikacji.
Ubierając temat z innej strony, klient to aplikacja, którą użytkownik otwiera u siebie. W przypadku aplikacji webowej służy do tego przeglądarka internetowa, która pobiera pliki strony z serwera (jako maszyny, a nie komponentu) i otwiera użytkownikowi "u siebie", korzystając z zasobów jego komputera. Jeszcze bardziej wyraźne staje się to w aplikacjach desktopowych oraz mobilnych, gdzie następuje instalacja (lub przynajmniej pobranie pliku wykonywalnego) i uruchomienie bezpośrednio w systemie operacyjnym urządzenia. Z kolei serwer jest oprogramowaniem działającym na serwerze i klienci komunikują się z nim poprzez interfejs, taki jak choćby API.
To oznacza, że istnieje jeden wspólny punkt kontaktu różnych użytkowników, ale także to, że najbardziej krytyczne informacje z perspektywy bezpieczeństwa powinny być przechowywane właśnie w aplikacji serwerowej - do plików aplikacji klienckich użytkownicy mają bezpośredni dostęp, a więc teoretycznie mogą modyfikować ich kod, narażającym tym samym system na nieuprawnione działanie. Z drugiej strony, to aplikacje klienckie zazwyczaj są szybsze - jako że są uruchamiane bezpośrednio na maszynach użytkowników, te obliczenia nie obciążają serwera i jeśli tylko jest to możliwe, warto to wykorzystać, nie nadwyrężając komunikacji z aplikacją serwerową. Jest to wykorzystywane np. w grach sieciowych, gdzie synchronizowane przez serwer są tylko krytyczne informacje, a np. szczegóły animacji kart czy broni są kalkulowane już bezpośrednio na komputerze gracza. Jest to rozsądne biorąc pod uwagę, że serwer obsługuje w danym momencie nie tylko nasz mecz, ale tysiące innych, a do tego - szczególnie w dynamicznych rozgrywkach - musi być wydajny i szybko odpowiadać.
Architektura klient-serwer jest tak powszechnie stosowana, że już nikt o niej specjalnie nie mówi, biorąc to rozwiązanie za pewnik. Tym niemniej, warto powiedzieć o przeciwieństwie, które mogą kojarzyć np. użytkownicy tzw. torrentów, a więc peer-to-peer (zapisywane niekiedy jako P2P). Taka architektura oznacza, że każdy komputer w danej sieci (tzw. węzeł) jest jednocześnie klientem i serwerem. Może zatem korzystać z zasobów innych serwerów, ale też sam świadczy zasoby dla reszty. Omija to koncepcję centralnego serwera, podnosząc odporność na awarię sieci, ale z drugiej strony nakładając więcej "obowiązków" na każdego użytkownika. Oba podejścia mają swoje zalety i wady, ale trzeba przyznać, że P2P jest na tyle specyficzny, że w dobie aplikacji webowych i chmur nie jest tak często używany. Zdecydowanie nie powinien być to pierwszy wybór przy projektowaniu "uniwersalnego" oprogramowania wykorzystującego sieć.
Ważna uwaga - podejście klient-serwer to zupełnie coś innego niż master-slave. To drugie odnosi się do sytuacji, gdy pewien węzeł, mający dokonać pewnych obliczeń (rozumianych bardzo ogólnie), dzieli dane i wykorzystuje inne jednostki, z których każda wykonuje część kalkulacji. Krótko mówiąc, jeden serwer synchronizuje pracę innych serwerów, a nie klientów. Trochę jak product owner w zespole programistów.
Frontend i backend
Z pojęciem klient-serwer wiąże się też często przywoływane w artykułach, a nawet na tym blogu pojęcie frontendu oraz backendu. W pewnym uproszczeniu można powiedzieć, że frontend to to, co widzi użytkownik, a backend to to, czego nie widzi.
Frontend generalnie kojarzy się z interfejsem graficznym i kontrolkami. W aplikacjach webowych to właśnie ta część, która jest pobierana przez przeglądarkę użytkownika i która ma za zadanie zaprezentować dane oraz dać możliwość operowania na systemie. Tenże frontend komunikuje się z backendem, który jest częścią systemu odpowiadającą za logikę obliczeniową oraz komunikację z bazą danych. Często więc za frontend uważa się aplikację kliencką, a za backend - aplikację serwerową.
Natomiast warto wiedzieć, że może istnieć aplikacja w architekturze klient-serwer, w której frontend i backend są zgromadzone w jednym kawałku oprogramowania. Wiele starszych stron (szczególnie powstałych w języku PHP przy udziale osób, które nie poświęciły temu zbyt dużo czasu lub nie miały innej możliwości) jest tzw. monolitami i trudno w nich jednoznacznie wydzielić obie warstwy. Podział na "przód" i "tył" ma na celu nie tylko utworzenie dwóch osobnych aplikacji, ale też oddzielenie od siebie odpowiedzialności oraz lepszy rozwój oprogramowania poprzez przydzielenie do obu fragmentów innych zespołów programistycznych, szczególnie w większych firmach. Nierzadko słyszymy, że ktoś jest frontend developerem, backend developerem lub full stack developerem, łączącym oba światy - wynika to z faktu, że do frontendu i backendu odpowiednie są inne technologie i ktoś, kto lubi tworzyć interfejs, niekoniecznie sprawdzi się po stronie serwerowej oraz vice versa. Obie części wymagają rozwiązywania innych problemów architektonicznych oraz wiążą się z nimi inne wymagania pozafunkcjonalne.
Model View Controller (MVC)
Omawiając tę sekcję zacznijmy od jednej ważnej rzeczy - MVC nie jest architekturą. Jest to wzorzec projektowy (jeden z naprawdę wielu), który może być składnikiem projektu architektonicznego.Powiedzmy sobie, co kryje się pod tym znanym skrótem.
Kod aplikacji można pisać w jednym dużym pliku i często tak zaczynali adepci programowania wiele lat temu. Szybko jednak okazuje się, że nie jest to najefektywniejszy sposób tworzenia oprogramowania. Nie tylko dlatego, że jeden duży zbiór linijek jest trudny w obsłudze i modyfikacjach, ale także dlatego, że na pierwszy rzut oka nie widać, gdzie znajduje się część odpowiedzialna za prezentację interfejsu, gdzie jest logika, gdzie połączenie z bazą danych itd. Gdy oprogramowanie zacznie się rozwijać, coraz trudniej jest zmieniać taki kod i się w nim odnaleźć, zarówno nowej osobie, jak i nawet autorowi. Naprawa jest prosta - rozbić plik na kilka plików, a jeszcze lepiej na kilka "sekcji". Tylko jakie powinny one być?
W 1979 roku Norweg Trygve Reenskaug zaproponował koncepcję, która po małym "rebrandingu" opisuje trzy części oprogramowania:
- Model (M) - klasy przechowujące logikę aplikacji, manipulujące danymi lub stanowiące encje reprezentujące różne zbiory informacji.
- View (V) - klasy reprezentujące to, co widzi użytkownik i obsługujące komunikację z nim.
- Controller (C) - klasy przyjmujące żądanie użytkownika, walidujące je i wywołujące klasy modelu po to, aby zaktualizowały klasy widoku.
Koncepcja prosta, ale pozwaląca w logiczny sposób podzielić kod. W dodatku posiada kilka innych zalet, wśród których można wyróżnić "luźniejsze" klasy, będące bardziej podatne na rozbudowę, a także ulatwiające ponowne użycie kodu, unikając tym samym jego duplikacji. Dodatkowo, zachodzi inna nieoczywista zaleta, która związana jest z tym, w jaki sposób w praktyce powstaje oprogramowanie. Nie ukrywajmy tego, że w danej aplikacji formularze lub kontrolery często są bardzo podobne do siebie i programiści zazwyczaj nie piszą kolejnych tego typu klas od zera, tylko posiłkują się już istniejącymi fragmentami kodu. A łatwiej to robić, kiedy wszystko jest ładnie podzielone na różne pliki.
Jak to bywa z takimi koncepcjami, trochę się nią nacieszono, ale szybko przestano używać klasyki - powstały wersje z dodatkowym prezenterem (który restrukturyzuje dane pomiędzy modelem i widokiem) czy podejście MVVM, gdzie kontroler staje się View-Modelem, który obsługuje bardziej aktualizację danych na widoku zgodnie ze stanem logiki. To podejście jest widoczne np. w Angularze czy WPF (w technologii .NET). Sama warstwa M może zostać podzielona na kilka podrodzajów, gdyż mieszczą się tam formularze (ich logika), logika obliczeniowa, repozytoria (bezpośrednio manipulujące danymi w bazie danych) czy klasy DAO (ang. Data Access Object, przenoszące dane). Dodatkowo, MVC nie jest podejściem, który jest przeznaczone dla całej aplikacji - to oczywiste, że nie mamy jednej klasy modelu, jednego kontrolera i widoku, tylko jest to wiele plików. I to nawet nie w jednym folderze - takich struktur MVC-podobnych może być w oprogramowaniu wiele. O czym jeszcze będziemy dzisiaj rozmawiać.
Architektura trójwarstwa
Koncepcja MVC i architektura trójwarstwowa (ang. three-layer architecture) są czymś innym, ale nie da się ukryć, że można w nich znaleźć podobne pierwiastki. Warstwy, o których mowa, to prezentacja, logika oraz dane, a więc coś, co do złudzenia przypomina części powyżej omówionego wzorca. Jednak istnieją trzy kolosalne różnice.
Po pierwsze, komunikować się ze sobą mogą tylko dwie sąsiednie warstwy - prezentacja odnosi się do logiki, logika do danych, dane przekazują wartości logice i wreszcie logika prezentacji. Mamy w ten sposób pewien porządek i ograniczony obszar poszukiwań, co pomaga przy debugowaniu.
Po drugie, do tych warstw dokładane "z zewnątrz" mogą być inne, jak warstwa interfejsów czy core, a więc główne wspólne narzędzia wykorzystywane przez cały system. Nie bez powodu mówi się w ogólności o architekturze n-warstwowej, o czym, zresztą, jeszcze zaraz wspomnimy.
Po trzecie, z założenia każda warstwa to osobna aplikacja. A więc widać tutaj miejsce nie tylko na architekturę klient-serwer, ale też oddzielenie od siebie frontendu (warstwa prezentacji) od backendu (reszta). Oczywiście, nic nie stoi na przeszkodzie, aby przygotować przykład, który zawiera w sobie te trzy warstwy, jednak ta koncepcja architektoniczna sięga dużo dalej.
Oczywiście, w takiej czystej formie architektura trójwarstwowa to też już pewna klasyka - liczba warstw zależy od potrzeb i obecnie wyróżnilibyśmy jeszcze warstwę kontrolera, która przyjmuje żądania po stronie backendowej, a wszystko tak naprawdę zależy od aplikacji. Tym niemniej, ten tradycyjny opis pozwala zrozumieć ogólną koncepcję projektowania architektur, nawet w dzisiejszych czasach - starannie odseparowane od siebie części (często programowane przez różne osoby), utrzymanie porządku, możliwość rozbudowy w zależności od potrzeb, a nawet przygotowanie do osobnego skalowania poszczególnych warstw. Wiele frameworków wspiera ten sposób budowania i staje się on naturalny, hołdując zasadzie luźno związanych ze sobą komponentów, ale będących spójnych wewnętrznie. Oczywiście, to zależy od wielu innych czynników - równie dobrze można utworzyć aplikację zgodną z tą koncepcją, która od razu nadaje się do ponownego napisania. Tak samo należy dobierać narzędzia odpowiednie do zadania - nikt nie przygotuje trójwarstwówki (nie mylić z trójpolówką) do prostego skryptu np. usuwającego niedowiązane pliki na dysku. Tym niemniej, opisywane zagadnienie należy do tych fundamentalnych, które ułatwiają zrozumienie innych, bardziej specyficznych konstrukcji.
Architektura modułowa
Powyżej wspomnieliśmy o tym, iż aplikacja wykorzystująca koncepcję MVC nie ma zazwyczaj trzech folderów na poszczególne warstwy, w których znajdują się wszystkie pliki modelu, wszystkie pliki kontrolera itd. Może tak być, jednak nie jest to sposób pisania, który jest zalecany w dzisiejszych czasach. Zamiast tego poleca się podzielić oprogramowanie jeszcze bardziej - na moduły.
Modułem może być spójny obszar oprogramowania, który zawiera w sobie kontroler, widoki, modele i inne klasy przypisane tej części. Przykładowo, dla sklepu internetowego modułem może być produkt, koszyk, zamówienie czy dostawca. Wówczas, gdy programista chce zmodyfikować część odpowiedzialną za obliczanie wartości zamówienia, prawdopodobnie najszybciej zrobi to szukając modułu zamówienia i dopiero w nim klasy logiki. To zresztą pierwsza i najważniejsza zaleta takiego podejścia - jeszcze bardziej separuje od siebie mniejsze fragmenty oprogramowania, ułatwiając poruszanie się w ich obrębie i ogranicza wpływ zmian w jednym module na inny moduł (choć, oczywiście, to też można spektakularnie zepsuć, jak wszystko zresztą). W wielkim uproszczeniu można powiedzieć, że każdy moduł może zawierać swoje MVC, choć niekoniecznie tak to wygląda w praktyce.
Druga zaleta to jeszcze bardziej widoczny wpływ tego, o czym wspomniałem w poprzedniej sekcji - duża spójność wewnętrzna, mała zewnętrzna, sprawiająca, że odpowiednie moduły stają się trochę osobnymi wysepkami, niewpływającymi na siebie nawzajem, a jednocześnie nadal stanowiące część jednej aplikacji. Koncepcja wysp prowadzi nas zresztą do kolejnej konsekwencji - w momencie, gdy jeden moduł okazuje się ważniejszy lub bardziej obciążony niż inne, można go albo jeszcze bardziej podzielić, albo wynieść do osobnej podaplikacji, co sprawia, że moduły stają się dobrym wstępem do koncepcji mikroserwisów.
Ponownie - w bardzo prostych aplikacjach nie ma sensu dzielenie jej na moduły, gdyż zwykle nie będzie ich wiele lub staną się za bardzo ze sobą związane. Natomiast w dużym oprogramowaniu jest to rzecz niemal obowiązkowa, będąca wstępem do jeszcze większej separacji.
Podsumowanie
Oczywiście, przedstawione koncepcje nie wyczerpują wachlarza podstawowych pojęć związanych z architekturą oprogramowania. Są jednak terminami, które i tak trzeba znać, pracując jako programista, gdyż albo są wykorzystywane bezpośrednio, albo stanowią wstęp do innych, bardziej złożonych struktur. Na pewno znajomość powyżej zaprezentowanych informacji nikomu nie zaszkodzi, a może pomóc i uporządkować umysł młodego programisty, który do tej pory spotykał w artykułach fragmenty niepozwalające ubrać w całość całych pojęć. Jak najbardziej można je jeszcze rozwinąć i każdemu poświęcić osobny artykuł - dajcie znać, czy któreś z zagadnień powinno zostać rozszerzone lub czy czy w artykule nie zabrakło innej "klasyki".
Pozdrawiam i dziękuję - Jakub Rojek.