JavaRush /Курсы /Kotlin SELF /package и папки: ка...

package и папки: как код получает “адрес” в проекте

Kotlin SELF
17 уровень , 0 лекция
Открыта

1. Введение

Когда программа маленькая, соблазн хранить всё в одном .kt файле огромен. Это как “одна коробка для всего”: и носки, и зарядки, и документы — пока коробка маленькая, вроде даже удобно. Но как только код растёт, коробка превращается в чёрную дыру: вы точно помните, что где-то была функция readIntOrNull(), но где именно — уже нет.

Пакеты (package) в Kotlin — это способ дать коду понятный “адрес”, чтобы вы (и IDE) могли быстро находить нужные функции, а одинаковые имена не дрались друг с другом за право жить в одном пространстве.

Важно сразу уловить ключевую мысль дня:

package — это “пространство имён” (namespace), логическая группировка кода.
Папки в проекте — это уже “физическая организация” файлов на диске. Обычно они согласованы, но это разные вещи.

2. Что такое package: пространство имён и “адрес” объявлений

Если смотреть на Kotlin‑файл как на письмо, то package — это строка “куда доставить”. В начале файла вы пишете:

package app

И всё, что объявлено в этом файле (функции, константы и т.д.), начинает “жить” по адресу app.

Минимальный пример: файл в пакете app

Давайте представим, что мы делаем консольную игру “Угадай число” (из мини‑проекта). Пока всё простое, у нас может быть один файл Main.kt:

package app

fun main() {
    println("Игра началась!") // Игра началась!
}

Теперь main формально имеет “полное имя” (fully qualified name): app.main. Обычно его так не пишут, но мыслить этим полезно.

Один файл — один package

В Kotlin package задаётся для файла целиком. Вы не можете в одном .kt файле объявить половину функций в app, а половину в app.util. Если нужно другое пространство имён — создаём другой файл.

Top-level объявления “принадлежат” пакету

Вы уже видели, что переменные и функции в Kotlin можно объявлять на верхнем уровне файла (вне классов). Так вот: такие объявления тоже живут в пакете файла. Пакет — это “дом” для объявлений, а файл — скорее “контейнер/лист бумаги”, где вы эти объявления записали.

3. Полное имя: как Kotlin различает одинаковые имена

Когда проект растёт, вы почти неизбежно напишете две функции с одинаковым “коротким” именем. Например, format() — отличное имя, и его хочется использовать в разных местах.

С точки зрения Kotlin, уникальность имени обеспечивается не только “именем функции”, а сочетанием:

package + имя

(и ещё сигнатура, но это уже отдельная история).

Две одинаковые функции в разных пакетах — нормально

Файл 1: src/main/kotlin/app/util/format.kt

package app.util

fun formatScore(score: Int): String {
    return "Score: $score"
}

Файл 2: src/main/kotlin/app/cli/format.kt

package app.cli

fun formatPrompt(text: String): String {
    return ">> $text"
}

Обе функции называются по-разному (formatScore и formatPrompt), но даже если бы они обе назывались просто format, Kotlin всё равно мог бы их различить по пакетам. Проблема начнётся не в “существовании”, а в “использовании” — когда вы попытаетесь вызвать format() из третьего места. Это мы будем аккуратно решать через import в следующей лекции, а сегодня нам важно понять сам принцип адресации.

Важный нюанс: файл не является областью видимости для разрешения имён

Этот момент звучит как фраза из документа про компилятор, но он реально практичный. Kotlin (и IDE) стараются сделать так, чтобы перенос функции в другой файл того же пакета не ломал код только потому, что “она теперь в другом файле”. В спецификации разрешения имён прямо обсуждается идея, что “нет отдельной области видимости файла” — важен пакет, а не конкретный .kt.

Перевод на человеческий: файл — это не “владелец” функции. Владелец — пакет. Поэтому пакет можно мыслить как “папку в телефонной книге”: не важно, на какой страничке записан номер, важно, в какой категории он лежит.

