1. Вступ
Спершу давайте чесно зізнаємося: писати все в main() — цілком нормальний етап. Так роблять усі. Навіть досвідчені розробники… коли поспішають, втомилися й думають: «тимчасово, потім виправлю» (спойлер: не виправлять). Проблема починається тоді, коли main() стає надто довгим і без прокручування та внутрішнього діалогу із собою ви вже не розумієте, що там відбувається.
Уявіть, що main() робить усе підряд: зчитує дані, перевіряє введення, рахує результат, друкує, показує меню, знову зчитує, знову перевіряє… У підсумку будь-які зміни стають небезпечними: ви виправили одну дрібницю, а зламалося щось на іншому кінці файла. Не тому, що ви «поганий програміст», а тому, що код не поділено на смислові частини, у яких є назви й межі відповідальності.
Декомпозиція — це спосіб перетворити код із «простирадла» на набір деталей конструктора: кожна деталь відповідає за свою роботу. А ще це значно спрощує налагодження: якщо помилка в друку меню, ви йдете у функцію друку меню, а не шукаєте серед 300 рядків, де там був cout.
Функція як «іменована дія»
Функція — це іменований блок коду, який можна викликати. Але важливіше інше: це спосіб дати імʼя дії. Коли ви даєте імʼя, то перестаєте щоразу думати про деталі. Це як із кнопкою на мікрохвильовці: вам не потрібно щоразу заново вигадувати, «як розігріти їжу», — ви просто натискаєте «Розігрів».
З погляду програми функція допомагає у двох речах: по-перше, ховає деталі — цикл, перевірки, формат виведення — за зрозумілою назвою; по-друге, дає змогу повторно використовувати код без копіпасти. І так, копіпаста — це не «швидко». Це «швидко зараз, дорого потім».
Міні-ідея, яку варто тримати в голові з першого дня знайомства з функціями: хороший main() — це сценарій. Він має читатися приблизно як інструкція: «прочитати команду → обробити → показати результат → повторити». А деталі нехай живуть у функціях.
2. «Тонкий main» як сценарій
Що означає «тонкий main»
Термін «тонкий main» означає, що в main() майже немає деталей. Він не займається тим, «як саме друкувати меню» або «як саме додавати елемент до списку». Він лише каже: зараз покажемо меню, зараз прочитаємо команду, зараз обробимо команду.
Це схоже на роль режисера: режисер не тримає камеру, не пише музику й не клеїть декорації. Він каже: «сцена 1», «сцена 2», «сцена 3». У коді «режисер» — це main(), а «сцени» — функції.
Щоб цей образ закріпився, ось невелика схема. Не сприймайте її як сувору архітектуру: це радше «образ у голові».
flowchart TD
A[main()] --> B[показати меню]
A --> C[прочитати команду]
A --> D[обробити команду]
D --> E[додати елемент]
D --> F[показати елементи]
D --> G[очистити елементи]
Головна думка: main() не зобовʼязаний знати, як улаштовані add_item або print_items. Він просто викликає їх як «інструменти з назвами».
Коли справді час виділяти функцію
Початківці часто запитують: «А як зрозуміти, що цей шматок треба винести?» Це хороше запитання, бо тут легко впасти у дві крайнощі. Перша крайність — усе в main(), друга — 50 функцій по одному рядку, і ви вже не розумієте, де саме перебуваєте.
Практичне правило, без філософії, таке: виділяйте функцію, якщо шматок коду або повторюється, або має самостійний зміст, який можна назвати одним дієсловом. «Показати меню», «перевірити, що рядок не порожній», «порахувати суму», «надрукувати список».
Невелика таблиця — як підказка для мозку:
| Спостереження в коді | Що це зазвичай означає | Що зробити |
|---|---|---|
| Один і той самий фрагмент скопійовано два чи більше разів | Майбутній біль під час змін | Винести у функцію |
| У середині main() починається «мікроалгоритм» (цикл + умови + виведення) | Це окрема дія | Дати імʼя: винести у функцію |
| main() складно переказати одним реченням | Змішані відповідальності | Розділити на кроки-функції |
| Функція виростає до 40–60 рядків, і ви не можете коротко сказати, «що вона робить» | Вона робить забагато | Розділити на 2–3 функції |
Важливо: на перших порах не женіться за «ідеальністю». Наша мета — зробити код зрозумілішим насамперед для вас самих. Якщо після винесення функції читати стало легше, отже, ви все зробили правильно.
3. Практичний приклад: міні-застосунок «Список справ»
Зараз ми почнемо збирати маленький консольний застосунок, який супроводжуватиме нас упродовж усього курсу й поступово вдосконалюватиметься. Сьогоднішня версія буде простою: список рядків (справ), команди додати/показати/очистити/вийти. Жодної магії — лише вже знайомі std::string, std::vector, цикли та введення/виведення.
Спочатку ми спеціально зробимо «погану» версію, де все в main(). Потім почнемо виносити частини у функції — і побачимо, як main() стає тонким і зручним.
Поганий старт: усе в main()
Цей шматок коду не «жахливий», він просто типовий. І це саме те, із чого зручно починати рефакторинг.
#include <iostream>
#include <string>
#include <vector>
int main() {
std::vector<std::string> tasks;
while (true) {
std::cout << "1) додати 2) список 3) очистити 0) вихід\n> ";
std::string cmd;
std::getline(std::cin, cmd);
if (cmd == "1") {
std::cout << "Справа: ";
std::string text;
std::getline(std::cin, text);
tasks.push_back(text);
} else if (cmd == "2") {
for (int i = 0; i < static_cast<int>(tasks.size()); ++i)
std::cout << (i + 1) << ") " << tasks[i] << '\n';
} else if (cmd == "3") {
tasks.clear();
} else if (cmd == "0") {
break;
}
}
}
Зауважте: тут уже є все — меню, введення команди, обробка, робота з даними, друк. Поки програма маленька, це ще терпимо. Але варто додати «видалити одну справу», «пошук за підрядком», «зберегти/завантажити» — і main() швидко перетвориться на кашу.
Крок 1: виносимо «показати меню»
Зараз ми акуратно зробимо найпростіше: дамо імʼя друку меню. Це майже завжди хороший перший крок, бо меню зазвичай друкується в кількох місцях або друкуватиметься пізніше, і воно взагалі не зобовʼязане жити всередині main().
Головна перевага тут така: main() починає читатися як сценарій, а не як драйвер принтера. Ми поки не обговорюємо тонкощі параметрів і значень, які повертаються, — нам важливо відчути саму ідею «іменованої дії».
#include <iostream>
void print_menu() {
std::cout << "1) додати 2) список 3) очистити 0) вихід\n> ";
}
І тепер main() викликає цю дію:
#include <iostream>
#include <string>
#include <vector>
void print_menu() {
std::cout << "1) додати 2) список 3) очистити 0) вихід\n> ";
}
int main() {
std::vector<std::string> tasks;
while (true) {
print_menu();
std::string cmd;
std::getline(std::cin, cmd);
// обробка команди поки тут
}
}
Смішно, але це вже перемога: ми прибрали одну «деталь» із main() і дали їй імʼя.
Крок 2: виносимо читання рядка з підказкою
Дуже часто програми роблять одне й те саме: друкують підказку й читають рядок. Це ідеальний кандидат на функцію, бо така дія повторюється і має зрозумілий зміст: «прочитати рядок».
Зверніть увагу: функція повертає рядок. Це зручно, бо її можна використовувати як значення. І це хороший стиль для початківця: обчислення та отримання результату краще оформлювати як значення, яке повертає функція, а void залишати для друку.
#include <iostream>
#include <string>
std::string read_line(const std::string& prompt) {
std::cout << prompt;
std::string s;
std::getline(std::cin, s);
return s;
}
Тепер main() стає ще більш «режисерським»:
#include <iostream>
#include <string>
#include <vector>
std::string read_line(const std::string& prompt) {
std::cout << prompt;
std::string s;
std::getline(std::cin, s);
return s;
}
int main() {
std::vector<std::string> tasks;
while (true) {
std::string cmd = read_line("1) додати 2) список 3) очистити 0) вихід\n> ");
// обробка команди поки тут
}
}
Так, ми поки що друкуємо меню прямо з read_line() — це нормально для поточного кроку. Декомпозицію часто роблять «драбинкою», а не одним стрибком.
Крок 3: операції зі списком справ
Тепер винесемо обробку списку справ: друк, додавання, очищення. Тут важливо тримати в голові один нюанс: ми ще не проходили передавання за посиланням, тому заради простоти повертатимемо новий std::vector<std::string> із функцій, які його змінюють. Для маленьких навчальних прикладів це нормально: мета — зрозуміти декомпозицію, а не займатися оптимізацією.
Друк списку справ
Функція друку не змінює список, вона просто його показує.
#include <iostream>
#include <string>
#include <vector>
void print_tasks(const std::vector<std::string>& tasks) {
for (int i = 0; i < static_cast<int>(tasks.size()); ++i) {
std::cout << (i + 1) << ") " << tasks[i] << '\n';
}
}
Якщо вас збентежив const std::vector<std::string>&: це «посилання на константний вектор», тобто ми обіцяємо не змінювати tasks. Докладніше ми розберемо це на наступному занятті, але саму ідею можна зрозуміти вже зараз: «я хочу передати список у функцію, але не копіювати його повністю».
Якщо поки хочеться зовсім обійтися без посилань, можна зробити версію «за значенням» — вона теж працюватиме, просто створюватиме копію. Для навчання декомпозиції це допустимо.
Додавання справи
Додавання змінює список, тому в нашому «поки що без посилань» стилі повернемо новий вектор.
#include <string>
#include <vector>
std::vector<std::string> add_task(std::vector<std::string> tasks, std::string text) {
tasks.push_back(text);
return tasks;
}
Очищення списку
Очищення теж змінює список.
#include <string>
#include <vector>
std::vector<std::string> clear_tasks(std::vector<std::string> tasks) {
tasks.clear();
return tasks;
}
Збираємо «тонкий main»: сценарій замість простирадла
Зараз найприємніше: ми поєднуємо все в main() так, щоб він читався як сценарій. Зверніть увагу: main() більше не знає, як саме друкуються справи або як саме додається нова справа. Він просто каже: «якщо команда така — робимо ось це».
Це і є «тонкий main»: мінімум деталей, максимум змісту.
#include <iostream>
#include <string>
#include <vector>
std::string read_line(const std::string& prompt) {
std::cout << prompt;
std::string s;
std::getline(std::cin, s);
return s;
}
void print_tasks(const std::vector<std::string>& tasks) {
for (int i = 0; i < static_cast<int>(tasks.size()); ++i) {
std::cout << (i + 1) << ") " << tasks[i] << '\n';
}
}
std::vector<std::string> add_task(std::vector<std::string> tasks, std::string text) {
tasks.push_back(text);
return tasks;
}
std::vector<std::string> clear_tasks(std::vector<std::string> tasks) {
tasks.clear();
return tasks;
}
int main() {
std::vector<std::string> tasks;
while (true) {
std::string cmd = read_line("1) додати 2) список 3) очистити 0) вихід\n> ");
if (cmd == "1") {
std::string text = read_line("Справа: ");
tasks = add_task(tasks, text);
} else if (cmd == "2") {
print_tasks(tasks);
} else if (cmd == "3") {
tasks = clear_tasks(tasks);
std::cout << "Очищено\n";
} else if (cmd == "0") {
break;
} else {
std::cout << "Невідома команда\n";
}
}
}
Так, main() усе ще містить розгалуження за командами. Але тепер усередині гілок майже немає деталей. І це вже величезний крок: програма стала розширюваною. Хочете додати команду "4) видалити"? Ви просто додасте гілку й функцію, а не почнете вбудовувати нову операцію в гігантський main().
4. Як називати функції, щоб вас розуміли
Імена функцій — це не прикраса, а частина інтерфейсу програми. Початківці часто соромляться довших назв, але краще чесне print_tasks(), ніж загадкове doStuff2() (яке за тиждень означатиме «роби щось, але що саме — не знаю навіть я»).
Хороша назва функції зазвичай починається з дієслова й відповідає на запитання «що робить?»: print_menu, read_line, add_task, clear_tasks. Погана назва — це або «як робить»: loop_and_print_vector (надто детально), або така, що нічого не каже: f1.
Усередині команди розробників назви — це ще й інструмент домовленості. У навчальному проєкті це особливо важливо, бо ви самі для себе теж команда, просто поки що з однієї людини. І так, іноді ви сперечаєтеся з керівником команди — і цим керівником теж є ви.
5. Типові помилки
Помилка № 1: функція «робить усе одразу» (введення + обчислення + виведення) без потреби.
Початківцям часто здається, що зручно написати void add_task() так, щоб вона сама запитувала текст, сама додавала його й сама друкувала «успішно». У маленьких прикладах це працює, але дуже швидко перетворює код на заплутаний клубок. Намагайтеся, щоб функція виконувала одну зрозумілу дію. Якщо потрібно і запитати, і додати, — нехай main() організує сценарій: спочатку read_line(), потім add_task().
Помилка № 2: винести у функцію «шматок коду», але не винести зміст.
Буває так: ви створили функцію process() і перенесли туди половину main(). Формально декомпозиція є, але фактично змісту не побільшало. Простий тест — спробуйте прочитати main() як текст: якщо він усе ще незрозумілий без переходу всередину функцій, отже, функції названо надто загально або виділено не за смисловими межами.
Помилка № 3: надто дрібні функції «заради функцій».
Іноді хтось зі студентів виносить у функцію рядок на кшталт std::cout << '\n'; і гордо каже: «у мене все у функціях». Формально так, але читати стало гірше. Якщо функція не додає змісту, тобто не дає хорошої назви дії, і не зменшує повторення, то вона радше заважає, ніж допомагає.
Помилка № 4: копіпаста замість функції — «потім зроблю красиво».
Найпідступніший сценарій: ви один раз скопіювали 6 рядків. Потім другий. Потім третій. А далі змінюєте одну деталь і забуваєте змінити її у двох копіях. У підсумку зʼявляються «примарні баги», коли однакові шматки поводяться по-різному. Щойно ви помітили другий копіпаст того самого фрагмента, це майже гарантований кандидат на функцію.
Помилка № 5: намагатися зробити main() «ідеально порожнім» і втратити керованість.
Іноді після слів «тонкий main» хочеться довести ідею до абсурду: сховати взагалі все, зокрема цикл і вибір команд. У підсумку main() стає незрозумілим набором магічних викликів, а логіка керування виявляється розпорошеною. Правильний тонкий main() — це сценарій, а сценарій зазвичай містить цикл і розвилки. Тонкість тут в іншому: усередині сценарію не повинно бути брудних деталей.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