expect/actual против интерфейсов и DI
Два способа протащить платформенную деталь в общий код — и как выбрать правильный.
Инверсия зависимостей в KMP — приём, при котором общий код зависит от интерфейса, а платформенная реализация передаётся ему извне через конструктор или DI-контейнер.
Две техники для одной задачи
И expect/actual, и «интерфейс + DI» решают одну проблему: дать общему коду доступ к платформенному поведению. Разница — в связывании. expect/actual связывается компилятором статически: одна expect-функция — ровно одна actual на платформу. Интерфейс связывается в рантайме: вы можете подставить любую реализацию, включая моки в тестах.
Когда уместен expect/actual
Берите его для мелких, единичных, не подменяемых вещей: имя платформы, текущее время, генерация UUID, путь к директории кэша. Здесь не нужна подмена реализации, и expect-функция лаконичнее, чем заводить интерфейс, реализацию и DI-проводку.
// commonMain
expect fun currentTimeMillis(): Long
expect fun randomUuid(): StringКогда уместен интерфейс + DI
Берите интерфейс, когда: реализация большая (база данных, сетевой клиент), нужна подмена в тестах, или у зависимости есть собственная конфигурация. Интерфейс позволяет в тесте передать фейк без всякого Kotlin/Native.
// commonMain
interface AnalyticsTracker {
fun track(event: String, params: Map<String, String>)
}
class OrderViewModel(private val analytics: AnalyticsTracker) {
fun onPay() {
analytics.track("pay_clicked", mapOf("screen" to "cart"))
}
}В тесте подставляем FakeAnalyticsTracker, в проде — платформенную реализацию.
Как работает под капотом
Тестируемость — главный аргумент за интерфейсы. expect/actual жёстко прибивает реализацию к платформе на этапе компиляции; чтобы протестировать общий код, использующий expect, вам нужна actual в тестовом наборе — это возможно, но громоздко. Интерфейс же подменяется одной строкой. Поэтому зрелые KMP-проекты используют expect/actual точечно (системные мелочи) и строят основную архитектуру на интерфейсах с DI.
Частые ошибки
Сделать всё на expect/actual и упереться в нетестируемость общего кода. Или наоборот — завести интерфейс ради одной системной константы, раздув проект. Правило: подменяемое и крупное — интерфейс; единичное и системное — expect/actual.
Итоги
expect/actual— статическое связывание; интерфейс + DI — рантайм-связывание.expect/actualхорош для мелких системных вещей без подмены.- Интерфейс нужен для крупных зависимостей и тестируемости.
- Зрелые проекты комбинируют оба подхода осознанно.