1. Введение
Сначала давайте честно признаемся: писать всё в main() — абсолютно нормальная стадия. Так делают все. Даже опытные разработчики… когда торопятся, устали и «временно, потом поправлю» (спойлер: не поправят). Проблема начинается, когда main() становится слишком длинным, и вы уже не понимаете, что там происходит без прокрутки и внутреннего диалога с собой.
Представьте, что main() делает всё подряд: вводит данные, проверяет ввод, считает результат, печатает, рисует меню, снова вводит, снова проверяет… В итоге изменения становятся опасными: вы исправили одну мелочь, а сломалось что-то в другом конце файла. Не потому что вы «плохой программист», а потому что код не разделён на смысловые куски, у которых есть имена и границы ответственности.
Декомпозиция — это способ превратить код из «простыни» в набор деталей конструктора: каждая деталь отвечает за свою работу. А ещё это сильно упрощает отладку: если баг в печати меню — вы идёте в функцию печати меню, а не ищете среди 300 строк, где там был cout.
Функция как «именованный кусок смысла»
Функция — это именованный блок кода, который можно вызывать. Но важнее другое: функция — это способ дать имя действию. Когда вы даёте имя, вы перестаёте думать о деталях каждый раз. Это как с кнопкой на микроволновке: вам не нужно каждый раз заново изобретать «как разогревать еду», вы просто нажимаете «Разогрев».
С точки зрения программы функция помогает сделать две вещи: во-первых, спрятать детали (цикл, проверки, формат вывода) за понятным названием; во-вторых, переиспользовать код без копипаста. И да, копипаст — это не «быстро». Это «быстро сейчас, дорого потом».
Мини-идея, которую полезно держать в голове с первого дня функций: хороший main() — это сценарий. Он должен читаться примерно как инструкция: «прочитать команду → обработать → показать результат → повторить». А детали пусть живут в функциях.
2. «Тонкий main» как сценарий
Что значит «тонкий main»
Термин «тонкий main» означает, что в main() почти нет деталей. Он не занимается «как именно печатать меню» или «как именно добавлять элемент в список». Он лишь говорит: сейчас покажем меню, сейчас прочитаем команду, сейчас обработаем команду.
Это похоже на роль режиссёра: режиссёр не держит камеру, не пишет музыку и не клеит декорации. Он говорит: «сцена 1», «сцена 2», «сцена 3». В коде «режиссёр» — это main(), а «сцены» — функции.
Чтобы картинка закрепилась, вот небольшая схема. Не воспринимайте её как строгую архитектуру, это именно «образ в голове».
flowchart TD
A[main] --> B[print_menu]
A --> C[read_command]
A --> D[handle_command]
D --> E[add_item]
D --> F[print_items]
D --> G[clear_items]
Главная мысль: main() не обязан знать, как устроены add_item или print_items. Он просто вызывает их, как «инструменты по имени».
Когда действительно пора выделять функцию
Новички часто спрашивают: «А как понять, что этот кусок надо вынести?» Это хороший вопрос, потому что можно впасть в две крайности. Первая крайность — всё в main(), вторая — 50 функций по одной строке, и вы уже не понимаете, где вы находитесь.
Практичное правило (без философии) такое: выделяйте функцию, если кусок кода либо повторяется, либо имеет самостоятельный смысл, который можно назвать одним глаголом. «Показать меню», «проверить, что строка не пустая», «посчитать сумму», «напечатать список».
Небольшая табличка — как подсказка для мозга:
| Наблюдение в коде | Что это обычно значит | Что сделать |
|---|---|---|
| Один и тот же фрагмент скопирован 2+ раза | Будущая боль при изменениях | Вынести в функцию |
| В середине 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) add 2) list 3) clear 0) exit\n> ";
std::string cmd;
std::getline(std::cin, cmd);
if (cmd == "1") {
std::cout << "Task: ";
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) add 2) list 3) clear 0) exit\n> ";
}
И теперь main() вызывает это действие:
#include <iostream>
#include <string>
#include <vector>
void print_menu() {
std::cout << "1) add 2) list 3) clear 0) exit\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) add 2) list 3) clear 0) exit\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) add 2) list 3) clear 0) exit\n> ");
if (cmd == "1") {
std::string text = read_line("Task: ");
tasks = add_task(tasks, text);
} else if (cmd == "2") {
print_tasks(tasks);
} else if (cmd == "3") {
tasks = clear_tasks(tasks);
std::cout << "Cleared\n";
} else if (cmd == "0") {
break;
} else {
std::cout << "Unknown command\n";
}
}
}
Да, main() всё ещё содержит ветвление по командам. Но теперь внутри веток почти нет деталей. И это уже огромный шаг: программа стала расширяемой. Хотите добавить команду "4) remove"? Вы добавите ветку и функцию, а не начнёте «вживлять операцию» внутрь гигантского 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() — это сценарий, а сценарий обычно содержит цикл и развилки. Тонкость здесь в другом: внутри сценария не должно быть грязных деталей.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