4. Папки и package: договорённость, которая спасает нервы

Сейчас будет важная практическая часть: как принято организовывать проект на Kotlin/JVM.

В Gradle‑проекте (а у большинства из вас он именно такой) код обычно лежит здесь:

src/main/kotlin/

И дальше начинается самое интересное: внутри src/main/kotlin обычно делают папки, которые повторяют package.

Соответствие “точки ↔ папки”

Есть простое правило‑ассоциация:

В package На диске
app
app/
app.util
app/util/
app.cli
app/cli/
com.example.myapp
com/example/myapp/

То есть:

  • точки в package превращаются в “папки”;
  • порядок папок повторяет “адрес”.

Например, если вы пишете:

package app.util

то файл обычно лежит по пути:

src/main/kotlin/app/util/ИмяФайла.kt

Почему это именно “договорённость”, а не жёсткий закон

Компилятор Kotlin технически может собрать проект, где файл лежит в “не той” папке (особенно если вы вручную настроили исходные директории). Но IDE и люди будут страдать.

IntelliJ IDEA умеет подсвечивать проблему примерно в стиле Package directive doesn't match file location. И это тот редкий случай, когда IDE не занудствует, а реально пытается спасти вас от будущего хаоса.

Аналогия: адрес и склад

package — это “адрес на конверте” (как вас найти логически). Папки — это “расположение коробок на складе” (как это хранится физически).

Если адрес и склад совпадают — вы счастливы. Если не совпадают — вы всё ещё можете найти коробку, но будете делать это как герой квеста “принеси артефакт из подвала, но карта у тебя от другой игры”.

5. Практика на уровне проекта: разнос игры “Угадай число” по пакетам

Сейчас мы не будем добавлять новые языковые конструкции. Мы возьмём уже знакомые функции и просто разложим их по смысловым “адресам”.

Представим, что у нас есть логика:

  • ввод числа,
  • генерация секрета,
  • основной игровой цикл,
  • печать сообщений пользователю.

Если держать всё в одном файле Main.kt, он быстро раздуется. Поэтому разделим на пакеты:

  • app — точка входа и сборка сценария (то есть main)
  • app.game — логика игры
  • app.io — ввод и валидация (утилиты чтения)
  • app.util — мелкие помощники форматирования

Нарисуем это как “карту адресов”:

flowchart TD
    A["package app
Main.kt"] --> B["package app.game
Game.kt"] A --> C["package app.io
Input.kt"] A --> D["package app.util
Text.kt"]

app: точка входа

Файл: src/main/kotlin/app/Main.kt

package app

fun main() {
    println("Добро пожаловать в 'Угадай число'!") // Добро пожаловать в 'Угадай число'!
}

Пока тут мало. Но идея такая: main — это “дирижёр”. Он вызывает функции из других пакетов и соединяет сценарий в одно приложение.

app.util: нормализация текста

Файл: src/main/kotlin/app/util/Text.kt

package app.util

fun normalizeCommand(raw: String): String {
    return raw.trim().lowercase()
}

Эта функция маленькая, но очень “пакетная”: она не про игру, не про ввод чисел, а про обработку строк в целом. Значит, ей логично жить в util.

app.io: чтение числа “по-человечески”

Файл: src/main/kotlin/app/io/Input.kt

package app.io

fun readIntOrNull(prompt: String): Int? {
    print(prompt)
    val raw = readln().trim()
    return raw.toIntOrNull()
}

Обратите внимание: здесь нет никакой магии. Это та же логика, которую вы писали раньше, просто она теперь в отдельном файле и с понятным адресом app.io.readIntOrNull.

app.game: генерация секрета

Файл: src/main/kotlin/app/game/Secret.kt

package app.game

import kotlin.random.Random

fun generateSecret(from: Int, until: Int): Int {
    return Random.nextInt(from, until)
}

Тут мы снова используем уже знакомую тему Random (вы её проходили раньше). Важно, что логика, относящаяся к игре, начинает жить в app.game, и это делает проект читабельнее: вы прямо по пути файла понимаете, “о чём он”.

6. Дисциплина пакетов: default package и именование

Пакет по умолчанию и почему он почти всегда плохая идея

Иногда вы видите файл без строки package в начале. Это означает, что файл находится в пакете по умолчанию (default package).

Технически это работает. Практически — это как “жить без адреса”: курьер всё равно может вам позвонить и спросить “ну вы где-то возле подъезда, да?”, но нормальная жизнь так не строится.

Основные проблемы default package обычно проявляются не сразу. Сначала всё хорошо: один файл, второй файл, третий… А потом вы внезапно хотите:

  • разделить код на подпакеты,
  • переиспользовать функции в другом проекте,
  • понять, где какая ответственность,
  • избежать конфликтов имён.

И вот тут отсутствие пакетов начинает мстить.

Хорошее правило для учебных и маленьких проектов: как только у вас больше одного файла — заведите нормальный package, хотя бы package app.
Да и для одного файла это часто полезно: вы привыкаете к дисциплине и меньше страдаете при росте проекта.

Нюансы именования пакетов

В индустрии есть общие соглашения:

Имена пакетов пишутся в нижнем регистре, обычно через точки, например app.util или com.example.myapp. Такая форма удобна и читаема, и она почти не конфликтует с именами классов/функций.

Если очень хочется назвать пакет MyCoolStuff, остановитесь на секунду и представьте, как вы будете жить с этим через полгода. Скорее всего, вы всё равно придёте к mycoolstuff или my.cool.stuff. Kotlin‑код читается много раз, пишется один раз — поэтому лучше сразу выбрать вариант, который не раздражает глаз.

Ещё один момент: пакет — это логическая группировка. Не пытайтесь сделать “пакет на каждую функцию”. Это как в супермаркете: если у вас отдельная полка для каждого батона, вы не упорядочили магазин, вы построили музей батонов.

7. Типичные ошибки при работе с package и папками

Ошибка №1: package не соответствует расположению файла в src.
Это самая распространённая история: файл лежит в src/main/kotlin/app/util, а в начале написано package app.utils (лишняя s) или вообще package app. Формально вы можете даже какое-то время жить так, но IDE будет путаться, навигация станет странной, а проект начнёт напоминать шкаф, где футболки лежат в ящике “ложки”.

Ошибка №2: всё складывается в один пакет “потому что так проще”.
Пакет app быстро превращается в “пакет‑мусорку”, где живёт и ввод, и логика игры, и форматирование. Это сначала кажется удобным, а потом вы боитесь открывать автодополнение, потому что там 30 функций с похожими именами. Лучше добавлять подпакеты по смыслу: app.io, app.game, app.util.

Ошибка №3: ожидание, что “один файл = один пакет”.
Наоборот: один пакет почти всегда состоит из множества файлов. Пакет — логическая область, а файл — просто место, где вы записали часть объявлений. Важно привыкнуть мыслить пакетами, а не файлами, иначе вы будете “искать функцию по имени файла” — это не самая приятная форма археологии.

Ошибка №4: оставлять файлы в default package “пока проект маленький”.
Это ловушка. Проект маленький ровно до тех пор, пока вы не добавили второй файл и не захотели чуть-чуть порядка. А дальше начинается миграция: перенос файлов, починка ссылок, переезд директорий. Гораздо дешевле завести нормальный пакет сразу, даже если он один.

Ошибка №5: странные имена пакетов (пробелы, верхний регистр, “как попало”).
Технически некоторые вещи могут даже собраться (до первого серьёзного конфликта), но читаемость и культура проекта падают. Нижний регистр и понятные слова — скучно, зато работает. Как и большинство хороших инженерных решений.

Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