Bagaimana Anda mengelola basis kode yang mendasari untuk API berversi?

105

Saya telah membaca tentang strategi pembuatan versi untuk ReST API, dan sesuatu yang tampaknya tidak ditangani oleh mereka adalah bagaimana Anda mengelola basis kode yang mendasarinya.

Katakanlah kita membuat banyak perubahan yang merusak pada API - misalnya, mengubah sumber daya Pelanggan kita sehingga mengembalikan bidang forenamedan surnamebukan satu namebidang. (Untuk contoh ini, saya akan menggunakan solusi pembuatan versi URL karena mudah untuk memahami konsep yang terlibat, tetapi pertanyaannya juga berlaku untuk negosiasi konten atau header HTTP kustom)

Kami sekarang memiliki titik akhir di http://api.mycompany.com/v1/customers/{id}, dan titik akhir lain yang tidak kompatibel di http://api.mycompany.com/v2/customers/{id}. Kami masih merilis perbaikan bug dan pembaruan keamanan untuk v1 API, tetapi pengembangan fitur baru sekarang semuanya berfokus pada v2. Bagaimana kita menulis, menguji dan menerapkan perubahan ke server API kita? Saya dapat melihat setidaknya dua solusi:

  • Gunakan cabang / tag kontrol sumber untuk basis kode v1. v1 dan v2 dikembangkan, dan diterapkan secara independen, dengan gabungan kontrol revisi yang digunakan seperlunya untuk menerapkan perbaikan bug yang sama ke kedua versi - serupa dengan cara Anda mengelola basis kode untuk aplikasi asli saat mengembangkan versi baru utama sambil tetap mendukung versi sebelumnya.

  • Buat basis kode itu sendiri mengetahui versi API, sehingga Anda mendapatkan satu basis kode yang menyertakan representasi pelanggan v1 dan representasi pelanggan v2. Perlakukan pembuatan versi sebagai bagian dari arsitektur solusi Anda, bukan masalah penerapan - mungkin menggunakan beberapa kombinasi namespace dan perutean untuk memastikan permintaan ditangani oleh versi yang benar.

Keuntungan nyata dari model cabang adalah bahwa mudah untuk menghapus versi API lama - cukup hentikan penerapan cabang / tag yang sesuai - tetapi jika Anda menjalankan beberapa versi, Anda bisa berakhir dengan struktur cabang dan pipeline penerapan yang sangat berbelit-belit. Model "basis kode terpadu" menghindari masalah ini, tetapi (menurut saya?) Akan jauh lebih sulit untuk menghapus sumber daya dan titik akhir yang tidak digunakan lagi dari basis kode saat tidak lagi diperlukan. Saya tahu ini mungkin subjektif karena tidak mungkin ada jawaban sederhana yang benar, tetapi saya ingin tahu bagaimana organisasi yang mengelola API kompleks di berbagai versi memecahkan masalah ini.

Dylan Beattie
sumber
41
Terima kasih telah menanyakan pertanyaan ini! SAYA TIDAK BISA percaya lebih banyak orang yang tidak menjawab pertanyaan ini !! Saya muak dan lelah dengan semua orang yang memiliki pendapat tentang bagaimana versi memasuki sistem, tetapi tampaknya tidak ada yang menangani masalah sulit yang sebenarnya dalam mengirimkan versi ke kode yang sesuai. Sekarang harus ada setidaknya serangkaian "pola" atau "solusi" yang diterima untuk masalah yang tampaknya umum ini. Ada banyak sekali pertanyaan tentang SO tentang "versi API". Memutuskan bagaimana menerima versi adalah FRIKKIN SIMPLE (relatif)! Menanganinya dalam basis kode setelah masuk, SULIT!
arijeet

Jawaban:

45

Saya telah menggunakan kedua strategi yang Anda sebutkan. Dari keduanya, saya menyukai pendekatan kedua, yang lebih sederhana, dalam kasus penggunaan yang mendukungnya. Artinya, jika kebutuhan pembuatan versi sederhana, gunakan desain perangkat lunak yang lebih sederhana:

  • Jumlah perubahan rendah, perubahan kompleksitas rendah, atau jadwal perubahan frekuensi rendah
  • Perubahan yang sebagian besar bersifat ortogonal terhadap basis kode lainnya: API publik dapat ada secara damai dengan tumpukan lainnya tanpa memerlukan "berlebihan" (untuk definisi apa pun dari istilah yang Anda pilih untuk diterapkan) bercabang dalam kode

