CodeGym /Kursy /Spring Core /Pierwszy projekt

Pierwszy projekt

Spring Core
Poziom 1, Lekcja 1
Dostępny

1. Potrzebny jest uczciwy baseline

Jeśli po rozmowie o Spring Core od razu przejść do kontenera, łatwo odnieść wrażenie, że framework rozwiązuje jakiś abstrakcyjny, wręcz akademicki problem. Żeby tak się nie stało, potrzebny jest uczciwy baseline: mała aplikacja bez Springa, gdzie widać gołym okiem, kto tworzy obiekty, kto co dostaje w konstruktorze i gdzie w ogóle żyje kod uruchomienia 🛕.

To ważny punkt metodyczny 🗿. Spring lepiej tłumaczyć nie przez frazę „kontener jest wygodny", lecz przez kontrast. Dopóki nie zobaczysz zwykłego kodu Java, który trzeba składać ręcznie, słowa takie jak wiring, object graph i ApplicationContext brzmią zbyt abstrakcyjnie. A gdy tylko zobaczysz wersję plain Java, staje się zrozumiałe, dlaczego temat budowania aplikacji zasługuje na osobną rozmowę inżynierską 🔧.

Dlatego pojawia się u nas przekrojowy projekt kursu — ContextFlow: Order Processing, Notifications & Audit. Nie jako „duży produkt", lecz jako poligon szkoleniowy. Nie budujemy sklepu internetowego na sto encji, nie wchodzimy w web, nie podłączamy bazy i nie udajemy, że pierwszego dnia potrzebujemy świat na poziomie produkcji. Potrzebujemy scenariusza, w którym naturalnie istnieje kilka powiązanych obiektów i kilka efektów ubocznych. To już wystarczy, żeby pokazać, skąd bierze się ból 💣.

2. Pierwszy kawałek przyszłego projektu

Dzisiejszy przekrój ContextFlow jest celowo niewielki. Jest scenariusz „utwórz zamówienie". Żeby zadziałał, trzeba przynajmniej zapisać zamówienie, wysłać powiadomienie i zapisać audyt. Już w tej skali pojawiają się różne role: ktoś odpowiada za przechowywanie, ktoś — za powiadomienie, ktoś — za rejestrację działania. A więc pojawiają się zależności, a wraz z nimi — pytanie o budowanie.

Taka domena jest dobra z dwóch powodów. Po pierwsze, jest zrozumiała: zamówienie, powiadomienie i audyt nie wymagają osobnej lektury o dziedzinie. Po drugie, rośnie naturalnie. Wystarczy chcieć anulowania zamówienia, raportu lub innego kanału powiadomień — i graf obiektów zacznie się komplikować bez żadnej sztucznej dramaturgii. To znaczy sama tema kursu będzie się ujawniać przez naturalny wzrost aplikacji, a nie przez specjalnie wymyśloną złożoność.

Scenariusz można przedstawić bardzo prosto:

sequenceDiagram
    participant R as ScenarioRunner
    participant S as OrderPlacementService
    participant St as OrderStore
    participant N as NotificationSender
    participant A as AuditWriter

    R->>S: placeOrder(command)
    S->>St: save(order)
    S->>N: send("order created")
    S->>A: write("order created")
    S-->>R: orderId

Nie ma tu jeszcze nic o Springu 🪄 — i właśnie o to chodzi. Jest zwykły serwis use-case, kilka zależnych obiektów i punkt uruchomienia, który musi to wszystko złożyć. Jeśli dalej trzymać w głowie właśnie ten przekrój, wszystkie kolejne rozmowy o kontenerze będą odbierane nie jako abstrakcja, lecz jako rozwinięcie konkretnej sytuacji.

3. Wersja plain Java projektu

Przyjrzyjmy się najzwyklejszemu startowi aplikacji. Jest main, jest kilka zależności, jest serwis scenariusza i jest ScenarioRunner, który ten scenariusz uruchamia. Na razie wszystko składane ręcznie, bez żadnej magii i bez Springa.

