Architektura oparta na komponentach to styl architektoniczny w inżynierii oprogramowania, w którym system jest budowany jako zbiór niezależnych, samodzielnych komponentów, które komunikują się ze sobą przez dobrze zdefiniowane interfejsy. Każdy komponent odpowiada za konkretną funkcjonalność i może być rozwijany, testowany, wdrażany oraz utrzymywany niezależnie od innych.
--------------------------------------------------------------------------------------------------------------------------------
Cechy architektury komponentowej
- Modularność
system składa się z niezależnych części. - Reużywalność
komponenty mogą być używane w różnych systemach. - Separacja odpowiedzialności
każdy komponent ma jasno zdefiniowaną rolę. - Interoperacyjność
komponenty współdziałają dzięki standardowym interfejsom. - Łatwość testowania i utrzymania
możliwa wymiana lub modernizacja komponentu bez wpływu na całość. - Hermetyzacja
wewnątrz komponentu implementacja jest ukryta (enkapsulacja), a interakcja odbywa się tylko przez jasno określony interfejs. - Wymienność
można zastępować komponenty innymi, pod warunkiem że implementują ten sam interfejs.
--------------------------------------------------------------------------------------------------------------------------------
Zalety architektury opartej na komponentach
- Lepsza organizacja kodu
jasne granice i odpowiedzialności. - Łatwiejsza konserwacja i rozwój
zmiany w jednym komponencie zazwyczaj nie wpływają na inne. - Reużywalność
komponenty mogą być użyte w wielu projektach, co przyspiesza development. - Skalowalność zespołu
różne zespoły mogą pracować równolegle nad różnymi komponentami. - Testowalność
można testować komponenty indywidualnie, co ułatwia wykrywanie błędów. - Elastyczność i wymienność
możliwość podmiany komponentów bez wpływu na całość, jeśli przestrzegają interfejsów.
--------------------------------------------------------------------------------------------------------------------------------
Wady i wyzwania
- Złożoność integracji
komunikacja między komponentami musi być dobrze zaprojektowana, by unikać problemów z kompatybilnością. - Nadmiarowość
czasem projektowanie zbyt wielu komponentów na drobne funkcje może utrudnić zarządzanie. - Koszt wydajności
komunikacja między komponentami (zwłaszcza w systemach rozproszonych) może powodować opóźnienia. - Wymagania dotyczące standaryzacji
wszystkie zespoły muszą ściśle trzymać się umówionych interfejsów i kontraktów. - Trudności w debugowaniu
w przypadku rozproszenia komponentów (np. w mikrousługach) trudniej jest śledzić przepływ danych i błędy.
--------------------------------------------------------------------------------------------------------------------------------
Przykłady zastosowań, w których architektura komponentowa jest idealna:
--------------------------------------------------------------------------------------------------------------------------------
Komunikacja między komponentami
W architekturze opartej na komponentach kluczowym aspektem jest efektywna wymiana danych i sygnałów między poszczególnymi komponentami. W praktyce komunikacja ta może przebiegać na różne sposoby, w zależności od relacji między komponentami oraz skali aplikacji.
Input i Output
Najprostszy sposób przekazywania danych to użycie dekoratorów @Input() i @Output() (w Angularze), które umożliwiają komunikację pomiędzy komponentem nadrzędnym a jego dzieckiem.
@Input() służy do przekazywania danych w dół (parent → child).
@Output() wraz z EventEmitterem pozwala dziecku wysłać zdarzenie do rodzica (child → parent).
Ten mechanizm jest prosty i naturalny, ale sprawdza się głównie w drzewiastych strukturach komponentów o ściśle określonej hierarchii.
Uwaga na prop drilling to sytuacja, gdy dane lub zdarzenia muszą być przekazywane przez wiele poziomów komponentów, które same tych danych nie potrzebują, a jedynie przekazują dalej.
Serwisy jako mediatorzy
Aby ominąć prop drilling, często stosuje się serwisy, które działają jako pośrednicy komunikacji:
- Serwis przechowuje stan lub emitery zdarzeń (np. za pomocą RxJS Subject, BehaviorSubject).
- Komponenty subskrybują zmiany w serwisie i publikują do niego zdarzenia.
- Dzięki temu komponenty nie muszą się bezpośrednio znać ani przekazywać danych pośrednikom.
To podejście poprawia luźne powiązanie komponentów i ułatwia zarządzanie stanem.
Zarządzanie stanem: Redux, NgRx, Context API
W dużych aplikacjach z wieloma współzależnymi komponentami warto rozważyć użycie dedykowanych bibliotek do zarządzania stanem:
- Redux / NgRx — wzorzec oparty na centralnym store, który przechowuje globalny stan aplikacji i udostępnia go komponentom.
- Komponenty mogą czytać stan ze store i wysyłać akcje (actions), które powodują zmiany stanu za pomocą reducerów. Takie rozwiązanie zapewnia przewidywalność, ułatwia debugowanie i testowanie aplikacji.
- Context API (np. w React) — pozwala na przekazywanie danych globalnych do zagnieżdżonych komponentów bez konieczności prop drillingu, działając jak globalny kontekst dostępny w dowolnym miejscu drzewa komponentów.
--------------------------------------------------------------------------------------------------------------------------------
Przykład zastosowania w aplikacji biznesowej
Aplikacja do zarządzania zamówieniami hurtowymi (B2B OMS)
System pozwala pracownikom i klientom B2B na:
- składanie zamówień,
- zarządzanie produktami i stanami magazynowymi,
- obsługę faktur,
- śledzenie dostaw,
- raportowanie.
Rozbicie na komponenty
1. Komponent uwierzytelniania i autoryzacji
- Odpowiada za logowanie, rejestrację, SSO, prawa dostępu.
- Interfejs: AuthService.login(), AuthService.hasPermission()
- Wymienny: można go podmienić na np. Keycloak, Auth0, itp.
2. Komponent zarządzania produktami
- Przechowuje dane produktów: SKU, opisy, ceny, kategorie.
- API: ProductService.getById(), ProductService.search()
- Może działać niezależnie jako mikrousługa.
3. Komponent koszyka
- Przechowuje koszyk użytkownika (session lub persistent).
- API: CartService.addItem(), CartService.calculateTotal()
- Można zintegrować z zewnętrznymi systemami rabatowymi.
4. Komponent zarządzania zamówieniami
- Tworzenie i edycja zamówień, zmiana statusów.
- API: OrderService.placeOrder(), OrderService.getStatusHistory()
5. Komponent płatności
- Obsługuje płatności (np. integracja z PayU, Stripe, przelewy).
- API: PaymentService.charge(), PaymentService.refund()
- Hermetyzowany — można wymienić na innego dostawcę.
6. Komponent fakturowania
- Tworzy faktury VAT, noty korygujące, eksport do PDF/CSV.
- API: InvoiceService.generate(), InvoiceService.send()
7. Komponent zarządzania klientami
- Dane firmowe, NIP, adresy dostaw, kontakty.
- API: CustomerService.getCustomerByNIP(), CustomerService.update()
8. Komponent śledzenia przesyłek
- Integracja z firmami kurierskimi (np. DPD, InPost, DHL).
- API: ShippingService.trackPackage(), ShippingService.estimateDelivery()
9. Komponent raportowy
- Eksporty danych, KPI, wskaźniki sprzedaży.
- Interfejs użytkownika: widgety, wykresy (np. w Power BI lub Chart.js).
10. Komponent powiadomień
- Wysyła e-maile, SMS-y, powiadomienia push.
- API: NotificationService.sendEmail(), sendSms(), sendInApp()
11. Komponent konfiguracji / core
- Przechowuje konfigurację systemową (limity, tryby pracy).
- Może zawierać komponent loggera, cache, monitoring.
Przykładowa struktura katalogów (Angular):
--------------------------------------------------------------------------------------------------------------------------------
Najlepsze praktyki na przykładzie Angulara
Komponenty
- Zasada
pojedynczej odpowiedzialności (SRP)
Komponent powinien robić jedną rzecz i robić ją dobrze (np. wyświetlać listę produktów, a nie zarządzać logiką zakupów). - Rozdziel duże komponenty na mniejsze
Komponent, który ma więcej niż 300 linii, prawdopodobnie robi za dużo. - Komponent
= UI, nie logika biznesowa
Logikę (np. obliczenia, walidacje, operacje na danych) przenieś do serwisów. - Nazewnictwo
folderów i plików
Ułatwia odnalezienie i skalowanie struktury.
Jednolity schemat nazewnictwa sprawia, że kod jest przewidywalny — każdy programista wie, czego się spodziewać i gdzie.
Gdy projekt rośnie, czytelna struktura folderów pozwala bezproblemowo rozdzielać odpowiedzialności na zespoły i moduły. - Nie
przesyłaj całych modeli przez @Input()
Lepiej przekazywać tylko potrzebne dane, np. @Input() productName: string
Komponent potomny powinien wiedzieć tylko to, co naprawdę musi wiedzieć. Gdy podajesz cały obiekt, otwierasz mu dostęp do całej struktury — nawet tej, której nie potrzebuje. To łamanie zasady pojedynczej odpowiedzialności.Gdy przekazujesz cały model, komponent potomny staje się ściśle powiązany z jego strukturą. Jeśli model się zmieni (np. zmiana
Dobrze zdefiniowaneProduct
), musisz aktualizować też komponenty dzieci.
Przy podawaniu tylko potrzebnych danych — ten problem znika.@Input()
działają jak publiczne API komponentu.
Każdy, kto używa komponentu, od razu widzi, czego się spodziewać.
Przy testach jednostkowych dużo łatwiej jest przekazać kilka prostych wartości niż konstruować cały obiekt domenowy (Product
,User
,Order
itd.). - Unikaj
logiki w szablonach (HTML)
Szablon to nie miejsce na przetwarzanie danych. To miejsce na ich prezentację.
Nie używaj *ngIf="getTotalPrice()". Przenieś to do komponentu i przekaż przez zmienną.
Angular wywołuje getTotalPrice() za każdym razem, gdy sprawdza zmiany (a to może być często).
Jeśli ta funkcja coś liczy, iteruje po tablicy itp. — pogarsza to wydajność.
Jeśli zmienia stan (np. przez przypadek), może spowodować błąd logiki lub nawet infinite loop.
Szablon powinien być prosty i czytelny.
Logika powinna być trzymana w komponencie (.ts), czyli w warstwie kontrolera, nie w widoku.
Poprawna implementacja:
Jeśli dane się zmieniają możesz zaktualizować wartość w metodzie ngOnChanges() albo użyć RxJS (np. BehaviorSubject, combineLatest, async pipe) i przypisać wynik do zmiennej. - Używaj
dedykowanych komponentów do dialogów, modalek, itp.
Zamiast wrzucać kod dialogu, modala czy okienka bezpośrednio do dużego komponentu strony (np. OrdersPageComponent), stwórz osobny, mały komponent dedykowany tylko do tej funkcji.
Dedykowane komponenty dialogów i modalek to czystszy, bardziej modularny kod, który łatwiej rozwijać, testować i ponownie wykorzystywać.
Trzymanie logiki modali w dużym komponencie strony powoduje chaos i utrudnia skalowanie aplikacji. - Każdy komponent powinien być samowystarczalny – mieć własny CSS, HTML, TS.
Samowystarczalność komponentu to podstawa czystej i skalowalnej architektury frontendowej.
Oddzielne pliki HTML, CSS i TS pozwalają utrzymać porządek, ułatwiają rozwój i poprawiają jakość kodu. - Shared/components
Jeśli komponent jest używany w wielu miejscach, przenieś go do shared/components.
Przykład komponentów w shared/components:
Przyciski (np. app-button)
Komponenty formularzy (np. app-input, app-select)
Modal/dialog
Kafelki informacyjne
Ikony, avatary
Małe widgety, np. licznik powiadomień
--------------------------------------------------------------------------------------------------------------------------------
Moduły i struktura projektu
- Feature Modules + Lazy Loading = Skalowalność
Każdy moduł funkcjonalny (feature module) powinien być lazy-loaded, aby zapewnić szybkie ładowanie aplikacji, lepszą organizację i łatwiejsze skalowanie.
Aplikacja ładuje się szybciej, bo nie musi wczytywać całego kodu na start. - Nie wrzucaj wszystkiego do AppModule
Rozdzielaj odpowiedzialności: CoreModule, SharedModule, FeatureModule. - SharedModule zawiera tylko współdzielone elementy
SharedModule ma być zbiorem statycznych, niezależnych od stanu elementów UI, które nie mają wpływu na logikę aplikacji.
SharedModule powinien zawierać tylko te elementy, które są czysto wizualne i wielokrotnie używane w różnych miejscach aplikacji — czyli:
- komponenty UI (np. przyciski, karty, nagłówki),
- pipe’y (np. formatowanie dat, tekstu),
- dyrektywy.
Dzięki temu unikamy plątania się logiki biznesowej z wizualnymi komponentami.
Nie powinno się umieszczać w SharedModule serwisów ani rzeczy, które zależą od kontekstu konkretnej funkcjonalności lub modułu.
Kiedy serwis jest zadeklarowany w SharedModule, a ten moduł jest importowany przez wiele innych modułów (np. różne FeatureModules), Angular tworzy nową instancję tego serwisu za każdym razem, gdy moduł jest importowany.
Serwisy i inne zależności kontekstowe powinny trafić do CoreModule lub FeatureModules, aby uniknąć problemów z wielokrotnym ładowaniem i niejasnym zarządzaniem stanem. - CoreModule ładowany raz (singletony)
W Angularze CoreModule to specjalny moduł, który tworzysz po to, by przechowywać globalne, współdzielone zasoby, takie jak:
- serwisy (np. AuthService, LoggerService, UserService),
- guardy (np. AuthGuard, AdminGuard),
- interceptory HTTP (np. dodawanie tokenu JWT do zapytań),
Taki moduł importujesz tylko raz – w AppModule. Dzięki temu Angular tworzy tylko jedną instancję (singleton) każdego z tych elementów. - Nie eksportuj wszystkiego z każdego modułu
Eksportuj tylko to, co powinno być używane gdzie indziej (czyli API modułu). - Trzymaj routing danego feature’a w osobnym pliku
(feature-routing.module.ts)
W Angularze, gdy tworzysz moduł funkcjonalny (feature module), np. ProductsModule, możesz dodać do niego trasowanie (routing) — czyli definicję ścieżek, komponentów i ewentualnych guardów.
Zamiast trzymać te ścieżki bezpośrednio w products.module.ts, lepiej stworzyć osobny plik:
products-routing.module.ts
I tam skonfigurować wszystkie ścieżki związane z tym featurem.
I nie musisz się martwić, że routing miesza się z logiką aplikacji głównej.
Trzymanie routingów w osobnych plikach ułatwia lazy loading, czyli ładowanie modułów dopiero wtedy, gdy są potrzebne.
--------------------------------------------------------------------------------------------------------------------------------
Serwisy i logika
- Serwisy
są miejscem na logikę biznesową i operacje z API
Komponent powinien tylko inicjować akcje i reagować, nie wie jak działa logika – tym zajmuje się serwis.
Komponent = widok i reakcje użytkownika
Serwis = logika, dane, API i przetwarzanie
Co może robić serwis? - Komunikacja z API (HTTP)
- Logika biznesowa: walidacja, filtrowanie, transformacje danych
- Obsługa lokalnego stanu (np. BehaviorSubject)
- Buforowanie danych
- Łączenie i synchronizacja różnych źródeł danych
- Obsługa błędów
- Obsługuj
błędy w serwisach, nie w komponentach
Zamiast pisać kod obsługujący błędy (np. catchError) w komponentach, lepiej zrobić to w serwisach czyli tam, gdzie znajduje się logika i komunikacja z API.
--------------------------------------------------------------------------------------------------------------------------------
Narzędzia wspierające architekturę opartą na komponentach
Wdrożenie architektury opartej na komponentach jest dziś znacznie prostsze dzięki rozwojowi nowoczesnych frameworków i bibliotek frontendowych. Wśród najpopularniejszych narzędzi, które wspierają ten sposób budowy aplikacji, warto wymienić:
-
Angular – kompleksowy framework, który od podstaw projektuje aplikacje w oparciu o komponenty i moduły. Angular oferuje wbudowany system routingu, dependency injection oraz mechanizmy zarządzania stanem, co ułatwia tworzenie skalowalnych rozwiązań biznesowych.
-
React – biblioteka skupiona na budowie interfejsów użytkownika poprzez komponenty. React pozwala na tworzenie wielokrotnego użytku, izolowanych komponentów, które można łatwo łączyć i zarządzać ich stanem za pomocą hooków i kontekstów.
-
Vue.js – lekki i elastyczny framework, który umożliwia budowę aplikacji krok po kroku, zaczynając od prostych komponentów, a następnie rozwijając je do pełnych aplikacji.
Ważnym aspektem w dużych aplikacjach biznesowych jest zarządzanie stanem – synchronizacja danych i komunikacja pomiędzy komponentami. Tutaj sprawdzają się narzędzia takie jak:
-
NgRx – biblioteka dla Angulara bazująca na wzorcu Redux, zapewniająca scentralizowane i przewidywalne zarządzanie stanem aplikacji.
-
Redux – popularna biblioteka dla Reacta i innych frameworków, pozwalająca utrzymać spójność i kontrolę nad danymi aplikacji.
-
MobX – alternatywa dla Redux, która automatycznie reaguje na zmiany danych, upraszczając kod i logikę aplikacji.
Ponadto, przy rozwijaniu i testowaniu komponentów bardzo pomocne są narzędzia takie jak:
-
Storybook – środowisko do tworzenia i dokumentowania komponentów UI w izolacji, które ułatwia współpracę w zespole i przyspiesza rozwój.
-
Angular CLI, Create React App, Vite – narzędzia wspomagające szybkie tworzenie, budowanie i optymalizację aplikacji komponentowych.
Warto zatem korzystać z tych technologii, aby w pełni wykorzystać potencjał architektury opartej na komponentach i tworzyć aplikacje łatwe w rozwoju, utrzymaniu i skalowaniu.
--------------------------------------------------------------------------------------------------------------------------------
Porównanie do innych podejść
CBA to taki złoty środek między monolitem a totalną mikrousługą. Komponenty są niezależne, ale współpracują. A to daje mega elastyczność.
--------------------------------------------------------------------------------------------------------------------------------
Komponent jako paczka
W dojrzałych projektach często idziemy krok dalej: pakujemy komponenty jako paczki:
-
@my-lib/card
-
@my-lib/button
-
@shared/notifications
To mogą być paczki npm, .jar
w Javie albo cokolwiek innego. Dzięki temu łatwiej je testować, aktualizować i współdzielić między projektami.
--------------------------------------------------------------------------------------------------------------------------------
Backend w architekturze komponentowej
Architektura komponentowa na backendzie jest mniej popularna jako „buzzword”, ale... jak najbardziej istnieje i działa świetnie. Po prostu nazywa się to często inaczej – np. jako modułowa architektura, DDD, pluginy, albo komponentowe frameworki
Struktura katalogów (Component-Based Backend w .NET)
Infrastructure to warstwa techniczna w architekturze aplikacji, której głównym zadaniem jest obsługa niskopoziomowych szczegółów implementacyjnych i integracji z zewnętrznymi systemami.
Warstwa Infrastructure zawiera wszystko, co pozwala na techniczne "podpięcie" aplikacji do rzeczywistego środowiska, hardware’u i usług — ale bez logiki biznesowej i domenowej. Jest to więc implementacja technologiczna, którą można wymienić lub zmodyfikować bez wpływu na domenę.