Saya tidak merasa terlalu sulit untuk menghapus versi yang tidak digunakan lagi menggunakan model ini:

  • Cakupan pengujian yang baik berarti bahwa merobek API yang dihentikan dan kode pendukung terkait memastikan tidak ada regresi (baik, minimal)
  • Strategi penamaan yang baik (nama paket berversi API, atau lebih jelek, versi API dalam nama metode) memudahkan untuk menemukan kode yang relevan
  • Kekhawatiran lintas sektoral lebih sulit; modifikasi pada sistem backend inti untuk mendukung beberapa API harus dipertimbangkan dengan sangat hati-hati. Pada titik tertentu, biaya pembuatan versi backend (Lihat komentar "berlebihan" di atas) melebihi manfaat dari satu basis kode.

Pendekatan pertama tentu lebih sederhana dari sudut pandang mengurangi konflik antara versi yang sudah ada bersama, tetapi biaya pemeliharaan sistem terpisah cenderung lebih besar daripada manfaat mengurangi konflik versi. Meskipun demikian, sangat mudah untuk membuat tumpukan API publik baru dan mulai melakukan iterasi pada cabang API terpisah. Tentu saja, hilangnya generasi segera terjadi, dan cabang-cabang berubah menjadi kekacauan penggabungan, resolusi konflik gabungan, dan kesenangan lainnya.

Pendekatan ketiga ada pada lapisan arsitektural: mengadopsi varian dari pola Fasad, dan mengabstraksi API Anda menjadi lapisan berversi yang dihadapi publik yang berbicara ke instance Fasad yang sesuai, yang pada gilirannya berbicara ke backend melalui set API-nya sendiri. Fasad Anda (saya menggunakan Adaptor di proyek saya sebelumnya) menjadi paketnya sendiri, mandiri dan dapat diuji, dan memungkinkan Anda untuk memigrasi API frontend secara independen dari backend, dan satu sama lain.

Ini akan berfungsi jika versi API Anda cenderung menampilkan jenis sumber daya yang sama, tetapi dengan representasi struktural yang berbeda, seperti dalam contoh nama lengkap / nama depan / nama belakang Anda. Akan sedikit lebih sulit jika mereka mulai mengandalkan komputasi backend yang berbeda, seperti dalam, "Layanan backend saya mengembalikan bunga majemuk yang dihitung dengan tidak benar yang telah diekspos di API v1 publik. Pelanggan kami telah memperbaiki perilaku yang salah ini. Oleh karena itu, saya tidak dapat memperbaruinya komputasi di backend dan menerapkannya hingga v2. Oleh karena itu, kita sekarang perlu membagi kode perhitungan bunga kita. " Untungnya, itu cenderung jarang: secara praktis, konsumen RESTful API lebih menyukai representasi sumber daya yang akurat daripada kompatibilitas mundur bug-untuk-bug, bahkan di antara perubahan yang tidak melanggar pada GETsumber daya yang secara teoritis tidak berdaya .

Saya akan tertarik untuk mendengar keputusan akhir Anda.

Palpatim
sumber
5
Hanya penasaran, di kode sumber, apakah Anda menduplikasi model antara v0 dan v1 yang tidak berubah? Atau apakah Anda memiliki v1 menggunakan beberapa model v0? Bagi saya, saya akan bingung jika saya melihat v1 menggunakan model v0 untuk beberapa bidang. Namun di sisi lain, hal itu akan mengurangi penggembungan kode. Untuk menangani beberapa versi, apakah kita hanya perlu menerima dan menggunakan kode duplikat untuk model yang tidak pernah berubah?
EdgeCaseBerg
1
Ingatan saya adalah bahwa model kode sumber kami berversi independen dari API itu sendiri, jadi misalnya API v1 mungkin menggunakan Model V1, dan API v2 mungkin juga menggunakan Model V1. Pada dasarnya, grafik ketergantungan internal untuk API publik menyertakan kode API yang terpapar, serta kode "pemenuhan" backend seperti kode server dan model. Untuk beberapa versi, satu-satunya strategi yang pernah saya gunakan adalah duplikasi seluruh tumpukan - pendekatan hybrid (modul A digandakan, modul B diversi ...) tampaknya sangat membingungkan. YMMV tentu saja. :)
Palpatim
2
Saya tidak yakin saya mengikuti apa yang disarankan untuk pendekatan ketiga. Apakah ada contoh kode publik yang terstruktur seperti itu?
Ehtesh Choudhury
13

Bagi saya pendekatan kedua lebih baik. Saya telah menggunakannya untuk layanan web SOAP dan berencana menggunakannya untuk REST juga.

Saat Anda menulis, basis kode harus memperhatikan versi, tetapi lapisan kompatibilitas dapat digunakan sebagai lapisan terpisah. Dalam contoh Anda, basis kode dapat menghasilkan representasi sumber daya (JSON atau XML) dengan nama depan dan belakang, tetapi lapisan kompatibilitas akan mengubahnya menjadi hanya nama saja.

Basis kode harus menerapkan hanya versi terbaru, katakanlah v3. Lapisan kompatibilitas harus mengubah permintaan dan respons antara versi terbaru v3 dan versi yang didukung, misalnya v1 dan v2. Lapisan kompatibilitas dapat memiliki adaptor terpisah untuk setiap versi yang didukung yang dapat dihubungkan sebagai rantai.

Sebagai contoh:

Permintaan klien v1: v1 beradaptasi dengan v2 ---> v2 beradaptasi dengan v3 ----> basis kode

Permintaan klien v2: v1 beradaptasi dengan v2 (lewati) ---> v2 beradaptasi dengan v3 ----> basis kode

Untuk respons, adaptor berfungsi hanya dalam arah yang berlawanan. Jika Anda menggunakan Java EE, Anda dapat menggunakan rantai filter servlet sebagai rantai adaptor misalnya.

Menghapus satu versi itu mudah, hapus adaptor yang sesuai dan kode uji.

S.Stavreva
sumber
Sulit untuk menjamin kompatibilitas jika seluruh basis kode yang mendasari telah berubah. Jauh lebih aman untuk mempertahankan basis kode lama untuk rilis perbaikan bug.
Marcelo Cantos
5

Percabangan tampaknya jauh lebih baik bagi saya, dan saya menggunakan pendekatan ini dalam kasus saya.

Ya, seperti yang telah Anda sebutkan - perbaikan bug backport akan membutuhkan usaha, tetapi pada saat yang sama mendukung beberapa versi di bawah satu basis sumber (dengan perutean dan semua hal lainnya) akan membutuhkan Anda jika tidak kurang, tetapi setidaknya upaya yang sama, membuat sistem lebih banyak rumit dan mengerikan dengan cabang-cabang logika yang berbeda di dalamnya (pada beberapa titik pembuatan versi Anda pasti akan sampai pada poin besar case()ke modul versi yang memiliki kode duplikat, atau bahkan lebih buruk if(version == 2) then...). Juga jangan lupa bahwa untuk tujuan regresi Anda masih harus membuat pengujian bercabang.

Mengenai kebijakan pembuatan versi: saya akan menyimpan versi max -2 dari saat ini, menghentikan dukungan untuk versi lama - yang akan memberikan motivasi bagi pengguna untuk pindah.

edmarisov
sumber
Saat ini saya sedang berpikir untuk menguji dalam satu basis kode. Anda menyebutkan bahwa tes akan selalu perlu bercabang tetapi saya berpikir bahwa semua tes untuk v1, v2, v3 dll dapat hidup dalam solusi yang sama juga dan semua dijalankan pada waktu yang sama. Aku sedang berpikir untuk menghias tes dengan atribut yang menentukan versi apa yang mereka mendukung: misalnya [Version(From="v1", To="v2")], [Version(From="v2", To="v3")], [Version(From="v1")] // All versions Hanya menjelajahi sekarang, pernah mendengar siapa pun melakukannya?
Lee Gunn
1
Nah, setelah 3 tahun saya belajar bahwa tidak ada jawaban yang tepat untuk pertanyaan asli: D. Ini sangat bergantung pada proyek. Jika Anda mampu membekukan API dan hanya memeliharanya (misalnya perbaikan bug) maka saya masih akan mencabangkan / melepaskan kode terkait (logika bisnis terkait API + tes + titik akhir istirahat) dan memiliki semua hal yang dibagikan di perpustakaan terpisah (dengan tesnya sendiri ). Jika V1 akan hidup berdampingan dengan V2 untuk beberapa waktu dan pekerjaan fitur masih berlangsung maka saya akan menyatukannya dan menguji juga (mencakup V1, V2, dll. Dan diberi nama yang sesuai).
edmarisov
1
Terima kasih. Ya, sepertinya ini adalah ruang yang penuh opini. Saya akan mencoba pendekatan satu solusi terlebih dahulu dan melihat bagaimana kelanjutannya.
Lee Gunn
0

Biasanya, pengenalan versi utama API yang membuat Anda berada dalam situasi harus mempertahankan beberapa versi adalah peristiwa yang tidak (atau seharusnya tidak) terjadi sangat sering. Namun, hal itu tidak bisa dihindari sepenuhnya. Saya pikir secara keseluruhan adalah asumsi yang aman bahwa versi mayor, setelah diperkenalkan, akan tetap menjadi versi terbaru untuk jangka waktu yang relatif lama. Berdasarkan ini, saya lebih suka mencapai kesederhanaan dalam kode dengan mengorbankan duplikasi karena ini memberi saya kepercayaan diri yang lebih baik untuk tidak melanggar versi sebelumnya ketika saya memperkenalkan perubahan pada yang terbaru.

pengguna1537847
sumber