Mobil Uygulama API Geliştiricileri için Backward Compatibility

Arif Acar
6 min readJan 13, 2021

--

Mobil uygulamalar için web servis geliştiricilerin korkulu rüyalarından biri de backward compatibility yani geriye dönük uyumluluk. Hadi bu konu üzerine biraz kafa yoralım…

görsel kaynağı: https://19yw4b240vb03ws8qm25h366-wpengine.netdna-ssl.com/wp-content/uploads/API-Change-Strategy-API-versioning.png

Günümüz mobil uygulamaları çoğunlukla rest API’lerle JSON formatındaki şemalar üzerinden haberleşmektedir. Bunun için backend servislerinin veri türleri ile hizmet alacak uygulamaların yani istemcilerin (Android, iOS) el sıkışmış olması gerekiyor. Mobil uygulamaların ilk sürümlerinde API’ler ile istemcilerin anlaşması konusunda pek sıkıntı yaşanmaz. Ancak uygulama store’a çıktıktan sonra ve yeni feature’lar geliştirildikçe artık işler backend servisleri için karmaşıklaşmaya başlar. API şemanızda yapacağınız en ufak değişiklikte istemcilerin eski sürümlerinin etkilenmemesi gerekiyor. Backward compatibility yani geriye dönük uyumluluk işte tam burada devreye giriyor.

Web uygulamalarda backward compatibility sorunlarını backend/frontend’i birlikte çıkarak kolaylıkla aşabiliyorsunuz. (Burada state ve cache’lerle alakalı farklı challange’lar var). Ancak bir mobil uygulamayı iOS App Store veya Google Play Store’a gönderdiğinizde bir inceleme sürecine dahil olursunuz ve bu bazen uzun sürebilir. Ayrıca uygulamanız o anda incelenip publish edilse bile güncelleme almayan ve eski sürümlerinizi kullanmaya devam eden çok fazla müşteriniz olacaktır. Güncellemelerin yayılması uygulamanızın bazı dinamiklerine bağlı olarak saatler hatta günler sürebilir. -Burada uygulamayı yayınlayan tarafların kararıyla belli yüzdeliklerle de yaygınlaşma sağlanabilir- Müşterileriniz uygulamanızın otomatik güncellemelerini açmamış da olabilir.

Backward compatibility size istemcilerin güncellenmesini beklemeden sunucularınızda rahat bir şekilde güncelleme yapabilme esnekliğini sağlar.

Örneğin bir JSON şemanızda bir veri alanın ismi isActive iken onu active olarak değiştirdiniz. Yeni mobil versiyonlarınız buna uyum sağlayabiliyorken eski versiyonlar servisinizde isActive‘i arayacak ve bulamadığında uygulama hataya düşecek. Bu hataya düşmeye terminolojide “BC breaks” denilmektedir.

BC Breaks hangi durumlarda oluşur?

  • Serviste bir endpoint’in silinmesi, isminin değiştirilmesi, url path’in değiştirilmesi veya HTTP status code’un değiştirilmesi.
  • Metod’ta yer alan bir parametrenin silinmesi, isminin değişmesi
  • Bir parametrenin daha önceden opsiyonel iken sonrasında required bir alana dönüştürülmesi
  • Response’un tamamen değiştirilmesi veya daha önce yapılan veri modelleme hatalarının düzeltilmesi gibi, liste uzar gider.

BC Breaks’e sebep olmayan smooth değişiklikler:

  • Yeni bir metodun eklenmesi
  • Bir metoda yeni bir parametre eklenmesi
  • Response’a yeni bir alan eklenmesi

Tabi bu smooth değişiklikler uygulama içerisinde farklı davranışlarda bulunabilir o yüzden servis tarafında bunları da handle etmek gerekir.

Nasıl Önleriz?

Diyelim ki bir path değişikliği yapacağız. Bu durumda servis tarafında bu metodun @deprecated işaretlenmiş olması ve yeni çağrılacak olan servise yönlenmesi gerekiyor.

Örneğin;user/saveUser ismindeki servisi user/create olarak değiştirelim.

/**
* This method is no longer acceptable after Android 1.4.2 and iOS 1.4.4
*
@param user
*
@return
*/
@Deprecated
@PostMapping(value = "/saveUser")
public GenericInfoResponse<User> saveUser(@RequestBody UserRequest userRequest) {
return create(userRequest);
}

@PostMapping(value = "/create")
public GenericInfoResponse<User> create(@RequestBody UserRequest userRequest) {
...
// create user operations
...
}

