Расскажите о случае, когда особенности Kotlin повлияли на архитектурное решение, производительность или сопровождение проекта.
Особенности Kotlin (data class, sealed class, extension functions, корутины) регулярно влияют на архитектуру: sealed class делает конечные автоматы exhaustive, extension functions разделяют ответственность без наследования.
Как особенности Kotlin влияют на архитектуру
Kotlin — не просто «Java с синтаксическим сахаром». Ряд языковых особенностей напрямую определяет архитектурные решения в production-проектах.
Кейс 1: sealed class для конечных автоматов
В проекте на Ktor требовалось моделировать состояния платёжной транзакции. Java Enum не позволял нести данные. Kotlin sealed class решил задачу элегантно:
sealed class PaymentState {
object Pending : PaymentState()
data class Processing(val processorId: String) : PaymentState()
data class Success(val transactionId: String, val amount: Long) : PaymentState()
data class Failed(val code: String, val message: String) : PaymentState()
data class Refunded(val refundId: String) : PaymentState()
}
fun handleState(state: PaymentState): String = when (state) {
is PaymentState.Pending -> "Ожидание"
is PaymentState.Processing -> "Обработка через ${state.processorId}"
is PaymentState.Success -> "Успех: ${state.transactionId}"
is PaymentState.Failed -> "Ошибка: ${state.message}"
is PaymentState.Refunded -> "Возврат: ${state.refundId}"
// Компилятор требует обработки всех случаев — exhaustive when
}
Архитектурный эффект: добавление нового состояния компилятор отмечает как ошибку во всех when-блоках без else — полная безопасность при рефакторинге.
Кейс 2: extension functions вместо наследования
Нужно было добавить метод toDomain() к сгенерированным Protobuf-классам без изменения их кода:
// Protobuf-класс из другого модуля (изменить нельзя)
// class UserProto { val id: String; val email: String }
// Extension function в domain-модуле
fun UserProto.toDomain(): User = User(
id = UserId(this.id),
email = Email(this.email)
)
// Extension на коллекции
fun List<UserProto>.toDomain(): List<User> = map { it.toDomain() }
Это позволило держать маппинг в domain-слое без загрязнения Protobuf-классов или создания отдельных Mapper-классов.
Кейс 3: корутины и архитектура репозитория
Kotlin Coroutines изменили интерфейс репозитория: вместо RxJava Observable или callback — suspend functions и Flow:
interface UserRepository {
suspend fun getUser(id: UserId): User // одноразовый запрос
fun observeUser(id: UserId): Flow<User> // поток изменений
suspend fun updateUser(user: User): Unit
}
class UserRepositoryImpl(
private val api: UserApi,
private val dao: UserDao
) : UserRepository {
override suspend fun getUser(id: UserId): User =
withContext(Dispatchers.IO) {
dao.getUser(id.value) ?: api.fetchUser(id.value).also {
dao.insert(it)
}
}
override fun observeUser(id: UserId): Flow<User> =
dao.observeUser(id.value) // Room возвращает Flow
.map { it.toDomain() }
.flowOn(Dispatchers.IO)
}
Кейс 4: data class и иммутабельность
Иммутабельные data class упростили concurrent доступ к состоянию — не нужны locks для чтения:
@Immutable
data class AppConfig(
val apiUrl: String,
val timeoutMs: Long,
val retries: Int
)
// Обновление через copy() — потокобезопасно для чтения
val newConfig = config.copy(timeoutMs = 10_000L)
_config.value = newConfig // атомарное обновление StateFlow
Подводные камни
- data class copy() — поверхностное копирование; вложенные List/Map всё равно изменяемы.
- sealed class из разных модулей требует rebuild всех зависимых при добавлении subclass.
- Extension functions не имеют доступа к private-членам — иногда нужен refactoring для тестируемости.
- companion object инициализируется при первом обращении к классу — lazy инициализация с побочными эффектами может удивить.
- Flow без flowOn(Dispatchers.IO) выполняется на вызывающем диспетчере — блокирует Main поток.
- when без else для не-sealed типов компилируется без ошибки — exhaustive проверка только для sealed.
- operator fun invoke() на object может сбить с толку — выглядит как функция, является объектом.
What hurts your answer
- Выдумывать опыт или говорить слишком общими фразами
- Не объяснять свою личную роль в работе с Kotlin
- Не показывать результат, метрики или извлечённые уроки
What they're listening for
- Может подготовить честный пример использования Kotlin
- Показывает свою роль, решения и результат
- Умеет рефлексировать над trade-offs и уроками