Версионирование 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
}
}
Лучшие Практики
- Версия в URL для мажорных изменений - Ясно, дружелюбно к кэшу
- Используйте headers для минорных изменений - Гибко, обратно совместимо
- Всегда поддерживайте N-1 версии - Дайте пользователям время обновиться
- Реализуйте механизм принудительного обновления - Критические исправления
- Используйте feature flags - Постепенный rollout, A/B тестирование
- Мониторьте распределение версий - Знать, когда deprecate старые версии
Заключение
Версионирование API для мобильных требует:
- Стратегию multi-version тестирования
- Слои обратной совместимости
- Механизмы принудительного обновления
- Graceful деградацию
- Инфраструктуру A/B тестирования
- Чёткие timelines deprecation
Правильное версионирование обеспечивает плавные переходы при поддержании существующих пользователей, балансируя инновации со стабильностью.