public class ContextFlowApp {
    public static void main(String[] args) {
        // Ręcznie tworzymy zależności infrastrukturalne (wybór konkretnych implementacji).
        OrderStore store = new InMemoryOrderStore(); // Magazyn zamówień: na razie in-memory
        NotificationSender sender = new ConsoleNotificationSender(); // Kanał powiadomień: na razie konsola
        AuditWriter audit = new ConsoleAuditWriter(); // Audyt: też piszemy do konsoli

        // Ręcznie składamy serwis use-case: jawnie przekazujemy zależności przez konstruktor.
        OrderPlacementService placement = new OrderPlacementService(store, sender, audit);

        // Ręcznie tworzymy punkt uruchomienia scenariusza (bootstrap): ktoś musi „odpalić" use-case.
        ScenarioRunner runner = new ScenarioRunner(placement);
        runner.run(); // Uruchamiamy scenariusz (np. „utwórz zamówienie")
    }
}

Taki kod ma ważną zaletę: jest bardzo uczciwy. Nic nie jest ukryte. Od razu widać, że OrderPlacementService nie może działać bez store, sender i audit. Dla tekstu szkoleniowego jest to szczególnie wartościowe, bo student widzi prawdziwą formę zależności, a nie domyśla się jej z opisu.

Sam serwis też wygląda jak zwykła klasa Java. Nic specjalnego, niepowiązana z frameworkiem i nieoznaczona magią. Używa po prostu tego, co jej przekazano.

public class OrderPlacementService {
    // Jawne zależności serwisu: bez nich use-case nie może się wykonać.
    private final OrderStore store; // odpowiada za zapisanie zamówienia
    private final NotificationSender sender; // odpowiada za wysłanie powiadomienia
    private final AuditWriter audit; // odpowiada za zapis audytu

    public OrderPlacementService(
            OrderStore store,
            NotificationSender sender,
            AuditWriter audit
    ) {
        // Zapisujemy zależności — dalej serwis będzie ich używał w metodach biznesowych.
        this.store = store;
        this.sender = sender;
        this.audit = audit;
    }

    public String placeOrder(CreateOrderCommand command) {
        // Generujemy identyfikator zamówienia (uproszczony, tylko dla przykładu).
        String orderId = "ORD-" + System.currentTimeMillis();

        // Zapisujemy obiekt domenowy do magazynu.
        store.save(new Order(orderId, command.customerName()));

        // Efekt uboczny №1: wysyłamy powiadomienie.
        sender.send("Order created: " + orderId);

        // Efekt uboczny №2: piszemy do audytu.
        audit.write("Created order: " + orderId);

        // Zwracamy identyfikator — wynik scenariusza.
        return orderId;
    }
}

Na razie czyta się to spokojnie 🤯. Nawet przyjemnie. Klasa wygląda normalnie, zależności są jawne, kod uruchomienia krótki. W tej skali projektu ręczne składanie nie wydaje się problemem — i w tym jest uczciwość pierwszego poziomu. Spring nie trzeba ogłaszać zbawicielem ludzkości z powodu jednego konstruktora. Ale forma przyszłego wzrostu jest już widoczna: jeden serwis ma trzy zależności, i ktoś musi je wybrać, stworzyć i powiązać 🔗.

4. Graf obiektów po ludzku

Gdy ktoś mówi „graf obiektów", nowicjusz łatwo słyszy w tym coś matematycznie strasznego 😅. W rzeczywistości to coś znacznie bardziej codziennego. To po prostu mapa tego, jakie obiekty istnieją w aplikacji i kto na kim się opiera. W naszym przykładzie graf jest na razie mały, ale już istnieje: ContextFlowApp tworzy OrderPlacementService, a ten zależy od OrderStore, NotificationSender i AuditWriter.

flowchart TD
    App[ContextFlowApp.main] --> Runner[ScenarioRunner]
    App --> Placement[OrderPlacementService]
    Placement --> Store[OrderStore]
    Placement --> Notify[NotificationSender]
    Placement --> Audit[AuditWriter]

Gdy graf jest mały, zarządza się nim wzrokowo 👀. Ale właśnie tu rodzi się ważne pytanie: kto odpowiada za reguły tego składania? Kto zdecydował, że NotificationSender będzie konsolowy? Kto zdecydował, że AuditWriter też jest konsolowy? Kto trzyma w głowie, który obiekt jest wspólny, a który można spokojnie tworzyć oddzielnie? Te decyzje już istnieją, tylko jest ich na razie niewiele.

