Введение: Почему «чистый код» — это не про эстетику
Когда вы только начинаете писать на 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