Bunun gibi HTTP status code değişikliği yaparken de aynısını yapabilirsiniz. Yukarıda görüldüğü gibi Android 1.4.2 ve iOS 1.4.4 versiyonlarından sonra user/saveUsermetodu kullanılmayacak. Haliyle mobil uygulamanız yeni versiyonlu bir apiye geçinceye kadar veya o servisi kullanan istemci versiyonu kalmayıncaya kadar bu metodun hiç bir şekilde silinmemesi gerekiyor. Uygulamanızda bu gibi force update bekleyen ve workaround çözümlerle aştığımız kısımların bir listesini tutabiliriz. Fırsatı geldiğinde kodun sürekliliğini ve temiz kalmasını sağlamak ilgili yerleri silmemiz gerekecektir.

Hazırladığımız bir API’de bir User ile One-to-one ilişki olan başka bir Address objesi düşünelim. Bu API’yi de kullanan versiyonların olduğunu varsayalım. Veri modelleme esnasında bir hata yaptık ve burada bir kullanıcının birden fazla adres bilgisi olabilirdi. Uygulama address objesinden adres bilgisini direkt ekranda gösteriyordu. Bu objeyi direkt refactor etmek yerine addressList isminde yeni bir One-to-many alan oluşturulur ve artık istemcilerin adresleri buradan alınması sağlanır. Eski obje bir yukarıda anlattığım işlemlere tâbi tutularak zamanı gelince silinir.

Yine değiştirdiğiniz parametre isimlerini de mükerrer birer key oluşturup benzer bir yaklaşım uygulayabilirsiniz. Yeni eklenen parametrelerde de istemciler o parametreler null olarak işaretlenecektir. Zaten içerde onunla alakalı bir işlem yapılmadığından uygulamanız sorunsuz bir şekilde yaşamına devam edecektir.

Her zaman şemanın geriye dönük uyumlu olması yeterli olmayabilir. Business katmanında da farklı versiyonlara göre farklı davranışlarda bulunulması gereken kısımlar olabilir. Bunun için eskiden çalışmasını istemediğiniz ancak yeni versiyonlar için aktif olmasını istediğiniz kısımları versiyon kontrolleri yaparak değiştirebilirsiniz.

if (Util.isVersionGreater("1.4.1", "1.4.3") {
// do something
}

Yine bu tarz workaround çözümleri de bir sonraki yeni versiyonlu API’lerde bulup tek tek temizlemeniz gerekecek.

Uygulamaya yeni feature’lar eklerken veya uygulama akışlarında değişikliklere giderken her zaman böyle basit workaround çözümler uygulamak yeterli olmayabilir. Bu durumda da REST API’lerinizi versiyonlamanız gerekecektir. Bunu ayrı bir başlıkta değerlendirelim.

API Versiyonlama

API’lerin versiyonlanması backward compability’i sağlama konusunda oldukça kolay bir yöntem gibi görünse de bakım ve gelişimi konusunda bizi zorlayacak safhalar içeriyor.

Yine bir örnek üzerinden gidelim. Yeni bir feature eklemeniz gerekiyor ve bu mobil uygulamanın akışlarını değiştiriyor. Var olan servisleri korumak ve yeni servislerle bu işi çözmek istiyorsunuz ama backend uygulamanızda business layer’da çok fazla değişiklik yapmanız gerekiyor. Geriye dönük uyumluluğu sağlamak için de için de yeni feature’lar için aynı business lojiği kopyalayıp yeni feature’lara uygulamakta çok doğru olmayacak. Konudunuz yönetilemeyecek şekilde karmaşıklaşacak. Bu noktada backend servislerinizi versiyonlamak çok doğru bir yaklaşım olacak.

Ancak her versiyonlamada o versiyon’a ait bir git branch’i tutmanız gerekecek. Canlıda kritik olarak belirtilmiş bir bug’ı hotfix yaparken tüm branch’lere ayrı ayrı uygulamanız gerekecek. 3 ayrı versiyona ait branch’iniz olduğunu düşündüğümüzde bir feature uygulanan hotfix diğer brancler için de ayrı ayrı cherry pick yapmanız gerekecek. Ayrıca master branch’inizde geliştirdiğiniz yeni iyileştirmeler ve performans geliştirmeleri gibi bazı kritik geliştirmeler eski sürümlerinizde yer almayacak.

diagrams.net ile çizildi

O anda API’nizin versiyonlama kararını biraz da sizin mobil uygulamanızın store’a çıkış sıklığı ve müşterilerinizin uygulamanızı güncelleme sıklığına göre değişebilir. Uygulama kullanım versiyonlarını takip ederek karar verdiğiniz bir yüzdeliğin altına düştüğünde az sayıda kalmış kullanıcılarınıza artık force update verebilirsiniz. Bu güncellemden sonra eski versiyonların path’ini ve ilgili branch’i silerek yolunuza devam edebilirsiniz. Tabii bu kararı almak da her zaman öyle kolay olmayabilir.

Bir havacılık uygulaması üzerinden örnek vereyim. Havacılık uygulamalarında müşterilerin büyük bir kısmı check-in işlemlerini mobil uygulamanız üzerinden yapmaya eğilimlidir. Acentadan ya da web arayüzünden bilet almış bir müşteriniz çok uzun zaman önce sizin havacılık uygulamanızı indirmiş olsun. Bu kullanıcı uçuşa giderken yolda check-in yapmak istiyor ve az zamanı kalmış. Uygulamayı açıyor ve o da ne! Artık bu uygulamanın güncellenmek zorunda olduğunu görüyor. Bu kullanıcı deneyimi açısından kötü bir tecrübe olacaktır.

BC Breaks’leri Yakalama

  • İstemcilerin şemalara uygun unit test yazılması

Tabi burada zorlayıcı bir durum var. API’ler sizin yeni versiyonlarınız için bir takım değişiklikler yaptığında sizi de yeni JSON şemasına göre unit testlerinizi düzeltmeniz ve geliştirmeniz gerekiyor. İstemciler de BC garantisi verilmiş bir servise eski versiyonların unit testleriyle de test edilmelidir. Burada istemcilerin de versiyonlama yönetiminin en az backend kadar önemli olduğunu görüyoruz.

Sunucu tarafında yapılacak değişikliklerin anında yakalanması için buna efor harcamak gelecekte çok hayat kurtaracaktır. iOS geliştiricileri için şöyle bir örnek bırakıyorum.

  • Otomasyon testleri;

Otomasyon testleri yazan ekibin mevcut testlerini BC garantisi verilen uygulama için de koşması gerekmektedir. Aynı şekilde bu otomasyon testlerini yazan ekibin de kendi test senaryolarını yeni versiyonlar için güncellemesi gerekecektir. Tıpkı unit testlerde olduğu gibi bu otomasyon testlerinin sürekliliğini sağlamak çok efor gerektirecektir. CI/CD sürecinde çalışacak otomasyon testleri ile oluşabilecek hatalar anında yakalanabilir. Buna bir örnek swagger-diff’e bir CI / CD pipeline’ı oluşturmak.

Tabii tüm bunların yanında yeni sürümlü istemcilerin sunucu tarafında da bir değişiklik yapmadan uygulamanın sürekliliğini sağlamakta mümkün. Burada da forwards compatibility yani ileriye yönelik uyumluluk devreye girmektedir. Burada da tam tersine sunucu versiyonunu yükseltmeye gerek kalmadan istemcilerin çıktıkları versiyonlarla kendisine göre eski kalan sunucu versiyonuna adapte olabilmelidir. Çok sağlıklı bir süreç olmasa da bazen sunucu tarafında deploy konularında hantal kalan durumlarda işimize yarayacaktır.

Şöyle bir örnek verelim. Bir servis yazdık ve servisi kullanan istemciler de START ve STOP enumlarını gönderiyor olsun. Backend’te de gelen enum’ları kontrol ederken şöyle bir yol izledik.

if (request.getAction == AppAction.START) {
// do something
} else if (request.getAction == AppAction.STOP) {
// do something
}
// keep going...

Bu bize ileriye yönelik bir uyumluluk sağlamamış olacak. Bu enumlara yeni bir CANCEL gibi bir enum eklendiğinde uygulama akışına devam edecek ve yukarıda if blokları arasında yapılması gereken bazı işlemler tamamlanmadığından uygulama hatalı çalışacaktır. Halbuki kullanıcıyı doğru bilgilendirmek ve hataya düşmesini engellemek için şöyle yapabilirdik.

if (request.getAction == AppAction.START) {
// do something
} else if (request.getAction == AppAction.STOP) {
// do something
} else {
// thow error or show message to UI
}

İstemci tarafında enum olarak tanımlanan parametreler için listede var olan bir öğe kaldırıldığında sunucunun buna ayak uydurabildiğini görebiliriz. Ancak yeni bir enum tipinde bir öğe eklendiğinde sunucu tarafındaki else case’ine düşecektir.

Kullanıcının yanlış bir akışa girip işleri berbat etmesindense kullanıcıyı doğru bilgilendirip akışı kesmek daha doğru olacaktır.

Sunucu ya da istemci tarafında bir değişiklik yaptığınızda hem ileri hem de geriye dönük uyumluysa, buna “Full Compatibility” yani tam uyumlu denmekte ve her iki tarafta da hiçbir şeyi bozmadan çalıştırabileceğiniz anlamına gelir. Yaptığınız değişiklikler ne ileri ne de geriye dönük uyumluluk sağlayamıyorsa buna da “Incompatibility” yani uyumsuzluk denmekte. Compatibility konularında daha fazla bilgi edinmek için şuraya göz atabilirsiniz.

Bir sonraki yazıda görüşmek üzere (:

--

--