Версионирование API критически важно для мобильных приложений, где пользователи не всегда обновляются до последней версии. В то время как контрактное тестирование API обеспечивает совместимость между frontend и backend, стратегия версионирования обрабатывает сложность поддержки нескольких версий клиентов в production. Это руководство охватывает стратегии версионирования, обратную совместимость, принудительные обновления и тестирование нескольких версий API одновременно.

Стратегии Версионирования

Версионирование через URL

val BASE_URL_V1 = "https://api.example.com/v1/"
val BASE_URL_V2 = "https://api.example.com/v2/"

interface ApiServiceV1 {
    @GET("users/{id}")
    suspend fun getUser(@Path("id") id: String): UserV1
}

Версионирование через Header

class VersionInterceptor(private val apiVersion: String) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request().newBuilder()
            .addHeader("API-Version", apiVersion)
            .build()
        return chain.proceed(request)
    }
}

Тестирование Обратной Совместимости

Тестирование нескольких версий API требует полного покрытия всех поддерживаемых версий клиентов. Как обсуждается в мастерстве тестирования API, хорошо структурированный набор тестов валидирует как функциональные, так и нефункциональные требования.

class MultiVersionApiTest {

    @Test
    fun testUserEndpointV1() = runTest {
        val apiV1 = createApiClient(version = "v1")
        val user = apiV1.getUser("123")

        assertNotNull(user.id)
        assertNotNull(user.name)
    }

    @Test
    fun testUserEndpointV2() = runTest {
        val apiV2 = createApiClient(version = "v2")
        val user = apiV2.getUser("123")

        assertNotNull(user.fullName) // переименовано из 'name'
        assertNotNull(user.phoneNumber) // новое поле
    }
}

Стратегия Принудительного Обновления

data class VersionResponse(
    val minimumVersion: String,
    val latestVersion: String,
    val forceUpdate: Boolean,
    val message: String
)

class VersionChecker(private val versionService: VersionCheckService) {
    suspend fun checkForUpdate(): UpdateStatus {
        val response = versionService.checkVersion("android", BuildConfig.VERSION_NAME)

        return when {
            response.forceUpdate -> UpdateStatus.ForceUpdate(response.message)
            isNewerVersion(BuildConfig.VERSION_NAME, response.latestVersion) ->
                UpdateStatus.OptionalUpdate(response.latestVersion)
            else -> UpdateStatus.UpToDate
        }
    }
}

@Test
fun testForceUpdateDetection() = runTest {
    mockService.respondWith(VersionResponse(
        minimumVersion = "2.0.0",
        forceUpdate = true,
        message = "Пожалуйста, обновите для продолжения"
    ))

    val status = VersionChecker(mockService).checkForUpdate()
    assertTrue(status is UpdateStatus.ForceUpdate)
}

Graceful Деградация

class FeatureManager(private val apiVersion: String) {
    fun isFeatureAvailable(feature: Feature): Boolean {
        return when (feature) {
            Feature.USER_PREFERENCES -> apiVersion >= "2.0"
            Feature.PUSH_NOTIFICATIONS -> apiVersion >= "1.5"
            else -> true
        }
    }
}

@Test
fun testFeatureAvailability() {
    val managerV1 = FeatureManager("1.0")
    assertFalse(managerV1.isFeatureAvailable(Feature.USER_PREFERENCES))

    val managerV2 = FeatureManager("2.0")
    assertTrue(managerV2.isFeatureAvailable(Feature.USER_PREFERENCES))
}

A/B Тестирование Разных Версий API

A/B тестирование версий API в мобильных приложениях позволяет постепенный rollout и сравнение производительности перед полным развертыванием.

class ApiVersionSelector(private val userId: String, private val rolloutPercentage: Int) {
    fun selectApiVersion(): String {
        val userHash = userId.hashCode().absoluteValue
        val bucket = userHash % 100

        return if (bucket < rolloutPercentage) "v2" else "v1"
    }
}

@Test
fun testGradualRollout() {
    val versions = (1..1000).map {
        ApiVersionSelector("user$it", 20).selectApiVersion()
    }

    val v2Count = versions.count { it == "v2" }
    val percentage = (v2Count.toDouble() / versions.size) * 100

    assertTrue(percentage in 15.0..25.0) // Приблизительно 20%
}

Стратегия Deprecation

В микросервисных архитектурах отслеживание метрик между версиями API становится ещё более критичным для выявления узких мест производительности и проблем совместимости.

class DeprecationMonitor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val response = chain.proceed(chain.request())

        val deprecationWarning = response.header("Deprecation-Warning")

        if (deprecationWarning != null) {
            log.warn("API Deprecation: $deprecationWarning")
            analytics.logEvent("api_deprecation_warning")
        }

        return response
    }
}

Лучшие Практики

  1. Версия в URL для мажорных изменений - Ясно, дружелюбно к кэшу
  2. Используйте headers для минорных изменений - Гибко, обратно совместимо
  3. Всегда поддерживайте N-1 версии - Дайте пользователям время обновиться
  4. Реализуйте механизм принудительного обновления - Критические исправления
  5. Используйте feature flags - Постепенный rollout, A/B тестирование
  6. Мониторьте распределение версий - Знать, когда deprecate старые версии

Заключение

Версионирование API для мобильных требует:

  • Стратегию multi-version тестирования
  • Слои обратной совместимости
  • Механизмы принудительного обновления
  • Graceful деградацию
  • Инфраструктуру A/B тестирования
  • Чёткие timelines deprecation

Правильное версионирование обеспечивает плавные переходы при поддержании существующих пользователей, балансируя инновации со стабильностью.