1. Konfiguracja w ReadLater Starter
Jeśli pisałeś tylko szkolne programy konsolowe, bardzo łatwo uwierzyć w naiwną magię: „przecież raz wpisałem String baseUrl = "...", więc tak już zostanie na zawsze”. Ale aplikacje backendowe żyją w rzeczywistości, w której tę samą aplikację trzeba uruchamiać w różnych warunkach: u siebie w domu, u kolegi z zespołu, na innym komputerze, z Internetem albo bez, na innym porcie, w trybie mock, bo zewnętrzny serwis „padł”. I to wszystko musi się rozstrzygać bez zmiany kodu źródłowego, inaczej każde uruchomienie zamienia się w mini-remont.
Zacznijmy od szczerego przyznania: nasz ReadLater Starter już teraz nie żyje w jednym trybie. Ma fazę klienta z komendami catalog search i catalog details, a wkrótce pojawi się faza serwera (server). Oznacza to, że od razu mamy parametry, które nieuchronnie różnią się od uruchomienia do uruchomienia: adres zewnętrznego API, timeouty, tryb real/mock, port serwera. I tu hardcoding zaczyna się zachowywać jak ten kolega, który „na pewno przyjdzie na czas” — ale tylko jeśli zawsze spotykacie się w tym samym miejscu, tego samego dnia i nigdy nie zmieniacie planów.
Dobra robocza zasada na dziś brzmi tak: kod odpowiada na pytanie „co robi aplikacja”, a konfiguracja odpowiada na pytanie „z jakimi wartościami to robi”. Na przykład „wysyła zapytanie HTTP do katalogu” — to logika. A „dokąd dokładnie wysyła zapytanie” — to konfiguracja.
Konfiguracja to oddzielna odpowiedzialność
Konfigurację najłatwiej zrozumieć przez analogię z przepisem. Przepis mówi, co robić: wymieszać, upiec, poczekać. Ale nie powinien być zaszyty w „jedyne możliwe składniki na planecie Ziemia”. Dziś masz mleko 2,5%, jutro 3,2%, i to nie jest powód, żeby przepisywać przepis i przebudowywać kuchnię od zera. W aplikacji jest tak samo: logika powinna pozostać stabilna, a parametry — zmieniać się bez rekompilacji.
W świecie backendu konfiguracja to zestaw wartości, które mogą się różnić między środowiskami (u różnych deweloperów, na różnych maszynach, w różnych warunkach sieciowych) lub między uruchomieniami (dziś mock, jutro real). Przy tym konfiguracja zwykle nie powinna trafiać do modelu domenowego i nie powinna rozłazić się po całym kodzie. To oddzielna odpowiedzialność: wygodnie trzymać ją „obok punktu wejścia” i przekazywać dalej do pozostałych części aplikacji już w zrozumiałej formie.
W tym samym miejscu pojawia się jeszcze jedna ważna myśl: konfiguracja to nie „wszystko, czego mi się nie chce wpisywać”. Jest pokusa, żeby konfigiem zrobić każdą zmienną, i wtedy aplikacja zamieni się w kasyno ustawień. Będziemy się trzymać zdrowego rozsądku: wynosić na zewnątrz tylko to, co naprawdę powinno się zmieniać bez ruszania kodu Javy.
2. Konfiguracja w ReadLater Starter
Teraz ostrożnie ustalimy konkretnie dla naszego projektu: co jest konfiguracją, a co danymi uruchomienia (argumentami) lub częścią logiki. Żeby nie zamienić tego w listę „na pamięć”, ujmijmy to w tabelę: tak prościej zobaczyć granicę.
| Kategoria | Przykład | Dlaczego to właśnie to |
|---|---|---|
| Konfiguracja aplikacji | app.name=readlater | Nazwa nie wpływa na logikę biznesową, ale może być różna w różnych środowiskach i logach |
| Konfiguracja klienta katalogu | catalog.api.base-url=https://openlibrary.org | Adres zewnętrznego serwisu może się różnić (real/mock, inny dostawca, inny stand) |
| Konfiguracja klienta katalogu | catalog.api.request-timeout-ms=2000 | Timeouty zależą od środowiska i sieci, a nie od „logiki wyszukiwania książki” |
| Konfiguracja klienta katalogu | catalog.api.mode=real | To przełącznik zachowania bez zmiany kodu (szczególnie przydatny do nauki) |
| Konfiguracja server-mode | server.host=localhost, server.port=8080 | Port może być zajęty, host może być inny, a kod serwera powinien pozostać ten sam |
| Dane konkretnego uruchomienia (args) | catalog search clean code | To jednorazowa komenda: dziś szukamy „clean code”, jutro — „java concurrency” |
| Dane konkretnego uruchomienia (args) | catalog details OL12345M | To jednorazowy identyfikator książki dla konkretnego wywołania |
| Część logiki aplikacyjnej | „jak wyszukiwać książki”, „jak normalizować DTO” | To zachowanie aplikacji, które nie powinno się zmieniać w zależności od środowiska |
Zwróć uwagę na subtelny, ale ważny moment. Tryb server to nie konfiguracja, to komenda. A server.port to już konfiguracja dla tej komendy. Tak samo „wyszukać książkę” — to komenda, a catalog.api.base-url — konfiguracja, z którą komenda pracuje.
Jeśli teraz nie czujesz różnicy, to normalne. Zwykle zaczyna się ją czuć, gdy któregoś dnia zmieniasz port z 8080 na 8081 i nagle robisz commit do Git „bo inaczej nie startuje”. I właśnie wtedy pojawia się pytanie: a dlaczego w ogóle zmiana portu wymagała zmiany kodu źródłowego?
3. Hardcoding w main(): szybki start, powolne życie
Czasem hardcoding wygląda niewinnie. Zwłaszcza na początku kursu. Wydaje się nawet „uczciwy”: oto wartość, oto kod, wszystko obok siebie. Problem w tym, że projekt backendowy to rzecz długowieczna, a ta „niewinność” szybko się kończy. Będziesz chciał przełączyć real/mock, zwiększyć timeout, podnieść serwer na innym porcie — i nagle okaże się, że każde takie życzenie wymaga: otworzyć kod źródłowy, zmienić linijkę, przekompilować, sprawdzić, czy nic nie popsułeś.
Spójrzmy na „typowy startowy hardcoding”, który w małych projektach pojawia się niemal automatycznie:
public class ReadLaterApplication {
public static void main(String[] args) {
// Wartości konfiguracyjne środowiska (dziś jedne, jutro inne)
// Problem: są "zaszyte" w kodzie źródłowym, czyli wymagają zmiany kodu i rekompilacji.
String baseUrl = "https://openlibrary.org"; // adres zewnętrznego API (może być real/mock lub inny stand)
int requestTimeoutMs = 2000; // timeouty sieciowe zależą od sieci i środowiska
String apiMode = "real"; // przełącznik zachowania: "real" lub "mock"
int serverPort = 8080; // port może być zajęty na konkretnej maszynie
// Dla demonstracji: wypisujemy bieżące wartości, z którymi aplikacja wystartowała
System.out.println("baseUrl = " + baseUrl); // baseUrl = https://openlibrary.org
System.out.println("requestTimeoutMs = " + requestTimeoutMs); // requestTimeoutMs = 2000
}
}
Na poziomie „uruchamiam sam u siebie” to działa. Ale teraz wyobraź sobie rzeczywistość: jesteś w samolocie bez Internetu i chcesz mock. Albo na twojej maszynie port 8080 jest zajęty (np. przypadkiem zostawiłeś włączony inny projekt). Albo zewnętrzny katalog stał się wolny, i twoje 2000 ms nagle zamienia się w „wieczny timeout”. I co robisz? Otóż to: idziesz zmieniać kod. A potem zapominasz cofnąć. A potem commitujesz. A potem kolega z zespołu się złości, bo u niego teraz wszystko działa inaczej.
Najbardziej nieprzyjemne nie jest nawet to, że „trzeba przepisać linijkę”. Najbardziej nieprzyjemne jest to, że zmienia się artefakt. Zmienia się kod źródłowy, czyli zmienia się „wersja” aplikacji. A różnica między „potrzebuję innego portu” a „zmieniłem logikę biznesową” staje się zatarta. To bardzo zły nawyk dla backend-dewelopera.
4. static final: lepiej, ale to nie konfig
Następną naturalną reakcją mądrego człowieka jest: „No dobra, przynajmniej wyniosę literały do stałych”. To rzeczywiście czyni kod czystszym. Stałe zmniejszają duplikację i pomagają nie zamienić projektu w „cmentarz magicznych liczb”. Ale ważne, żeby zrozumieć: to nadal nie jest zewnętrzna konfiguracja. Po prostu przeniosłeś hardcoding z jednego miejsca w drugie, ale nie przestał on być częścią kodu źródłowego.
Na przykład:
public class ReadLaterApplication {
// Stałe poprawiają czytelność i usuwają duplikację "magicznych" wartości.
// Ale to wciąż wartości w kodzie: żeby zmienić — trzeba zmienić kod i przekompilować.
private static final String BASE_URL = "https://openlibrary.org";
private static final int REQUEST_TIMEOUT_MS = 2000;
private static final String API_MODE = "real";
private static final int SERVER_PORT = 8080;
public static void main(String[] args) {
// Demonstracja: używamy stałej jako "jedynego źródła" wartości wewnątrz kodu
System.out.println("API mode = " + API_MODE); // API mode = real
}
}
Z punktu widzenia czytelności jest lepiej. Ale jeśli będziesz chciał zmienić API_MODE na "mock", znów edytujesz kod źródłowy. A więc znów rekompilujesz. I znów jest szansa, że zapomnisz. I znów w Gicie pojawia się szum. To przypomina sytuację, gdy napisałeś na drzwiach „wejście z prawej”, a potem sklep się przeprowadził — możesz oczywiście kupić nowe drzwi, ale zwykle prościej powiesić tabliczkę, którą można zmieniać bez remontu budynku.
Dlatego ustalmy: stałe to świetne narzędzie przeciw chaosowi w kodzie. Ale to nie narzędzie do oddzielania kodu od środowiska. Rozwiązują problem „nie chcę widzieć magicznych wartości po całym projekcie”, ale nie rozwiązują problemu „chcę zmieniać ustawienia bez rekompilacji”.
5. Konfig i args: nie mieszamy
Tutaj wielu początkujących wpada w pułapkę: skoro mamy args, to wszystko można przekazywać przez argumenty. Technicznie — można. Ale jeśli zaczniesz przekazywać absolutnie wszystko przez args, uruchamianie aplikacji zamieni się w zaklęcie na poziomie „Harry Potter i Zakon Linii Poleceń”.
Potrzebujemy prostego i przewidywalnego modelu:
- args wybierają scenariusz: catalog search, catalog details, server;
- konfiguracja zadaje parametry środowiska: baseUrl, timeouty, real/mock, host/port.
Żeby było to widać nie tylko w tekście, ale i w głowie, można wyobrazić sobie schemat (na poziomie idei, bez implementacji):
flowchart TD
A[args] -->|wybierają| C[Launch mode]
B[konfiguracja] -->|zadaje wartości| D[Parametry środowiska]
C --> E[Logika aplikacji]
D --> E
Czyli catalog search clean code — to komenda „co zrobić teraz”. A catalog.api.base-url — to „gdzie znajduje się katalog, do którego się zwracamy”. Komenda często zmienia się od uruchomienia do uruchomienia, i to normalne. Konfiguracja też może się zmieniać, ale zwykle rzadziej i z innego powodu: bo środowisko jest inne.
Osadźmy to w naszych realnych komendach uruchomieniowych (których już używaliśmy):
# Uruchamiamy scenariusz wyszukiwania (wartości środowiska nie ciągniemy do args)
./gradlew run --args="catalog search clean code"
# Uruchamiamy scenariusz szczegółów dla konkretnego identyfikatora
./gradlew run --args="catalog details OL12345M"
# Uruchamiamy serwer (port/host — to konfiguracja, a nie część komendy)
./gradlew run --args="server"
W tych komendach widać już prawidłowy podział. W args nie ma baseUrl, nie ma timeoutów, nie ma portu — i to dobrze. W przeciwnym razie przy każdym uruchomieniu nosiłbyś ze sobą „worek ustawień”. Wartości środowiska powinny żyć oddzielnie, żeby uruchomienie pozostało czytelne: od razu rozumiesz, co uruchamiasz, a nie odszyfrowujesz „co oznacza ósmy argument”.
6. Mini-model dalszej konfiguracji
Teraz nie wdrażamy pełnego systemu konfiguracji (to kolejne wykłady dnia), ale ważne, żeby z góry mieć „szkielet myśli”, żeby się nie pogubić. W ramach tego kursu będziemy się trzymać prostego, niemal codziennego modelu: jest bazowe źródło ustawień (plik), jest zewnętrzna warstwa nadpisań (zmienne środowiskowe) i jest komenda uruchomienia (args).
Wygląda to mniej więcej tak:
flowchart TD
P[application.properties] -->|baza| X[Wybór wartości]
E[Environment variables] -->|override| X
D[Defaults w kodzie] -->|fallback| X
X --> C[AppConfig]
A[args] --> L[LaunchCommand]
C --> R[Uruchomienie logiki]
L --> R
Zwróć uwagę: defaults w kodzie są dopuszczalne, ale powinny być sensowne i lokalne. „Jeśli nic nie powiedziano, startujemy na 8080” — normalny default. „Jeśli nic nie powiedziano, idziemy na produkcyjną bramkę płatności” — to już zły default (ale my teraz nie o płatnościach, na szczęście).
Jeszcze jedna ważna zasada, którą przeprowadzimy przez cały projekt: „źródła wartości” nie powinny być rozsmarowane po aplikacji. Jeśli jedna klasa czyta System.getenv(), druga czyta plik, trzecia parsuje args — szybko dostaniesz chaos konfiguracyjny. Dorosłe podejście to zbierać konfigurację w jednym miejscu (zwykle w warstwie app/config obok punktu wejścia), a dalej przekazywać do pozostałych komponentów już gotowe wartości.
7. Czego nie wynosić do konfigu
Gdy ludzie po raz pierwszy słyszą „konfiguracja poza kodem”, czasem pojawia się chęć wyniesienia na zewnątrz wszystkiego. I tu trzeba się zatrzymać i zadać sobie proste pytanie: „Czy ta wartość naprawdę powinna się zmieniać bez rekompilacji? Czy po prostu boję się podjąć decyzję w kodzie?”
Na przykład klucze JSON ("title", "author") w naszym DTO — to część kontraktu. Jeśli wyniesiesz je do konfigu, dostaniesz system, w którym zachowanie aplikacji zależy od przypadkowego tekstu w pliku ustawień. To nie elastyczność, to zwiększone prawdopodobieństwo dziwnych bugów. To samo dotyczy „reguł domeny”. Jeśli nasza domena mówi „title nie powinien być pusty”, to nie konfiguracja. To logika. Tak, w dużych systemach niektóre reguły mogą być konfigurowalne, ale to zupełnie inny poziom złożoności i w naszym kursie byłoby to przedwczesne komplikowanie.
W ReadLater Starter wynosimy na zewnątrz właśnie parametry środowiska: adresy, porty, timeouty, tryby. To są te rzeczy, które naprawdę chcesz zmieniać bez rekompilacji, a które przy tym nie powinny zmieniać sensu operacji biznesowych. Aplikacja pozostaje tą samą aplikacją, po prostu „podłącza się” do świata innymi przewodami.
8. Typowe błędy przy wynoszeniu konfigu
Błąd nr 1: hardcodują wartości w kilku miejscach i potem heroicznie szukają ich po projekcie.
Najczęściej powstaje tak zoo: gdzieś w ReadLaterApplication stoi 8080, gdzieś w ServerLauncher jeszcze jeden 8080, a gdzieś w metodzie testowym „tymczasowo” stoi 8081. To nieprzyjemne, bo nie wiesz, która wartość jest „prawdziwa”, a każda zmiana zamienia się w quest „znajdź wszystkie wystąpienia”. Lekarstwo proste: ustalić, że wartości konfiguracyjne wybierane są w jednym miejscu, a dalej przekazywane jako parametry.
Błąd nr 2: uważają, że static final — to już zewnętrzna konfiguracja.
Stałe rzeczywiście poprawiają czytelność i będziesz z nich korzystać. Ale to wciąż część kodu źródłowego, czyli zmiana wymaga rekompilacji. Gdy tylko łapiesz się na myśli „zmienię port i zacommituję, żeby nie zapomnieć” — to niemal stuprocentowy sygnał, że mylisz kod i konfigurację. Stałe są dobre dla niezmiennych znaczeń, konfig — dla środowiska.
Błąd nr 3: mieszają „komendę uruchomienia” i „ustawienia środowiska”.
Jeśli zaczynasz przekazywać baseUrl lub port przez args, uruchomienie szybko staje się nieczytelne. I odwrotnie, jeśli próbujesz „w konfigu wybrać komendę”, dostajesz aplikację, którą trudno uruchamiać jednoznacznie: uruchamiasz program, a on „w zależności od nastroju” decyduje, czy być serwerem, czy klientem. W naszym projekcie podział jest jasny: args to komenda (server lub catalog ...), konfig to parametry tej komendy.
Błąd nr 4: próbują zrobić konfiguracją każdą lokalną zmienną.
Czasem wygląda to tak: „a wynieśmy do konfigu napis ‚Searching…’ i komunikat błędu też, i długość wcięcia w konsoli…”. Skutek — ogromny plik ustawień, którego nikt nie czyta, i aplikacja, która stała się bardziej skomplikowana bez realnej korzyści. W tym kursie trzymamy się pragmatycznie: konfiguracją staje się to, co realnie zmienia się między środowiskami i wpływa na integracje oraz uruchomienie.
Błąd nr 5: robią konfigurację, ale zapominają nazwać klucze po ludzku.
Klucze typu x1, timeout2, modeFlag mogą być „zrozumiałe dla autora dzisiaj”, ale jutro sam będziesz przeklinał ten plik. Normalne klucze powinny czytać się jak zdanie: catalog.api.base-url, server.port. To nie estetyka — to część dyscypliny inżynierskiej, bo konfig to taki sam interfejs jak metoda publiczna.
GO TO FULL VERSION