Чистый код на Kotlin: правила, которые сэкономят вам нервы

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: Почему «чистый код» — это не про эстетику

Когда вы только начинаете писать на Kotlin, кажется, что сам язык уже решает половину проблем. Нет бойлерплейта, как в Java. Есть `data class`, лямбды, корутины. Но если просто накидать код «как пойдёт», через месяц вы сами не сможете в нём разобраться. А если над проектом работает команда — коллеги будут вас ненавидеть (или вы их).

Чистый код на Kotlin — это не про табы вместо пробелов. Это про то, как уменьшить когнитивную нагрузку, сделать логику прозрачной и избежать багов, которые прячутся в «удобных» конструкциях. В этой статье я покажу конкретные приёмы: от работы с nullable до использования scope functions и sealed классов. Без воды, только практика.

1. Null Safety: как не плодить «?» и «!!»

Kotlin гордится своей системой типов, но многие пишут так, будто это Java с «синтаксическим сахаром». Главный антипаттерн — цепочки `?.let` и панические `!!`.

Плохо (читаемость страдает):

user?.address?.city?.let { city ->
    println(city)
} ?: run {
    println("No city")
}

Хорошо (элегантно и чисто):

val city = user?.address?.city
println(city ?: "No city")

Используйте `!!` только если вы на 100% уверены, что значение не null (например, в тестах или после проверки в `if`). В продакшне — это граната. Если вам нужно преобразовать nullable в non-null и обработать оба случая — используйте when или элвис-оператор.

2. Scope Functions: `apply`, `run`, `with`, `let`, `also` — когда что?

Это мощный инструмент, но им часто злоупотребляют. Золотое правило: не вкладывайте scope functions друг в друга. Если у вас `list.apply { ... }.let { ... }` — вы пишете лапшу.

Правила выбора:

  • apply — для конфигурации объекта (возвращает сам объект). Идеально для билдеров.
  • let — для выполнения действия с non-null значением (часто в паре с `?.`).
  • run — когда нужно вычислить значение и вернуть результат.
  • with — для группировки вызовов одного объекта без возврата.
  • also — для побочных эффектов (логирование, валидация).

Пример чисто:

data class User(var name: String, var age: Int)

fun createUser(): User = User("Unknown", 0).apply {
    name = "Alice"
    age = 30
}

Не пишите user?.let { it.name } — это избыточно. Просто user?.name.

3. Extension Functions: не плодите утилитарные классы

В Java вы бы написали StringUtils.capitalize(str). В Kotlin — str.capitalize(). Extension functions — это способ «дописать» чужой класс, не наследуя его.

Пример полезного extension:

fun String.isValidEmail(): Boolean {
    return this.contains("@") && this.contains(".")
}

Но не переусердствуйте. Если extension-функция используется только в одном месте — возможно, она должна быть приватной. И не делайте extension на `Any` — это ломает систему типов.

4. Sealed Classes и When: ваш главный инструмент вместо if-else

Когда у вас есть конечное множество состояний (например, результат API-запроса), используйте sealed class. Это гарантирует, что вы обработали все варианты.

Правильный подход:

sealed class NetworkResult<out T> {
    data class Success<T>(val data: T) : NetworkResult<T>()
    data class Error(val message: String, val code: Int) : NetworkResult<Nothing>()
    object Loading : NetworkResult<Nothing>()
}

fun handle(result: NetworkResult<String>) {
    when (result) {
        is NetworkResult.Success -> println("Data: ${result.data}")
        is NetworkResult.Error -> println("Error: ${result.message}")
        NetworkResult.Loading -> println("Loading...")
    }
}

Компилятор проверит, что вы не забыли ни один вариант. Это чисто, безопасно и читаемо.

5. Data Class: не просто хранилище полей

data class генерирует equals, hashCode, toString, copy. Но многие пишут:

data class User(val name: String, val age: Int) {
    fun display() = "$name ($age)"
}

Это допустимо, но лучше вынести логику отображения в extension или отдельный слой. Data class должен быть тупым — только данные. Всё остальное — функции расширения или мапперы.

6. Иммутабельность: val > var

По умолчанию используйте val. var — только если состояние действительно должно меняться. Это снижает количество побочных эффектов и делает код предсказуемым.

// Плохо
var list = mutableListOf<Int>()
list.add(1)

// Хорошо
val list = listOf(1, 2, 3)

Если нужно изменять список — используйте MutableList, но старайтесь возвращать из функций неизменяемые коллекции.

7. Именование: говорите правду, но коротко

Имена переменных, функций и классов — это документация. Не экономьте буквы.

  • Плохо: val u: User
  • Хорошо: val currentUser: User
  • Плохо: fun proc()
  • Хорошо: fun processOrder()

Но не перегибайте: getUserByIdAndValidateAndSendEmail — это уже плохо. Разбивайте на маленькие функции.

8. Функции должны делать одну вещь

Это классика, но её постоянно нарушают. Если функция содержит слово «и» в описании — разбивайте.

// Плохо
fun saveUser(user: User) {
    validate(user)
    saveToDb(user)
    sendEmail(user)
}

// Хорошо
fun saveUser(user: User) {
    validate(user)
    saveToDb(user)
    notifyUser(user)
}

private fun notifyUser(user: User) {
    sendEmail(user)
}

Каждая функция — один уровень абстракции. Это облегчает тестирование и чтение.

9. Корутины: не забывайте про Structured Concurrency

Корутины — мощь, но их легко превратить в «ад колбэков». Используйте view

Блоги

Рекомендаций по книгам