KotlinSeniorExperience

Как понять, что команда использует Kotlin идиоматично, а не переносит привычки из другого языка?

Идиоматичный Kotlin виден в коде: extension functions вместо utility-классов, sealed/data classes вместо if-instanceof, корутины с правильным scope, отсутствие !! в production-коде.

Сигналы идиоматичного Kotlin

1. API-дизайн с extension functions

Команда не создаёт XxxUtils и XxxHelper классы — вместо этого расширяет существующие типы:

// Не идиоматично: Utils-класс
object DateUtils {
    fun formatForDisplay(date: LocalDate): String = date.format(DateTimeFormatter.ISO_LOCAL_DATE)
}

// Идиоматично: extension function на типе
fun LocalDate.toDisplayString(): String = format(DateTimeFormatter.ISO_LOCAL_DATE)
fun LocalDate.isWeekend(): Boolean = dayOfWeek in setOf(DayOfWeek.SATURDAY, DayOfWeek.SUNDAY)

2. Sealed classes вместо enum + when-else

// Не идиоматично: String/enum с magic values
enum class Status { LOADING, SUCCESS, ERROR }
var errorMessage: String? = null  // отдельное поле, связь неявная

// Идиоматично: sealed class несёт данные
sealed class UiState {
    object Loading : UiState()
    data class Success(val jobs: List<Job>) : UiState()
    data class Error(val message: String, val retryable: Boolean) : UiState()
}

// Компилятор проверяет exhaustiveness:
when (state) {
    is UiState.Loading -> showSpinner()
    is UiState.Success -> showJobs(state.jobs)
    is UiState.Error -> showError(state.message)
}

3. Scope functions по назначению

// Не идиоматично: цепочка временных переменных
val builder = AlertDialog.Builder(context)
builder.setTitle("Confirm")
builder.setMessage("Delete?")
builder.show()

// Идиоматично: apply для конфигурации с side effect
AlertDialog.Builder(context).apply {
    setTitle("Confirm")
    setMessage("Delete?")
}.show()

// let — для nullable pipeline
user?.email?.let { sendNotification(it) }

// run — для вычисления значения в блоке
val greeting = run {
    val hour = LocalTime.now().hour
    if (hour < 12) "Good morning" else "Good afternoon"
}

4. Корутины с правильным scope и cancellation

// Не идиоматично: GlobalScope, игнорирование отмены
GlobalScope.launch { loadData() }

// Идиоматично: scope привязан к lifecycle, отмена автоматическая
class JobViewModel : ViewModel() {
    private val _jobs = MutableStateFlow<UiState>(UiState.Loading)
    val jobs: StateFlow<UiState> = _jobs.asStateFlow()

    init {
        viewModelScope.launch {
            repository.getJobs()
                .catch { e -> _jobs.value = UiState.Error(e.message ?: "Unknown", true) }
                .collect { _jobs.value = UiState.Success(it) }
        }
    }
}

5. Отсутствие !! в production-коде

Grep по !! в production-коде — быстрый аудит. Исключение: тесты, где NPE — корректный сигнал о провале.

6. Иммутабельные data classes с copy()

// Не идиоматично: mutable state
class Job {
    var title: String = ""
    var salary: Int = 0
}

// Идиоматично: val + copy для изменений
data class Job(val title: String, val salary: Int)
val updated = job.copy(salary = job.salary + 1000)

Подводные камни при аудите команды

  • Наличие run { ... } как замена if-блока без явной причины — overengineering, признак неопытности.
  • Везде apply вместо also/let — команда не различает receiver-based и argument-based scope functions.
  • Data class с var полями — нарушение immutability, часто перенесено из Java.
  • Отсутствие flowOn в репозитории и явный withContext в ViewModel — нарушение разделения ответственности.
  • Companion object с тяжёлой логикой вместо top-level функций — Java-static мышление.
  • when с else -> throw для sealed class — компилятор уже проверяет exhaustiveness, else избыточен.
  • Использование @JvmStatic и @JvmField там, где нет Java-interop — признак Java-to-Kotlin механического перевода.
  • Лямбды с многострочным телом вместо вынесенной именованной функции снижают читаемость — идиоматичный Kotlin не боится named functions.

What hurts your answer

  • Сразу обвинять Kotlin, не проверив соседние слои системы
  • Чинить симптом без минимального воспроизведения и evidence
  • Не учитывать версии, конфигурацию, окружение и recent changes

What they're listening for

  • Умеет локализовать проблему вокруг Kotlin
  • Двигается от симптома к гипотезам и проверкам
  • Отличает баг инструмента от ошибки использования или окружения

Related topics