Всем привет, меня зовут Сергей Прощаев. Я техлид в FinTech, преподаю на курсах в Otus и продолжаю нашу серию «Kotlin для новичков».
В прошлый раз мы разобрались с условиями и циклами. Научились управлять программой, заставляли её принимать решения. Но что, если программа разрастается? Представьте, что вся логика кредитного скоринга, которую мы обсуждали в прошлой статье, написана в одном main(). Это будет монстр на 500–1000 строк. В такой «простыне» невозможно ориентироваться.
Сегодня разберём ключевую тему. Мы научимся делить программу на кирпичики — функции. Речь не только о синтаксисе. Это про то, как перестать писать «спагетти‑код» и начать строить системы, которые легко читать, тестировать и развивать.
1. Зачем нужны функции? (Или история про шкаф)
В начале своей карьеры я работал в банке, где один из модулей банковского приложения представлял собой файл более двух тысяч строк. Всё это было свалено в кучу. Чтобы поменять формулу расчёта процента, нужно было найти нужную строчку среди сотен if-else. Страх сломать что‑то был настолько велик, что разработчики просто копировали код, создавая новые баги.
Функции — это способ разложить программу по полочкам. Как в шкафу: носки в одном ящике, рубашки — в другом. Когда нужно найти носки, вы не перерываете весь шкаф, вы знаете, где лежит нужный ящик.
В программировании это называется декомпозиция. Мы разбиваем большую задачу на маленькие, логически завершенные подзадачи.
2. Как объявлять функции: простой синтаксис
В Java вы привыкли писать public static void main. В Kotlin всё короче и ближе к математике.
Базовая структура выглядит так:fun (сокращение от function) → имя функции → аргументы (входные данные) → возвращаемый тип.
package ru.otus // Простая функция: принимает имя, возвращает приветствие fun greetUser(name: String): String { return "Привет, $name! Добро пожаловать в OTUS." } fun main() { val message = greetUser("Алексей") println(message) }
Важный нюанс (мой любимый): если функция состоит из одного выражения, можно убрать фигурные скобки и return. Это называется single‑expression function.
// То же самое, но лаконично fun greetUserShort(name: String): String = "Привет, $name!"
Компилятор Kotlin настолько умен, что часто может вывести тип возвращаемого значения сам, если вы используете короткую форму. Но в публичных API рекомендую тип указывать явно — это делает код самодокументируемым.
3. Именованные аргументы и значения по умолчанию
Значения по умолчанию — это те фишки, за которые я полюбил Kotlin больше всего. В Java, если у вас есть метод с 5 параметрами (хотя в лучших практиках рекомендуют не более трех), и вам нужно передать только 2, вы создаете перегрузку или передаете null. Kotlin решает это элегантно.
Представьте, что мы пишем функцию отправки уведомления:
fun sendNotification( userId: String, message: String, priority: Int = 1, // Значение по умолчанию isSilent: Boolean = false, // Значение по умолчанию retryCount: Int = 0 // Значение по умолчанию ) { println("Отправка $userId: $message (приоритет $priority, без звука: $isSilent)") }
Теперь, вызывая эту функцию, мы можем указывать только то, что нам нужно:
fun main() { // Используем все дефолты sendNotification("user123", "Привет!") // Меняем только приоритет, используя именованный аргумент sendNotification("user456", "Срочно!", priority = 5) // Меняем несколько параметров. Порядок больше не важен! sendNotification( userId = "user789", message = "Важное обновление", isSilent = true, priority = 10 ) }
Best Practice: В команде мы договорились всегда использовать именованные аргументы, если в функции больше 2–3 параметров одного типа (и если параметров более трех — то пересматриваем дизайн класса, минимизируя до двух). Это спасает от путаницы, когда вы видите sendNotification("id", "text", 5, true). Глядя на такой код, сразу не поймешь, что значат 5 и true. Именованные аргументы делают код самодокументируемым.
4. Стек вызовов (Call Stack): что происходит под капотом
Когда вы вызываете функцию из функции, Kotlin (как и Java) запоминает, куда вернуться. Это называется стек вызовов.
Давайте визуализируем это. Допустим, у нас есть три функции: main() вызывает calculate(), а calculate() вызывает multiply().

Для начинающего разработчика важно понимать: когда функция заканчивается (доходит до return или закрывающей скобки), все локальные переменные этой функции «умирают» (освобождаются). Это называется область видимости (scope).
5. Области видимости (Scopes): где живет переменная
Переменные живут ровно столько, сколько живет функция, в которой они объявлены.
fun outerFunction() { val x = 10 // x доступна только здесь fun innerFunction() { val y = 20 // y доступна только здесь // Внутренняя функция видит x из внешней println(x + y) } // Здесь y уже недоступна! innerFunction() // А вот эту вызвать можно } fun main() { // x и y здесь НЕдоступны outerFunction() }
Кейс из практики: Однажды стажер в нашей команде пытался использовать переменную, объявленную внутри if, во всей остальной функции. Компилятор выдавал ошибку «Unresolved reference». Мы объяснили ему про scope. Это базовая дисциплина, которая учит нас не «мусорить» в памяти и четко разделять, где какие данные нужны.
6. Функция main() — точка входа
Мы постоянно её используем, но не акцентируем внимание. main() — это дверь, через которую JVM входит в вашу программу. В Kotlin она может быть как с аргументами (для обработки параметров командной строки), так и без.
// Простейший вариант fun main() { println("Старт приложения") } // С аргументами (если вы запускаете программу через консоль: java -jar app.jar arg1 arg2) fun main(args: Array<String>) { println("Первый аргумент: ${args.getOrNull(0)}") }
7. Функциональная декомпозиция: как это работает в реальной команде
Теперь давайте соберем всё вместе и посмотрим, как мы пишем код в промышленной разработке. Мы не пишем «кирпичи», мы строим «систему».
Задача: Написать скрипт, который рассчитывает бонус сотруднику. Бонус зависит от стажа и должности.
Плохой подход (монолит):
Всё в main(): запрос данных, логика расчета, форматирование, вывод.
Хороший подход (декомпозиция):
Разбиваем на логические функции. Каждая делает одно дело.
package ru.otus import java.time.Year // 1. Чисто логика расчета fun calculateBonus(yearsOfService: Int, isManager: Boolean): Double { val baseBonus = if (yearsOfService > 5) 1000.0 else 500.0 val multiplier = if (isManager) 1.5 else 1.0 return baseBonus * multiplier } // 2. Форматирование результата для вывода fun formatBonusMessage(employeeName: String, bonus: Double): String { return "Сотрудник $employeeName, ваш бонус: %.2f рублей".format(bonus) } // 3. Функция для ввода данных (здесь мы оборачиваем работу с консолью) fun readEmployeeInfo(): Pair<String, Int> { println("Введите имя сотрудника:") val name = readln() println("Введите стаж (лет):") val experience = readln().toIntOrNull() ?: 0 return Pair(name, experience) } fun main() { // Точка входа только координирует работу других функций val (name, experience) = readEmployeeInfo() // Допустим, мы знаем, что это менеджер (в реальности мы бы это тоже где-то брали) val isManager = name.contains("Руководитель", ignoreCase = true) val bonus = calculateBonus(experience, isManager) val message = formatBonusMessage(name, bonus) println(message) }
Что мы получили?
Тестируемость: Я могу написать тест для
calculateBonus(3, true)и проверить, правильно ли считается бонус, без запуска всей программы.Переиспользование:
formatBonusMessageможно использовать в других местах приложения (например, при отправке email).Читаемость:
main()читается как инструкция: прочитали данные → посчитали бонус → вывели.
Заключение и путь вперёд
Сегодня мы разобрали, как Kotlin делает работу с функциями удобной и безопасной:
Декомпозиция — разбиваем «монолит» на кирпичи.
Аргументы по умолчанию и именованные параметры — пишем чистые вызовы.
Call Stack и Scope — понимаем, как живут данные.
Best Practice — одна функция решает одну задачу.
Это основа, на которой строится вся архитектура приложений. В следующих статьях мы поговорим о функциональном программировании (лямбды, функции высшего порядка) и перейдем к самому интересному — работе с коллекциями.
Весь код из этой статьи доступен в моём репозитории на GitHub
Если вы чувствуете, что вам нужна система, а не разрозненные статьи, если хотите научиться применять эти паттерны в реальных проектах (бэкенд, мультиплатформа) — приходите на курс «Kotlin‑разработчик. Базовый уровень» в OTUS. Мы разбираем не только синтаксис, но и архитектуру, корутины, многопоточность и другие темы, необходимые промышленному разработчику.
Серия статей «Kotlin для новичков»:
А если хочется сначала посмотреть, как это выглядит вживую — можно начать с открытых уроков:
28 апреля 20:00 «Контрактные тесты в Kotlin: как подружить фронт и бэкэнд».
Записаться6 мая 19:00 «Разработка проекта на Kotlin: коллаборация человека, архитектурных шаблонов и ИИ‑команды».
Записаться
Они дают хороший срез того, как базовые вещи вроде функций и декомпозиции превращаются в реальные инженерные решения.
