CodeGym /Kursy /Spring Core /Constructor injection w ContextFlow

Constructor injection w ContextFlow

Spring Core
Poziom 7, Lekcja 4
Dostępny

1. Warstwa serwisowa: wyrównujemy styl DI

Jeśli projekt to miasto, to warstwa serwisowa jest jak system transportowy: niby nie najpiękniejsza dzielnica, ale od niej zależy, czy „zamówienie złożone" dotrze do „audyt zapisany" i „powiadomienie wysłane". To właśnie serwisy najczęściej stają się centrum grafu obiektów, więc wszelkie dziwactwa z DI tam się mnożą na całą aplikację.

W ContextFlow serwisy to klasy, które orkiestrują scenariusze: tworzenie zamówienia, anulowanie zamówienia, generowanie raportu. Tu spływają zależności do magazynu, logiki cenowej, audytu i powiadomień. Jeśli serwisy są wstrzykiwane różnie — część przez pola, część przez konstruktory, część „a tu po cichu zrobiłem new wewnątrz metody" — projekt nabiera charakteru. Zazwyczaj nieprzyjemnego. Dlatego na koniec dnia doprowadzamy całą warstwę serwisową do jednego stylu, żeby projekt był czytelny jednakowo w każdym miejscu.

Mapa serwisów ContextFlow

Przed refaktoringiem warto zrobić prostą rzecz: nie przepisywać kodu „na wdechu", lecz sporządzić mapę — jakie serwisy mamy i jakie zależności są naprawdę obowiązkowe. Brzmi nudno, ale w praktyce oszczędza godziny, bo przestajesz zgadywać „po co w ogóle ten serwis istnieje" i zaczynasz widzieć system jako zestaw ról.

W ContextFlow role są celowo „odchudzone", żebyśmy uczyli się Springa, a nie budowali nowego Amazona. Mimo to każda rola ma zrozumiałe obowiązkowe „kolaboratory". Poniżej — robocze przekroje bieżącej warstwy serwisowej.

Klasa (serwis) Co robi znaczeniowo Obowiązkowe zależności (przez konstruktor)
OrderPlacementService tworzy zamówienie OrderIdGenerator,
OrderStore
,
OrderPricingService
,
AuditService
,
NotificationDispatchService
OrderCancellationService anuluje zamówienie OrderStore,
AuditService
,
NotificationDispatchService
OrderPricingService oblicza sumę DiscountPolicy
AuditService zapisuje audyt biznesowy AuditWriter
NotificationDispatchService wysyła powiadomienia NotificationSender
ReportingService buduje raport OrderStore,
ReportFormatter
ScenarioRunner uruchamia scenariusze OrderPlacementService,
OrderCancellationService
,
ReportingService

Tę mapę trzymamy jako roboczy przekrój dnia: nie wypisuje każdej klasy pomocniczej projektu, lecz utrwala obowiązkowe zależności serwisów. Właśnie po niej wygodnie sprawdzać, czy rola serwisu nie rozrosła się i czy gdzieś nie pojawił się ukryty new.

2. Rozgrzewka: AuditService i NotificationDispatchService

Przejście na constructor injection lepiej zaczynać od małych i oczywistych serwisów, żeby „ręka sobie przypomniała", jak to się robi, i żebyś szybko zobaczył zysk w czytelności. AuditService i NotificationDispatchService pasują idealnie: każdy ma po jednej obowiązkowej zależności, rola jest zrozumiała, a refaktoring zajmuje minuty, a daje dużo.

Najpierw utrwalamy porty — interfejsy, od których zależą serwisy. Mamy je już z poprzednich dni, ale mały fragment pomaga zachować obraz:

public interface AuditWriter {
    // Port dla infrastruktury audytu: warstwa serwisowa nie powinna wiedzieć,
    // GDZIE i JAK dokładnie jest zapisywany audyt (do konsoli, pliku, bazy, logu itp.).
    void write(String message);
}

Teraz AuditService w stylu constructor-driven. Zwróć uwagę: żadnego @Autowired nie potrzeba, bo konstruktor jest jeden, a Spring doskonale rozumie, że właśnie jego należy użyć.

import com.example.contextflow.domain.ports.AuditWriter;
import org.springframework.stereotype.Service;

@Service
public class AuditService {
    // Zależność jest obowiązkowa: bez niej serwis nie ma sensu.
    private final AuditWriter auditWriter;

    public AuditService(AuditWriter auditWriter) {
        // Konstruktor utrwala zależność raz przy tworzeniu beana.
        this.auditWriter = auditWriter;
    }

    public void audit(String message) {
        auditWriter.write(message);
    }
}

I dokładnie tak samo NotificationDispatchService. Cała „magia" nie jest w adnotacji, lecz w tym, że zależność jest wyeksponowana na zewnątrz i stała się częścią kontraktu klasy.

import com.example.contextflow.domain.ports.NotificationSender;
import org.springframework.stereotype.Service;

@Service
public class NotificationDispatchService {
    // Nadawca powiadomień — infrastrukturalna zależność.
    private final NotificationSender sender;

    public NotificationDispatchService(NotificationSender sender) {
        // Ważne: nie tworzymy sendera przez new wewnątrz serwisu.
        this.sender = sender;
    }

    public void dispatch(String message) {
        sender.send(message);
    }
}

Dalej te klasy są używane jako serwisy poziomu scenariusza: audit i dispatch, a szczegóły zapisu i wysyłki pozostają u AuditWriter i NotificationSender.

Jeśli teraz myślisz: „No to są dwa pola i konstruktor — po co taki ceremonializm?" — to normalna reakcja. Constructor injection często wygląda „zbyt prosto, żeby być ważnym". Ale trik polega na skali: gdy takich klas jest 30, jednolity styl zamienia czytanie projektu z „archeologii" w „po prostu czytanie".

3. OrderPlacementService i constructor injection

OrderPlacementService to zazwyczaj najbardziej „obciążony" zależnościami serwis: orkiestruje główny use-case, więc naprawdę potrzebuje więcej kolaboratorów. Właśnie na nim najsilniej czuć różnicę między „zależności ukryte" a „zależności widoczne". A poza tym właśnie on najczęściej zamienia się w „boga-serwis", jeśli nie pilnować granic odpowiedzialności.

Zacznijmy od konstruktora i pól final. Tak, konstruktor nie mieści się w dwóch linijkach — i to normalne: serwis naprawdę zależy od kilku rzeczy. Nienormalne stanie się później, gdy zacznie zależeć „od wszystkiego". Jeśli cennik, audyt i powiadomienie są częścią scenariusza składania zamówienia, mają miejsce w konstruktorze obok generowania id i zapisu.

import org.springframework.stereotype.Service;

@Service
public class OrderPlacementService {
    // Generator identyfikatorów zamówień — obowiązkowy kolaborator.
    private final OrderIdGenerator orderIdGenerator;

    // Magazyn zamówień — obowiązkowy kolaborator do zapisania wyniku scenariusza.
    private final OrderStore orderStore;

    // Serwis cenowy — część reguł biznesowych, też obowiązkowy.
    private final OrderPricingService pricingService;

    // Audyt i powiadomienia — równie widoczne części scenariusza, a nie ukryte efekty uboczne.
    private final AuditService auditService;
    private final NotificationDispatchService notificationDispatchService;

    public OrderPlacementService(OrderIdGenerator orderIdGenerator,
                                 OrderStore orderStore,
                                 OrderPricingService pricingService,
                                 AuditService auditService,
                                 NotificationDispatchService notificationDispatchService) {
        // Konstruktor — jedyne miejsce, gdzie obiekt jest „składany".
        this.orderIdGenerator = orderIdGenerator;
        this.orderStore = orderStore;
        this.pricingService = pricingService;
        this.auditService = auditService;
        this.notificationDispatchService = notificationDispatchService;
    }
}

Tak wygląda OrderPlacementService na bieżącym przekroju projektu: jeśli scenariusz naprawdę obejmuje obliczanie ceny, zapis, audyt i powiadomienie, wszystkie te role muszą być na górze klasy, a nie pochowane w metodach.

Żeby poczuć praktyczny sens, zobaczmy mały kawałek scenariusza. Metoda nie musi być „w pełni realistyczna biznesowo" — dla nas ważne, że wywołuje zależności, a zależności nie są tworzone wewnątrz.

public String placeOrder(CreateOrderCommand cmd) {
    // 1) Generujemy identyfikator zamówienia.
    String id = orderIdGenerator.nextId();

    // 2) Obliczamy sumę końcową przez serwis cenowy.
    var total = pricingService.calculateTotal(cmd.items());

    // 3) Zapisujemy zamówienie w magazynie.
    orderStore.save(id, cmd.customerId(), cmd.items(), total);

    // 4) Utrwalamy efekty uboczne przez już wstrzyknięte serwisy.
    auditService.audit("ORDER_PLACED: " + id);
    notificationDispatchService.dispatch("Order created: " + id);

    // 5) Zwracamy id jako wynik scenariusza.
    return id;
}

Nawet taki krótki fragment metody pokazuje rolę serwisu: orkiestruje kroki, a nie zaczyna sam liczyć rabaty, pisać do konsoli czy wybierać kanał powiadomień przez if-else.

4. OrderCancellationService bez ukrytych zależności

Anulowanie zamówienia to dobry przykład tego, dlaczego warto mieć osobny serwis na osobny scenariusz. Początkujący często próbują wpychać wszystko do jednej klasy „OrderService", bo „to przecież wszystko o zamówieniach". A potem nagle ten klasa ma dwadzieścia metod i dziesięć zależności i zaczyna przypominać szwajcarski scyzoryk, który próbuje być jeszcze łyżką.

OrderCancellationService ma zazwyczaj mniej zależności niż placement i czyta się jeszcze przyjemniej. Tu ważne, że zależności orkiestracji są widoczne od razu: potrzebujemy magazynu, audytu i powiadomień.

import org.springframework.stereotype.Service;

@Service
public class OrderCancellationService {
    // Potrzebny dostęp do stanu zamówień, żeby anulować konkretne zamówienie.
    private final OrderStore orderStore;

    // Audyt i powiadomienia — obowiązkowe części scenariusza anulowania.
    private final AuditService auditService;
    private final NotificationDispatchService notificationDispatchService;

    public OrderCancellationService(OrderStore orderStore,
                                    AuditService auditService,
                                    NotificationDispatchService notificationDispatchService) {
        // Constructor injection: wszystko obowiązkowe „na wystawie".
        this.orderStore = orderStore;
        this.auditService = auditService;
        this.notificationDispatchService = notificationDispatchService;
    }
}

I mały fragment scenariusza, żeby zobaczyć, co serwis naprawdę robi. Znowu: zero new, zero szukania beanów, tylko praca przez już wstrzyknięte zależności.

public void cancel(String orderId) {
    // 1) Zmieniamy stan zamówienia w magazynie.
    orderStore.cancel(orderId);

    // 2) Zapisujemy audyt biznesowy i wysyłamy powiadomienie.
    auditService.audit("ORDER_CANCELLED: " + orderId);
    notificationDispatchService.dispatch("Order cancelled: " + orderId);
}

Tu dobrze widać, że anulowanie zamówienia to osobny scenariusz z własnym zestawem obowiązkowych zależności, a nie przypadkowa metoda w gigantycznym OrderService.

5. ReportingService i ScenarioRunner

ReportingService i raporty

Z raportami jest typowa pułapka: wydaje się, że skoro „generujemy plik", można w serwisie otworzyć FileWriter, złożyć ciąg znaków, zapisać, zamknąć. W projekcie szkolnym to nawet działa. Ale gdy tylko zechcesz zmienić format raportu lub miejsce wyjścia, nagle okaże się, że serwis rozrósł się w infrastrukturalne detale. Właśnie dlatego robimy ReportFormatter osobną zależnością.

Wiemy już, że część infrastruktury wygodnie jest rejestrować przez @Bean. Ale dla serwisu nie ma znaczenia, jak dokładnie jest zarejestrowana zależność. Ważne dla niego, że zależność przychodzi przez konstruktor i pozostaje stabilna.

import org.springframework.stereotype.Service;

@Service
public class ReportingService {
    // Źródło danych dla raportu: serwis wie CO wziąć, ale nie wie JAK jest przechowywane.
    private final OrderStore orderStore;

    // Formater — osobna rola: zamienia dane w ciąg znaków/format raportu.
    private final ReportFormatter reportFormatter;

    public ReportingService(OrderStore orderStore, ReportFormatter reportFormatter) {
        // Wszystkie obowiązkowe zależności są przekazywane jawnie.
        this.orderStore = orderStore;
        this.reportFormatter = reportFormatter;
    }
}

A oto mini-fragment, gdzie widać interakcję: serwis bierze dane z OrderStore, przekazuje je formaterowi, dostaje ciąg znaków (lub inny wynik) i dalej już decyduje, co z tym zrobić.

public String buildDailyReport() {
    // 1) Pobieramy dane (w rzeczywistości tu może być filtrowanie po dacie itp.).
    var orders = orderStore.findAll();

    // 2) Delegujemy formatowanie, żeby nie mieszać orkiestracji biznesowej z prezentacją.
    return reportFormatter.format(orders);
}

Zwróć uwagę, jak ReportingService pozostał „serwisem", a nie „menedżerem pliku". Nawet jeśli później pojawi się bardziej złożona infrastruktura wyjścia, sama zasada się utrzyma: warstwa biznesowa prosi „wygeneruj raport", a infrastruktura decyduje „jak dokładnie".

ScenarioRunner i wiring bez new

ScenarioRunner to nasz „dyrygent", który uruchamia demonstracyjne scenariusze aplikacji. To nie jest czysto biznesowy serwis, ale jest metodycznie bardzo użyteczny: pokazuje, że można mieć jeden startowy komponent, który dostaje serwisy i wywołuje ich metody. I właśnie na ScenarioRunner łatwo zobaczyć: wiring powinien żyć albo w kontenerze, albo w konfiguracji, ale nie w środku metod biznesowych.

Najpierw sam ScenarioRunner jako zwykła klasa z konstruktorem. Można go rejestrować przez @Component, ale w naszym kursie wygodnie jest pokazać wariant przez @Bean, żebyś widział: styl wstrzyknięcia nie zależy od sposobu rejestracji.

public class ScenarioRunner {
    // Scenariusze są uruchamiane przez serwisy, a nie przez ręczne składanie zależności.
    private final OrderPlacementService placementService;
    private final OrderCancellationService cancellationService;
    private final ReportingService reportingService;

    public ScenarioRunner(OrderPlacementService placementService,
                          OrderCancellationService cancellationService,
                          ReportingService reportingService) {
        // Wstrzykujemy obowiązkowe serwisy przez konstruktor.
        this.placementService = placementService;
        this.cancellationService = cancellationService;
        this.reportingService = reportingService;
    }

    public void run() {
        // Tu zwykle będą wywołania placementService, cancellationService i reportingService.
        System.out.println("ContextFlow started");
    }
}

Teraz konfiguracja, która tworzy ScenarioRunner. Tu ważny moment: metoda @Bean może przyjmować parametry i Spring sam podstawi potrzebne beany. W efekcie konfiguracja pozostaje cienka: zero ręcznych ctx.getBean wewnątrz, zero „wewnętrznych fabryk".

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ScenarioConfig {
    @Bean
    public ScenarioRunner scenarioRunner(OrderPlacementService placementService,
                                         OrderCancellationService cancellationService,
                                         ReportingService reportingService) {
        // Spring sam przekaże potrzebne beany jako parametry metody.
        return new ScenarioRunner(placementService, cancellationService, reportingService);
    }
}

Tak wiring pozostaje na powierzchni: zależności ScenarioRunner widoczne są wprost w sygnaturze metody @Bean, a nie są wydobywane przez kontener w trakcie scenariusza.

I wreszcie — start aplikacji. To nie jest główny temat dnia, ale warto zobaczyć, że cała nasza schludna constructor-driven warstwa serwisowa doskonale startuje w zwykłym AnnotationConfigApplicationContext.

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class ContextFlowApplication {
    public static void main(String[] args) {
        // Kontekst zamykamy przez try-with-resources, żeby poprawnie zwolnić zasoby.
        try (var ctx = new AnnotationConfigApplicationContext(ContextFlowAppConfig.class)) {
            // Pobieramy runner scenariuszy z kontekstu i uruchamiamy demo-scenariusz.
            ctx.getBean(ScenarioRunner.class).run();
        }
    }
}

Jeśli teraz czujesz, że kod stał się „nudniejszy" (mniej magii, więcej jawnych konstruktorów) — gratulacje: to normalny znak, że projekt dojrzewa. Prawdziwa stabilność zazwyczaj wygląda trochę nudno. Jak dobre hamulce: nikt się nimi nie zachwyca, dopóki nie są potrzebne.

6. Czytanie grafu zależności przez konstruktory

Gdy w warstwie serwisowej masz jednolity constructor injection, pojawia się fajny efekt uboczny: możesz czytać projekt jak mapę metra. Konstruktor to „stacja przesiadkowa": przez niego widać, z czym serwis jest połączony. I nawet jeśli jeszcze nieźle znasz Springa, możesz już robić dojrzałą diagnostykę: „ten serwis nie powinien zależeć od tego komponentu".

Oto jak może wyglądać uproszczony graf zależności ContextFlow na naszym bieżącym etapie:

flowchart TD
    ScenarioRunner --> OrderPlacementService
    ScenarioRunner --> OrderCancellationService
    ScenarioRunner --> ReportingService

    OrderPlacementService --> OrderIdGenerator
    OrderPlacementService --> OrderStore
    OrderPlacementService --> OrderPricingService
    OrderPlacementService --> AuditService
    OrderPlacementService --> NotificationDispatchService

    OrderCancellationService --> OrderStore
    OrderCancellationService --> AuditService
    OrderCancellationService --> NotificationDispatchService

    ReportingService --> OrderStore
    ReportingService --> ReportFormatter

    OrderPricingService --> DiscountPolicy
    AuditService --> AuditWriter
    NotificationDispatchService --> NotificationSender

Ważne, że ten schemat „odtwarza się" z kodu prawie automatycznie. Nie potrzebujesz osobnego dokumentu. Jeśli ktoś w zespole dodał zależność, np. OrderPlacementService zaczął zależeć od ReportFormatter, zobaczysz to wprost w konstruktorze i będziesz mógł zadać pytanie: „a po co?". I to jest jeden z głównych powodów, dla których tak uparcie dzisiaj wyrównywaliśmy styl: konstruktor staje się dokumentacją, która nie starzeje się, bo kompilator jej na to nie pozwala.

7. Typowe błędy przy constructor injection

Błąd nr 1: przeniesiono serwis na konstruktor, ale zostawiono new konkretnej implementacji wewnątrz scenariusza.
Na przykład OrderPlacementService uczciwie dostaje NotificationDispatchService, a potem gdzieś w metodzie i tak robi new ConsoleNotificationSender(). W takim momencie serwis znowu zaczyna sam wybierać infrastrukturę i jednolity graf zależności się sypie. Jeśli zależność jest potrzebna serwisowi, musi przychodzić z zewnątrz, a nie rodzić się w środku use-case'u.

Błąd nr 2: wyrównano warstwę serwisową tylko w połowie.
Jeden serwis przez constructor injection, sąsiedni przez field injection, trzeci przez setter „bo lenistwo przepisywać". Na zewnątrz wygląda że wszystko działa, ale czytanie i diagnozowanie takiego projektu jest o wiele trudniejsze: część klas ma kontrakt na górze, część — rozsmarowany po polach. Sens jednolitego stylu jest właśnie w tym, żeby graf zależności czytał się jednakowo w każdym serwisie.

Błąd nr 3: ignorowanie sygnału dużego konstruktora i dalsze dorzucanie do serwisu nowych ról.
Jeśli OrderPlacementService nagle zaczyna prosić jeszcze o ReportFormatter, file writera i coś do eksportu, problem nie leży w samym konstruktorze. Problem w tym, że serwis use-case rozrósł się i potrzebuje refaktoringu odpowiedzialności, a nie makijażu adnotacyjnego. Constructor injection jest tu przydatny właśnie dlatego, że pokazuje przeciążenie uczciwie i bez maskowania.

Błąd nr 4: zamienianie ScenarioRunner lub konfiguracji w service locator.
Gdy tylko w run() lub w metodach @Bean pojawia się ctx.getBean dla pobrania „kolejnego potrzebnego serwisu", jawny wiring znika. Dla punktu wejścia kontener jest właściwy jako miejsce, skąd pobierasz gotowy ScenarioRunner, ale nie jako kostyl do ukrytego wyszukiwania zależności w trakcie scenariusza.

Błąd nr 5: wykonywanie pracy w konstruktorze zamiast po prostu składania obiektu.
Czasem chce się w konstruktorze od razu anulować zamówienie, zbudować raport lub wysłać powiadomienie „skoro zależności już są". Tak serwis zaczyna tworzyć efekty uboczne przy starcie kontekstu i zachowanie aplikacji staje się mniej przewidywalne. Konstruktor ma składać obiekt, a scenariusz ma żyć w zwykłych metodach.

1
Zadanie
Spring Core, poziom 7, lekcja 4
Niedostępne
Constructor-driven mini service layer
Constructor-driven mini service layer
1
Zadanie
Spring Core, poziom 7, lekcja 4
Niedostępne
Subtelna konfiguracja i dwa serwisy scenariuszowe
Subtelna konfiguracja i dwa serwisy scenariuszowe
1
Ankieta/quiz
Wstrzykiwanie zależności, poziom 7, lekcja 4
Niedostępny
Wstrzykiwanie zależności
Spring i konstruktory
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION