Avatar ·

Аналог модификатора доступа package-private в Kotlin

Привет всем! Я недавно перешел с Java на Kotlin и столкнулся с проблемой. В Java я часто использовал модификатор доступа package-private (по умолчанию, когда не указываешь public/private/protected) для того, чтобы классы внутри одного пакета могли обращаться к методам и полям друг друга, но при этом эти члены были скрыты от внешнего мира. Это было очень удобно для организации внутренней логики модуля. В Kotlin такого модификатора нет. Есть `internal`, который работает на уровне модуля (все файлы, скомпилированные вместе), но это слишком широкая видимость для моих задач. Мне нужно ограничить видимость именно в пределах одного пакета (или, в терминах Kotlin, одного каталога/папки с исходниками). Вот пример из Java, который я хочу перенести на Kotlin: ```java // Файл: com/example/database/DatabaseHelper.java package com.example.database; class DatabaseHelper { // package-private класс void connect() { // package-private метод System.out.println("Connecting..."); } } ``` ```java // Файл: com/example/database/UserRepository.java package com.example.database; public class UserRepository { public void init() { new DatabaseHelper().connect(); // Доступно, так как в одном пакете } } ``` В Kotlin я не могу просто убрать модификатор, так как по умолчанию стоит `public`. Пробовал использовать `internal`, но тогда `DatabaseHelper` становится доступным из любого другого модуля в проекте, а мне это не нужно. Как правильно организовать код в Kotlin, чтобы добиться такого же уровня изоляции, как package-private в Java? Может быть, есть какой-то идиоматический способ или альтернативное решение? Буду благодарен за примеры кода. <<>>

Почему в Kotlin нет прямого аналога package-private

Kotlin сознательно отказался от модификатора package-private в пользу более строгой системы видимости. В Java пакеты — это не только логическая, но и физическая структура (каталоги). В Kotlin пакеты — чисто логическое понятие, файлы могут лежать в разных папках, но принадлежать одному пакету. Поэтому полагаться на файловую структуру для контроля доступа — ненадёжно. Вместо этого Kotlin предлагает internal (видимость в пределах модуля) и более продвинутые техники.

Основное решение: используйте `internal` + модульную структуру

Самый правильный и идиоматичный способ — разбить ваш проект на несколько Gradle-модулей. Например:

  • Модуль `:database` — содержит все классы, связанные с БД (ваш аналог пакета).
  • Модуль `:app` — основное приложение, которое зависит от `:database`.

Внутри модуля `:database` вы помечаете классы и функции как internal. Они будут видны всем классам внутри этого модуля, но абсолютно недоступны из модуля `:app`.

// Модуль: database
// Файл: DatabaseHelper.kt
package com.example.database

internal class DatabaseHelper { // Виден только внутри модуля ':database'
    internal fun connect() {
        println("Connecting...")
    }
}

// Файл: UserRepository.kt (тоже в модуле ':database')
package com.example.database

class UserRepository {
    fun init() {
        DatabaseHelper().connect() // Доступно, так как в одном модуле
    }
}
// Модуль: app
// Файл: Main.kt
package com.example.app

import com.example.database.UserRepository

fun main() {
    UserRepository().init() // Работает
    // DatabaseHelper() // Ошибка компиляции! Класс internal и недоступен из другого модуля
}

Этот подход полностью решает вашу задачу: вы получаете изоляцию, даже более строгую, чем package-private (так как модуль обычно больше одного пакета, но в вашем случае модуль может содержать ровно один пакет).

Альтернатива: приватные расширения и соглашения

Если вы не хотите или не можете разбивать проект на модули, можно использовать соглашения о нейминге и приватные функции-расширения. Например, положите все "внутренние" классы в один файл и используйте private:

// Файл: DatabaseInternals.kt
package com.example.database

// Класс с приватным конструктором — доступен только внутри этого файла
class DatabaseHelper private constructor() {
    fun connect() { /* ... */ }

    companion object {
        fun create(): DatabaseHelper = DatabaseHelper()
    }
}

// Публичный класс, который использует DatabaseHelper
class UserRepository {
    private val helper = DatabaseHelper.create()

    fun init() {
        helper.connect()
    }
}

Минус: все классы, которые должны видеть друг друга, приходится держать в одном файле. Для больших проектов это неудобно.

Самый гибкий вариант: использовать аннотации или sealed классы

Для сложной логики можно использовать sealed class или sealed interface. Они ограничивают иерархию наследования определённым файлом или пакетом (в Kotlin 1.9+ — подпакетами). Это не даёт прямого контроля доступа к методам, но ограничивает создание подклассов, что часто решает ту же задачу.

// Файл: DatabaseOperation.kt
package com.example.database

sealed class DatabaseOperation {
    abstract fun execute()
}

class ConnectOperation : DatabaseOperation() {
    override fun execute() { /* connect */ }
}

// В другом файле того же пакета:
class UserRepository {
    fun performOperation(op: DatabaseOperation) {
        op.execute() // Работает, так как ConnectOperation доступен
    }
}

Итог: Для 99% случаев используйте internal + модули. Это стандарт в Android-разработке и Kotlin-проектах. Остальные варианты — для специфических ситуаций.

<<>>

Дополнение: Использование `@PublishedApi` и `@RequiresOptIn`

Если вам нужно, чтобы какой-то внутренний API был доступен только для определённых потребителей (например, для тестов в том же модуле), можно комбинировать internal с аннотациями-маркерами.

Создайте свою аннотацию, помеченную @RequiresOptIn:

// Файл: InternalApi.kt
@RequiresOptIn(
    message = "Этот API предназначен только для внутреннего использования в модуле database",
    level = RequiresOptIn.Level.ERROR
)
@Retention(AnnotationRetention.BINARY)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
annotation class InternalDatabaseApi

Теперь пометьте внутренние классы этой аннотацией:

@InternalDatabaseApi
internal class DatabaseHelper {
    fun connect() { /* ... */ }
}

В месте использования (даже внутри того же модуля) компилятор потребует явного разрешения через @OptIn(InternalDatabaseApi::class):

class UserRepository {
    @OptIn(InternalDatabaseApi::class)
    fun init() {
        DatabaseHelper().connect() // Работает только с OptIn
    }
}

Этот подход не делает API полностью package-private, но добавляет "соглашение" и предупреждение для разработчиков: "Это внутренняя кухня, используй на свой страх и риск". Полезно для больших команд или библиотек.

Минус: Любой разработчик всё равно может поставить @OptIn и получить доступ. Это не защита на уровне компиляции, а лишь дисциплинарная мера.