piątek, 25 lipca 2025

Architektura oparta na komponentach (Component-Based Architecture)

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 Product), musisz aktualizować też komponenty dzieci.
    Przy podawaniu tylko potrzebnych danych — ten problem znika.

    Dobrze zdefiniowane @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 m
    a 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ę.