Jeszcze jedno przydatne słowo tutaj — bootstrap. To kod startu aplikacji, miejsce, gdzie składa się ona sama przed wykonaniem pierwszego scenariusza. W prostym świecie Java bootstrap bardzo często żyje bezpośrednio w main. I to jest normalne, gdy aplikacja jest mała. Ale gdy w bootstrap zaczynają spływać szczegóły z różnych zakątków systemu, przestaje być „kilkoma linijkami uruchomienia" i staje się osobną strefą troski.

5. Kod startu rośnie szybciej niż się wydaje

Najzdradliwszą częścią ręcznego łączenia obiektów (wiring) nie jest liczba new. Zdrada leży gdzie indziej: kod uruchomienia zaczyna wiedzieć zbyt wiele o wewnętrznej budowie aplikacji. Powiedzmy, że dodałeś drugi scenariusz — anulowanie zamówienia. Teraz potrzebny jest kolejny serwis, i też wymaga zależności.

// Nowy serwis use-case: anulowanie zamówienia (używa już stworzonych zależności).
OrderCancellationService cancellation = new OrderCancellationService(store, audit); // ważne: ten sam store/audit

// Kolejny scenariusz: raporty (potrzebuje tylko store).
ReportingService reporting = new ReportingService(store); // znowu ważne: ten sam store, żeby widzieć te same dane

// Punkt uruchomienia teraz zna kilka scenariuszy i musi to wszystko połączyć.
ScenarioRunner runner = new ScenarioRunner(placement, cancellation, reporting);

W tym momencie main już wie, jakich zależności potrzebuje placement, jakich potrzebuje cancellation, które obiekty muszą być wspólne, a które nie. I właśnie tu zaczyna się ciekawa inżynierska rozwidlenie 🤔. Na poziomie jednej nowej linijki wydaje się, że nic strasznego się nie stało. Ale na poziomie architektury kod uruchomienia stopniowo zamienia się w miejsce, gdzie skupia się zbyt wiele wiedzy o aplikacji.

Jest jeszcze bardziej subtelny moment 👀. Niektóre obiekty nie wystarczy tylko stworzyć — trzeba je stworzyć właściwie względem siebie nawzajem. Na przykład jeśli OrderPlacementService zapisuje zamówienie w jednym InMemoryOrderStore, a OrderCancellationService szuka go już w innym, aplikacja będzie działać, ale niepoprawnie. Czyli ręczny wiring rodzi nie tylko długi bootstrap, ale i nową klasę błędów: błędów budowania grafu, które nie zawsze ujawniają się jawnym wyjątkiem 🧨.

Z tego powodu wzrost ręcznego wiring jest zwodniczy. Z zewnątrz wydaje się, że po prostu dodajesz kolejny serwis. Ale wewnątrz pojawiają się nowe reguły uzgadniania: które obiekty muszą być wspólne, gdzie jest punkt wyboru implementacji, który obiekt powinien żyć przez cały runtime aplikacji, a który można tworzyć na każde wywołanie. I właśnie ta niewidzialna część rośnie potem szybciej niż same metody biznesowe.

6. Ręczne wiązanie nie jest przestępstwem, ale ma swój sufit

W tym miejscu ważne jest, żeby nie wyciągnąć błędnego wniosku: „znaczy, ręczne składanie jest zawsze złe" 🤬. Nie. Dla małego narzędzia, krótkiego skryptu jednorazowego lub bardzo prostej aplikacji ręczny wiring może być zupełnie adekwatnym wyborem. I uczciwy kurs o Springu powinien to powiedzieć. Framework nie jest potrzebny tam, gdzie problem jeszcze nie powstał.

Ale ręczny wiring ma całkiem obserwowalny sufit. Uderza nie w liczbę linii, lecz w liczbę decyzji o składaniu, które trzeba trzymać w głowie. Gdy jest jedna zależność i jeden scenariusz, wszystko jeszcze do zniesienia. Gdy scenariuszy jest kilka, implementacji robi się więcej niż jedna, a zachowanie aplikacji zaczyna zależeć od konfiguracji lub trybu uruchomienia, składanie staje się osobnym zadaniem.

Można to zobaczyć w bardzo kompaktowym kontraście:

Jeszcze spokojnie Zaczyna się napięcie inżynierskie
jeden serwis use-case kilka scenariuszy ze wspólnymi obiektami
jedna implementacja zależności kilka implementacji jednej roli
main zna parę new bootstrap zna wewnętrzną strukturę połowy aplikacji
błędy zwykle widać od razu część problemów to błędy samego wiringu

Właśnie tu pojawia się dojrzała myśl 😱. Okazuje się, że aplikacja ma nie tylko logikę biznesową, lecz i logikę własnego składania. Dopóki programista trzyma tę logikę ręcznie, żyje ona albo w main, albo w ręcznie robionych fabrykach, albo rozmywa się po kodzie. Spring nie pojawia się tam, gdzie leniwo pisać new, lecz tam, gdzie składanie aplikacji stało się już osobnym zadaniem inżynierskim 🔧.

7. Potrzebujemy ContextFlow

Projekt przekrojowy jest potrzebny nie dla ładnej nazwy. Jest potrzebny po to, żeby wszystkie kolejne tematy opierały się na jednym i tym samym kontekście dziedzinowym. Jeśli dziś tłumaczyć ręczny wiring na zamówieniach, jutro lifecycle — na bibliotece książek, a pojutrze events — na przykładzie bankowym, student traci siły nie na mechanikę Springa, lecz na ciągłą zmianę dekoracji.

ContextFlow jest dobry tym, że pozwala rosnąć naturalnie. Dziś wystarczyło nam OrderPlacementService, OrderStore, NotificationSender, AuditWriter i ScenarioRunner. Trochę dalej projekt spokojnie udźwignie kilka implementacji powiadomień, konfigurację trybów, zasoby z szablonami, application events, post-processors i nawet zachowanie oparte na proxy. Przy tym domena pozostanie na tyle prosta, żeby nie zasłonić tematu kursu.

Właśnie dlatego dziś nie wyrzucamy całego katalogu encji, serwisów i pakietów. Dla pierwszego dnia byłoby to nie plusem, lecz przeciążeniem. Na razie wystarczy zobaczyć jeden żywy łańcuch biznesowy i zauważyć, jak szybko wokół niego pojawia się pytanie o składanie. Następny naturalny krok dotyczy już nie samej domeny, lecz jakości tego kodu plain Java: czy może działać, będąc jednocześnie architektonicznie niewygodnym? Odpowiedź, niestety, tak 😐. I właśnie tu zaczyna się silne powiązanie, ukryte zależności i cena zmian.

8. Typowe błędy 👺

Błąd №1: od razu i na zawsze uznać ręczny wiring za zło.
Taki wniosek jest zbyt gruby. Na bardzo małym kodzie ręczne składanie może być normalne. Ważne nie jest zakaz new, lecz zrozumienie, gdzie liczba decyzji o składaniu staje się samodzielnym problemem.

Błąd №2: mierzyć wzrost złożoności tylko liczbą linii.
Czasem main jest wciąż krótki, ale wie już zbyt wiele o strukturze aplikacji. Problem ręcznego składania częściej ujawnia się jako wzrost powiązań i reguł uzgadniania, a nie po prostu jako długa metoda.

Błąd №3: zbyt wcześnie rozdymać domenę.
Jeśli pierwszego dnia wciągnąć płatności, dostawę, użytkowników, statusy, rabaty i integracje, temat kursu rozpłynie się w dziedzinie. Potrzebujemy nie „całego biznesu naraz", lecz właściwego przekroju, na którym dobrze widać problem składania.

Błąd №4: myśleć, że ból zaczyna się dopiero gdy aplikacja padnie.
Jedna z najbardziej nieprzyjemnych cech ręcznego składania polega na tym, że część błędów nie powala programu od razu. Po prostu sprawia, że jest on trudny do zmiany, czytania i lokalnej weryfikacji. A to często groźniejsze niż jawne padnięcie 👍.

1
Zadanie
Spring Core, poziom 1, lekcja 1
Niedostępne
Serwis z trzema jawnymi zależnościami
Serwis z trzema jawnymi zależnościami
1
Zadanie
Spring Core, poziom 1, lekcja 1
Niedostępne
Kilka scenariuszy i wspólny kod uruchomienia
Kilka scenariuszy i wspólny kod uruchomienia
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION