1. Конфіг у ReadLater Starter
Якщо ви писали лише навчальні консольні програми, дуже легко повірити в наївну магію: «Ну, я ж один раз написав String baseUrl = "...", отже так і буде завжди». Але бекенд-застосунки живуть у реальності, де один і той самий застосунок доводиться запускати в різних умовах: у вас удома, у напарника, на іншому комп’ютері, з інтернетом або без, на іншому порту, у режимі mock, якщо зовнішній сервіс «упав». І все це має вирішуватися без правки вихідного коду, інакше кожен запуск перетворюється на мініремонт.
Почнімо з чесного визнання: наш ReadLater Starter уже зараз живе не в одному режимі. У нього є клієнтська фаза з командами catalog search і catalog details, і незабаром з’явиться серверна фаза (server). Це означає, що в нас одразу виникають параметри, які неминуче відрізняються від запуску до запуску: адреса зовнішнього API, таймаути, режим real/mock, порт сервера. І саме тут хардкод починає поводитися як той знайомий, який «точно прийде вчасно» — але тільки якщо ви завжди зустрічаєтеся в одному й тому самому місці, в один і той самий день і ніколи не змінюєте плани.
Гарне робоче правило на сьогодні звучить так: код відповідає на запитання «що робить застосунок», а конфігурація — на запитання «з якими значеннями він це робить». Наприклад, «робить HTTP-запит до каталогу» — це логіка. А «куди саме робить запит» — це конфігурація.
Конфігурація — це окрема відповідальність
Найпростіше зрозуміти конфігурацію через аналогію з рецептом. Рецепт говорить, що робити: змішати, запекти, почекати. Але він не має бути зашитий в «єдино можливі інгредієнти на планеті Земля». Сьогодні у вас молоко 2,5 %, завтра — 3,2 %, і це не привід переписувати рецепт і перескладати кухню заново. У застосунку все так само: логіка має залишатися стабільною, а параметри — змінюватися без перекомпіляції.
У бекенд-світі конфігурація — це набір значень, які можуть відрізнятися між середовищами (у різних розробників, на різних машинах, у різних умовах мережі) або між запусками (сьогодні mock, завтра real). При цьому конфігурація зазвичай не має потрапляти в доменну модель і не має розповзатися по всьому коду. Це окрема відповідальність: її зручно тримати «поруч із точкою входу» і передавати іншим частинам застосунку вже в зрозумілому вигляді.
Тут виникає ще одна важлива думка: конфігурація — це не «все, що мені лінь писати». Є спокуса зробити конфігом кожну змінну, і тоді застосунок перетвориться на казино налаштувань. Ми триматимемося здорового глузду: виносити назовні лише те, що справді має змінюватися без правки Java-коду.
2. Конфігурація в ReadLater Starter
Тепер ми акуратно зафіксуємо саме для нашого проєкту: що є конфігурацією, а що — даними запуску (аргументами) або частиною логіки. Щоб не перетворювати це на список «на пам’ять», оформімо все в таблицю: так простіше побачити межу.
| Категорія | Приклад | Чому це саме воно |
|---|---|---|
| Конфігурація застосунку | app.name=readlater | Ім’я не впливає на бізнес-логіку, але може бути різним у різних середовищах і логах |
| Конфігурація клієнта каталогу | catalog.api.base-url=https://openlibrary.org | Адреса зовнішнього сервісу може відрізнятися (real/mock, інший провайдер, інший стенд) |
| Конфігурація клієнта каталогу | catalog.api.request-timeout-ms=2000 | Таймаути залежать від середовища й мережі, а не від «логіки пошуку книги» |
| Конфігурація клієнта каталогу | catalog.api.mode=real | Це перемикач поведінки без зміни коду, що особливо корисно для навчання |
| Конфігурація server-mode | server.host=localhost, server.port=8080 | Порт може бути зайнятий, хост може відрізнятися, а код сервера має бути тим самим |
| Дані конкретного запуску (args) | catalog search clean code | Це разова команда: сьогодні шукаємо «clean code», завтра — «java concurrency» |
| Дані конкретного запуску (args) | catalog details OL12345M | Це разовий ідентифікатор книги для конкретного виклику |
| Частина прикладної логіки | «як шукати книги», «як нормалізувати DTO» | Це поведінка застосунку, яка не має змінюватися від середовища |
Зверніть увагу на тонкий, але важливий момент. Режим server — це не конфігурація, а команда. А server.port — уже конфігурація для цієї команди. Так само «шукати книгу» — команда, а catalog.api.base-url — конфігурація, з якою команда працює.
Якщо зараз ви не відчуваєте різниці, це нормально. Її зазвичай починають відчувати, коли одного разу змінюють порт із 8080 на 8081 і раптом роблять коміт у Git «тому що інакше не запускається». І тут виникає запитання: а чому взагалі зміна порту вимагала зміни вихідного коду?
3. Хардкод у main(): швидкий старт, повільне життя
Іноді хардкод виглядає невинно. Особливо на початку курсу. Він навіть здається «чесним»: ось значення, ось код, усе поруч. Проблема в тому, що бекенд-проєкт — річ довгоживуча, і ця «невинність» швидко закінчується. Ви захочете перемкнути real/mock, збільшити таймаут, підняти сервер на іншому порту — і раптом виявиться, що кожне таке бажання потребує: відкрити вихідний код, змінити рядок, перескласти, перевірити, чи не зламали ви чогось.
Подивімося на типовий стартовий хардкод, який у маленьких проєктах виникає майже автоматично:
public class ReadLaterApplication {
public static void main(String[] args) {
// Конфігураційні значення середовища: сьогодні одні, завтра інші.
// Проблема в тому, що вони "зашиті" у вихідний код, а отже потребують правки коду й перескладання.
String baseUrl = "https://openlibrary.org"; // адреса зовнішнього API (може бути real/mock або інший стенд)
int requestTimeoutMs = 2000; // мережеві таймаути залежать від мережі та середовища
String apiMode = "real"; // перемикач поведінки: "real" або "mock"
int serverPort = 8080; // порт може бути зайнятий на конкретній машині
// Для демонстрації: виводимо поточні значення, з якими застосунок стартував
System.out.println("baseUrl = " + baseUrl); // baseUrl = https://openlibrary.org
System.out.println("requestTimeoutMs = " + requestTimeoutMs); // requestTimeoutMs = 2000
}
}
На рівні «я запускаю все у себе» це працює. Але тепер уявіть реальність: ви в літаку без інтернету й хочете mock. Або на вашій машині порт 8080 зайнятий, наприклад, ви випадково залишили запущеним інший проєкт. Або зовнішній каталог став повільним, і ваш 2000 ms раптом перетворюється на «вічний таймаут». І що ви робите? Правильно: ідете правити код. Потім забуваєте відкотити. Потім комітите. А потім напарник свариться, бо в нього тепер усе працює не так.
Найнеприємніше тут навіть не те, що «треба переписати рядок». Найнеприємніше — що змінюється артефакт. У вас змінюється вихідний код, а отже змінюється «версія» застосунку. І різниця між «мені потрібен інший порт» і «я змінив бізнес-логіку» стає розмитою. Це дуже погана звичка для бекенд-розробника.
4. static final: краще, але не конфіг
Наступна природна реакція розумної людини: «Гаразд, я хоча б винесу літерали в константи». Це справді робить код чистішим. Константи зменшують дублювання і допомагають не перетворювати проєкт на «цвинтар магічних чисел». Але важливо зрозуміти: це все ще не зовнішня конфігурація. Ви просто переклали хардкод з одного місця в інше, але він не перестав бути частиною вихідного коду.
Наприклад:
public class ReadLaterApplication {
// Константи покращують читабельність і прибирають дублювання "магічних" значень.
// Але це все одно значення в коді: щоб змінити їх, треба правити вихідний код і перескладати проєкт.
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) {
// Демонстрація: використовуємо константу як "єдине джерело" значення всередині коду
System.out.println("Режим API = " + API_MODE); // Режим API = real
}
}
З погляду читабельності стало краще. Але якщо ви захочете змінити API_MODE на "mock", ви знову редагуєте вихідний код. А отже — знову перескладаєте. І знову є шанс забути. І знову в Git з’являється шум. Це схоже на ситуацію, коли ви написали на дверях «вхід праворуч», а потім магазин переїхав: можна, звісно, купити нові двері, але зазвичай простіше повісити табличку, яку можна замінити без ремонту будівлі.
Тому зафіксуймо: константи — чудовий інструмент проти хаосу в коді. Але це не інструмент відокремлення коду від середовища. Вони вирішують проблему «не хочу бачити магічні значення по всьому проєкту», але не вирішують проблему «хочу змінювати налаштування без перескладання».
5. Конфіг і args: не змішуємо
Тут багато новачків потрапляють у пастку: раз у нас є args, значить усе можна передавати аргументами. Технічно — можна. Але якщо ви почнете передавати абсолютно все через args, то запуск застосунку перетвориться на заклинання рівня «Гаррі Поттер і Орден командного рядка».
Нам потрібна проста й передбачувана модель:
- args вибирають сценарій: catalog search, catalog details, server;
- конфігурація задає параметри середовища: baseUrl, таймаути, real/mock, host/port.
Щоб це було видно не лише в тексті, а й у голові, можна уявити схему на рівні ідеї, без реалізації:
flowchart TD
A[args] -->|вибирають| C[Режим запуску]
B[конфігурація] -->|задає значення| D[Параметри середовища]
C --> E[Логіка застосунку]
D --> E
Тобто catalog search clean code — це команда «що зробити зараз». А catalog.api.base-url — це «де знаходиться каталог, до якого ми звертаємося». Команда часто змінюється від запуску до запуску, і це нормально. Конфігурація теж може змінюватися, але зазвичай рідше і з іншої причини: тому що середовище інше.
Давайте приземлимо це на наші реальні команди запуску, які ми вже використовували:
# Запускаємо сценарій пошуку (значення середовища не тягнемо в args)
./gradlew run --args="catalog search clean code"
# Запускаємо сценарій деталей за конкретним ідентифікатором
./gradlew run --args="catalog details OL12345M"
# Запускаємо сервер (порт/хост — це конфігурація, а не частина команди)
./gradlew run --args="server"
У цих командах уже видно правильний поділ. У args немає baseUrl, немає таймаутів, немає порту — і це добре. Інакше ви б у кожному запуску тягали із собою «мішок налаштувань». Значення середовища мають жити окремо, щоб запуск залишався читабельним: ви одразу розумієте, що запускаєте, а не розшифровуєте «що означає восьмий аргумент».
6. Міні-модель подальшої конфігурації
Зараз ми не впроваджуємо повноцінну систему конфігурації (це наступні лекції курсу), але важливо заздалегідь мати «скелет думки», щоб не загубитися. У межах цього курсу ми триматимемося простої, майже побутової моделі: є базове джерело налаштувань (файл), є зовнішній шар перевизначень (змінні середовища), і є команда запуску (args).
Виглядає це приблизно так:
flowchart TD
P[application.properties] -->|база| X[Вибір значення]
E[Environment variables] -->|перевизначення| X
D[Типові значення в коді] -->|резервний варіант| X
X --> C[AppConfig]
A[args] --> L[LaunchCommand]
C --> R[Запуск логіки]
L --> R
Зверніть увагу: типові значення в коді допустимі, але вони мають бути осмисленими й локальними. «Якщо нічого не сказали, запускаємося на 8080» — нормальний дефолт. «Якщо нічого не сказали, йдемо на продовий платіжний шлюз» — уже поганий дефолт (але ми зараз, на щастя, не про платежі).
Ще одне важливе правило, яке ми проведемо через увесь проєкт: «джерела значень» не мають бути розмазані по застосунку. Якщо один клас читає System.getenv(), інший — файл, третій — парсить args, ви швидко отримаєте конфігураційний хаос. Дорослий підхід — збирати конфігурацію в одному місці, зазвичай у app/config-шарі поруч із точкою входу, а далі передавати в інші компоненти вже готові значення.
7. Що не виносити в конфіг
Коли люди вперше чують «конфігурація поза кодом», у них іноді з’являється бажання винести назовні взагалі все. І тут важливо зупинитися та поставити собі просте запитання: «Це значення справді має змінюватися без перекомпіляції? Чи я просто боюся прийняти рішення в коді?»
Наприклад, ключі JSON ("title", "author") у нашому DTO — це частина контракту. Якщо ви винесете їх у конфіг, ви отримаєте систему, де поведінка застосунку залежить від випадкового тексту у файлі налаштувань. Це не гнучкість, а підвищена ймовірність дивних багів. Те саме стосується «правил домену». Якщо наш домен каже «title не має бути порожнім», це не конфігурація. Це логіка. Так, у великих системах деякі правила можуть налаштовуватися, але це зовсім інший рівень складності, і в нашому курсі це було б передчасним ускладненням.
У ReadLater Starter ми виносимо назовні саме параметри середовища: адреси, порти, таймаути, режими. Це ті речі, які вам справді хочеться змінювати без перескладання і які при цьому не мають змінювати зміст бізнес-операцій. Застосунок і далі залишається тим самим, просто «під’єднується» до світу іншими дротами.
8. Типові помилки під час винесення конфігу
Помилка № 1: хардкодять значення в кількох місцях і потім героїчно шукають їх по проєкту.
Найчастіше так з’являється зоопарк: десь у ReadLaterApplication стоїть 8080, десь у ServerLauncher ще один 8080, десь у тестовому методі «тимчасово» стоїть 8081. Це неприємно тим, що ви не знаєте, яке значення є «істинним», і будь-яка зміна перетворюється на квест «знайди всі входження». Ліки прості: домовитися, що конфігураційні значення вибираються в одному місці, а далі передаються як параметри.
Помилка № 2: вважають, що static final — це вже зовнішня конфігурація.
Константи справді покращують читабельність, і ви будете ними користуватися. Але вони все ще є частиною вихідного коду, а отже зміна вимагає перескладання. Щойно ви ловите себе на думці «я зміню порт і закомічу, щоб не забути», — це майже стовідсотковий сигнал, що ви плутаєте код і конфігурацію. Константи хороші для незмінних смислів, конфіг — для середовища.
Помилка № 3: змішують «команду запуску» та «налаштування середовища».
Якщо ви починаєте передавати baseUrl або порт через args, запуск швидко стає нечитабельним. І навпаки, якщо ви намагаєтеся «у конфігу вибрати команду», у вас виходить застосунок, який важко запускати явно: ви запускаєте програму, а вона «за настроєм» вирішує бути сервером або клієнтом. У нашому проєкті поділ чіткий: args — це команда (server або catalog ...), конфіг — це параметри цієї команди.
Помилка № 4: намагаються зробити конфігурацією кожну локальну змінну.
Іноді це виглядає так: «А давайте винесемо в конфіг рядок “Searching…” і повідомлення про помилку теж, і довжину відступу в консолі…». Підсумок — величезний файл налаштувань, який ніхто не читає, і застосунок, який став складнішим без реальної користі. У цьому курсі ми тримаємося прагматично: конфігурацією стає те, що реально змінюється між середовищами й впливає на інтеграції та запуск.
Помилка № 5: роблять конфігурацію, але забувають називати ключі по-людськи.
Ключі на кшталт x1, timeout2, modeFlag можуть бути «зрозумілі автору сьогодні», але завтра ви самі ж проклинатимете цей файл. Нормальні ключі мають читатися як речення: catalog.api.base-url, server.port. Це не естетика — це частина інженерної дисципліни, тому що конфіг — такий самий інтерфейс, як публічний метод.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