Protobuffer salah

Untuk sebagian besar kehidupan profesional saya, saya menentang menggunakan Protokol Buffer. Mereka jelas ditulis oleh amatir, sangat sangat terspesialisasi, menderita banyak jebakan, sulit untuk dikompilasi dan memecahkan masalah yang tidak dimiliki oleh siapa pun kecuali Google. Jika masalah proto-buffer ini tetap berada di karantina abstraksi serialisasi, maka klaim saya akan berakhir di sana. Namun sayangnya, desain Protobuffers yang buruk sangat mengganggu sehingga masalah ini dapat bocor ke dalam kode Anda.

Spesialisasi dan pengembangan sempit oleh para amatir

Berhenti Tutup klien surel Anda, tempat Anda telah menulis surat kepada saya yang mengatakan bahwa "insinyur terbaik di dunia bekerja di Google", bahwa "desain mereka, menurut definisi, tidak dapat dibuat oleh amatir." Saya tidak ingin mendengarnya.

Mari kita tidak membahas topik ini. Pengungkapan penuh: Saya dulu bekerja di Google. Ini adalah tempat pertama (tapi sayangnya bukan yang terakhir) yang pernah saya gunakan Protobuffers. Semua masalah yang ingin saya bicarakan ada di basis kode Google; itu bukan hanya "penyalahgunaan protobuffers" dan sejenisnya.

Sejauh ini, masalah terbesar dengan Protobuffers adalah sistem tipe yang mengerikan. Penggemar Java seharusnya merasa betah di sini, tapi sayangnya secara harfiah tidak ada yang mengira Java adalah sistem tipe yang dirancang dengan baik. Orang-orang dari kamp pengetikan dinamis mengeluh tentang batasan yang tidak perlu, sementara perwakilan dari kamp pengetikan statis, seperti saya, mengeluh tentang batasan yang tidak perlu dan kurangnya semua yang Anda inginkan dari sistem mengetik. Kehilangan dalam kedua kasus.

Spesialisasi dan pengembangan yang sempit oleh para amatir berjalan seiring. Sebagian besar spesifikasinya tampaknya dibaut pada saat terakhir - dan itu jelas dibaut pada saat terakhir. Beberapa batasan akan memaksa Anda untuk berhenti, menggaruk-garuk kepala dan bertanya: "Apa-apaan ini?" Tapi ini hanya gejala dari masalah yang lebih dalam:

Jelas, protobuffer dibuat oleh amatir karena mereka menawarkan solusi yang buruk untuk masalah terkenal dan sudah diselesaikan.

Kurang komposisi


Protobuffers menawarkan beberapa fitur yang tidak bekerja satu sama lain. Misalnya, lihat daftar ortogonal, tetapi pada saat yang sama fungsi pengetikan terbatas yang saya temukan dalam dokumentasi.

  • oneof bidang tidak dapat repeated .
  • map<k,v> bidang map<k,v> memiliki sintaks khusus untuk kunci dan nilai, tetapi tidak digunakan dalam tipe lainnya.
  • Meskipun bidang map dapat diparameterisasi, tidak ada tipe yang ditentukan pengguna yang diizinkan lagi. Ini berarti bahwa Anda terjebak dengan menentukan spesialisasi Anda sendiri secara manual dalam struktur data umum.
  • Bidang map tidak dapat repeated .
  • Kunci map bisa berupa string , tetapi bukan bytes . Enum juga dilarang, meskipun yang terakhir dianggap setara dengan bilangan bulat di semua bagian lain dari spesifikasi Protobuffers.
  • Nilai map tidak boleh berupa map lain.

Daftar pembatasan gila ini adalah hasil dari pilihan desain dan fungsi pemasangan yang tidak berprinsip pada saat terakhir. Misalnya, satu bidang tidak dapat repeated , karena alih-alih jenis samping, pembuat kode akan menghasilkan bidang opsional yang saling eksklusif. Transformasi seperti itu hanya berlaku untuk bidang tunggal (dan, seperti yang akan kita lihat nanti, itu tidak bekerja bahkan untuk itu).

Pembatasan bidang map , yang tidak dapat repeated , kira-kira dari opera yang sama, tetapi menunjukkan batasan yang berbeda dari sistem tipe. Di belakang layar, map<k,v> berubah menjadi sesuatu yang mirip dengan repeated Pair<k,v> . Dan karena repeated adalah kata kunci ajaib dari bahasa, dan bukan tipe normal, itu tidak bergabung dengan dirinya sendiri.

Tebakan Anda tentang masalah enum sama benarnya dengan dugaan saya.

Apa yang sangat membuat frustrasi tentang semua ini adalah pemahaman yang buruk tentang bagaimana sistem tipe modern bekerja. Pemahaman ini secara dramatis akan menyederhanakan spesifikasi Protobuffers dan pada saat yang sama menghapus semua pembatasan sewenang-wenang .

Solusinya adalah sebagai berikut:

  • Buat semua bidang dalam pesan yang required . Ini membuat setiap pesan jenis produk.
  • Naikkan nilai bidang oneof menjadi tipe data yang berdiri sendiri. Ini akan menjadi jenis coproduct.
  • Untuk mengaktifkan parameterisasi jenis produk dan produk tambahan jenis lainnya.

Itu saja! Ketiga perubahan ini adalah semua yang Anda butuhkan untuk menentukan data yang memungkinkan. Dengan sistem sederhana ini, Anda dapat mengulang semua spesifikasi Protobuffers lainnya.

Misalnya, Anda dapat mengulangi bidang optional :

 product Unit { // no fields } coproduct Optional<t> { t value = 0; Unit unset = 1; } 

Membuat bidang repeated juga sederhana:

 coproduct List<t> { Unit empty = 0; Pair<t, List<t>> cons = 1; } 

Tentu saja, logika serialisasi sebenarnya memungkinkan Anda untuk melakukan sesuatu yang lebih pintar daripada mendorong daftar terkait melalui jaringan - lagipula, implementasi dan semantik tidak harus saling berhubungan .

Pilihan yang meragukan


Protobuffers gaya Java membedakan antara tipe skalar dan pesan . Skalars kurang lebih sesuai dengan primitif mesin - hal-hal seperti int32 , bool dan string . Jenis pesan, di sisi lain, adalah sisanya. Semua perpustakaan dan tipe pengguna adalah pesan.

Tentu saja, kedua jenis tipe ini memiliki semantik yang sama sekali berbeda.

Bidang dengan tipe skalar selalu ada. Bahkan jika Anda tidak menginstalnya. Saya sudah mengatakan itu (setidaknya dalam proto3 1 ) apakah semua proto-buffer diinisialisasi ke nol, bahkan jika mereka sama sekali tidak memiliki data? Bidang skalar mendapatkan nilai palsu: misalnya, uint32 diinisialisasi ke 0 , dan string diinisialisasi ke "" .

Tidak mungkin membedakan bidang yang tidak ada di proto-buffer dari bidang yang diberi nilai default. Agaknya, keputusan ini dibuat untuk optimasi agar tidak meneruskan standar skalar. Ini hanya sebuah asumsi, karena dokumentasi tidak menyebutkan optimasi ini, jadi asumsi Anda tidak lebih buruk dari saya.

Ketika kita membahas klaim Protobuffers tentang solusi ideal untuk kompatibilitas API ke belakang dan di masa depan, kita akan melihat bahwa ketidakmampuan untuk membedakan antara nilai-nilai yang tidak terdefinisi dan standar adalah mimpi buruk yang nyata. Terutama jika itu benar-benar keputusan sadar untuk menyimpan satu bit (set atau tidak) untuk lapangan.

Bandingkan perilaku ini dengan jenis pesan. Sementara bidang skalar adalah "bodoh", perilaku bidang pesan benar-benar gila . Secara internal, bidang pesan ada di sana atau tidak, tetapi perilakunya gila. Kode pseudo kecil untuk pengakses mereka bernilai ribuan kata. Bayangkan ini di Jawa atau di tempat lain:

 private Foo m_foo; public Foo foo { // only if `foo` is used as an expression get { if (m_foo != null) return m_foo; else return new Foo(); } // instead if `foo` is used as an lvalue mutable get { if (m_foo = null) m_foo = new Foo(); return m_foo; } } 

Secara teori, jika bidang foo tidak disetel, Anda akan melihat salinan awal yang diinisialisasi, apakah Anda memintanya atau tidak, tetapi Anda tidak dapat mengubah wadah. Tetapi jika Anda mengubah foo , itu juga akan mengubah induknya! Semua ini hanya untuk menghindari penggunaan tipe Maybe Foo dan "sakit kepala" terkait untuk mencari tahu apa arti nilai yang tidak terdefinisi.

Perilaku seperti itu sangat mengerikan karena melanggar hukum! Kami mengharapkan pekerjaan msg.foo = msg.foo; tidak akan bekerja Sebaliknya, implementasi sebenarnya diam-diam mengubah msg ke salinan foo dengan nol-inisialisasi jika tidak ada sebelumnya.

Tidak seperti bidang skalar, setidaknya Anda dapat menentukan bahwa bidang pesan tidak disetel. Binding bahasa untuk protobuffers menawarkan sesuatu seperti metode bool has_foo() dihasilkan. Jika ada, maka dalam kasus sering menyalin bidang pesan dari satu protobuffer ke yang lain, Anda harus menulis kode berikut:

 if (src.has_foo(src)) { dst.set_foo(src.foo()); } 

Harap dicatat bahwa, setidaknya dalam bahasa dengan pengetikan statis, templat ini tidak dapat diabstraksi karena hubungan nominal antara metode foo() , set_foo() dan has_foo() . Karena semua fungsi ini adalah pengidentifikasi mereka sendiri, kami tidak memiliki sarana untuk membuatnya secara terprogram, dengan pengecualian makro preprosesor:

 #define COPY_IFF_SET(src, dst, field) \ if (src.has_##field(src)) { \ dst.set_##field(src.field()); \ } 

(tapi macro preprosesor dilarang oleh panduan gaya Google).

Jika alih-alih semua bidang tambahan diimplementasikan sebagai Maybe , Anda dapat dengan aman mengatur rekan panggilan abstrak.

Untuk mengubah topik pembicaraan, mari kita bicara tentang keputusan meragukan lainnya. Meskipun Anda dapat menentukan satu bidang dalam oneof , semantiknya tidak cocok dengan jenis produk bersama! Kesalahan pemula, kawan! Sebagai gantinya, Anda mendapatkan bidang opsional untuk setiap kotak dan kode ajaib di setter, yang akan dengan mudah membatalkan bidang lain jika disetel.

Pada pandangan pertama, tampaknya ini harus secara semantik setara dengan jenis persatuan yang benar. Tetapi sebaliknya, kita mendapatkan sumber kesalahan yang menjijikkan dan tak terlukiskan! Ketika perilaku ini dikombinasikan dengan implementasi ilegal msg.foo = msg.foo; , tugas yang tampaknya normal seperti itu secara diam-diam menghapus jumlah data yang sewenang-wenang!

Akibatnya, ini berarti bahwa oneof bidang tidak membentuk Prism taat hukum, dan pesan tidak membentuk Lens taat hukum. Jadi semoga sukses dengan upaya Anda untuk menulis manipulasi protobuffer nontrivial tanpa bug. Secara harfiah tidak mungkin untuk menulis kode polimorfik universal, bebas kesalahan, pada protobuffers .

Ini tidak terlalu menyenangkan untuk didengar, terutama bagi kita yang menyukai polimorfisme parametrik, yang menjanjikan kebalikannya .

Kebohongan kompatibilitas ke belakang dan ke depan


Salah satu "fitur mematikan" Protobuffers yang sering disebut adalah "kemampuan mereka yang bebas masalah untuk menulis mundur dan meneruskan API yang kompatibel." Pernyataan ini digantung di depan mata Anda untuk mengaburkan kebenaran.

Protobuffer itu permisif . Mereka berhasil mengatasi pesan dari masa lalu atau masa depan, karena mereka sama sekali tidak menjanjikan tentang bagaimana data Anda akan terlihat. Semuanya opsional! Tetapi jika Anda membutuhkannya, Protobuffers akan dengan senang hati mempersiapkan dan memberi Anda sesuatu dengan pengecekan tipe, terlepas dari apakah itu masuk akal.

Ini berarti bahwa Protobuffer menjalankan "perjalanan waktu" yang dijanjikan sambil secara diam-diam melakukan hal yang salah secara default . Tentu saja, seorang programmer yang cermat dapat (dan seharusnya) menulis kode yang memeriksa kebenaran dari protobuffers yang diterima. Tetapi jika Anda menulis pemeriksaan kebenaran perlindungan di setiap situs, mungkin itu hanya berarti bahwa langkah deserialisasi terlalu permisif. Yang berhasil Anda lakukan adalah mendesentralisasikan logika validasi dari batas yang terdefinisi dengan baik dan coret di seluruh basis kode.

Salah satu argumen yang mungkin adalah bahwa protobuffers akan menyimpan informasi apa pun yang mereka tidak mengerti dalam pesan. Pada prinsipnya, ini berarti pengiriman pesan yang tidak merusak melalui perantara yang tidak memahami versi skema ini. Ini kemenangan yang jelas, bukan?

Tentu saja, di atas kertas ini adalah fitur keren. Tapi saya belum pernah melihat aplikasi di mana properti ini benar-benar disimpan. Dengan pengecualian perangkat lunak perutean, tidak ada program yang ingin memeriksa hanya sebagian kecil dari pesan dan kemudian meneruskannya tidak berubah. Sebagian besar program protobuffer akan men-decode pesan, mengubahnya menjadi yang lain dan mengirimkannya ke tempat lain. Sayangnya, konversi ini dibuat sesuai pesanan dan disandikan secara manual. Dan konversi manual dari satu protobuffer ke yang lain tidak mempertahankan bidang yang tidak diketahui, karena secara harfiah tidak ada gunanya.

Sikap di mana-mana terhadap protobuffer yang kompatibel secara universal juga dimanifestasikan dengan cara-cara jelek lainnya. Panduan gaya untuk Protobuffer secara aktif menentang KERING dan menyarankan penyisipan definisi dalam kode bila memungkinkan. Mereka berpendapat bahwa ini akan memungkinkan penggunaan pesan terpisah di masa depan jika definisi berbeda. Saya menekankan bahwa mereka menawarkan untuk meninggalkan praktik pemrograman yang baik selama 60 tahun untuk berjaga-jaga , tiba-tiba, suatu saat di masa depan Anda perlu mengubah sesuatu.

Akar masalahnya adalah bahwa Google menggabungkan makna data dengan representasi fisiknya. Saat Anda menggunakan skala Google, itu masuk akal. Pada akhirnya, mereka memiliki alat internal yang membandingkan pembayaran per jam programmer menggunakan jaringan, biaya penyimpanan X byte dan hal-hal lain. Tidak seperti kebanyakan perusahaan teknologi, gaji programmer adalah salah satu item pengeluaran terkecil Google. Secara finansial, masuk akal bagi mereka untuk menghabiskan waktu programmer untuk menghemat beberapa byte.

Selain lima perusahaan teknologi terkemuka, tidak ada orang lain yang berada dalam lima urutan besarnya Google. Startup Anda tidak mampu menghabiskan waktu rekayasa menghemat byte. Tetapi menghemat byte dan membuang waktu programmer dalam proses adalah apa yang dioptimalkan untuk Protobuffers.

Mari kita hadapi itu. Anda tidak sesuai dengan skala Google, dan Anda tidak akan pernah cocok. Berhentilah menggunakan kultus kargo teknologi hanya karena "Google menggunakannya," dan karena "ini adalah praktik terbaik industri."

Protobuffer mencemari basis kode


Jika mungkin untuk membatasi penggunaan Protobuffers hanya pada jaringan, saya tidak akan berbicara begitu keras tentang teknologi ini. Sayangnya, meskipun pada prinsipnya ada beberapa solusi, tidak ada satupun yang cukup baik untuk digunakan dalam perangkat lunak nyata.

Protobuffers berhubungan dengan data yang ingin Anda kirim melalui saluran komunikasi. Mereka sering konsisten , tetapi tidak identik , dengan data aktual yang ingin dikerjakan oleh aplikasi. Ini menempatkan kami pada posisi yang tidak nyaman, Anda harus memilih salah satu dari tiga opsi buruk:

  1. Pertahankan jenis terpisah yang menggambarkan data yang benar-benar Anda butuhkan, dan pastikan kedua jenis didukung secara bersamaan.
  2. Kemas data lengkap dalam format untuk transmisi dan digunakan oleh aplikasi.
  3. Ambil data lengkap setiap kali dibutuhkan dari format pendek untuk transmisi.

Opsi 1 jelas merupakan solusi "benar", tetapi tidak cocok untuk Protobuffers. Bahasa tidak cukup kuat untuk menyandikan tipe yang dapat melakukan pekerjaan ganda dalam dua format. Ini berarti bahwa Anda harus menulis tipe data yang benar-benar terpisah, mengembangkannya secara sinkron dengan Protobuffers, dan secara khusus menulis kode serialisasi untuk mereka . Tetapi karena kebanyakan orang tampaknya menggunakan Protobuffers untuk tidak menulis kode serialisasi, opsi ini jelas tidak pernah diterapkan.

Sebaliknya, kode yang menggunakan protobuffers memungkinkan mereka untuk didistribusikan di seluruh basis kode. Itu adalah kenyataan. Proyek utama saya di Google adalah kompiler yang mengambil "program" yang ditulis dalam satu variasi Protobuffers dan menghasilkan "program" yang setara di yang lain. Format input dan output sangat berbeda sehingga versi paralel yang benar dari C ++ tidak pernah bekerja. Akibatnya, kode saya tidak dapat menggunakan salah satu teknik penulisan kompiler yang kaya, karena data Protobuffers (dan kode yang dihasilkan) terlalu sulit untuk melakukan sesuatu yang menarik dengan mereka.

Akibatnya, alih-alih 50 garis skema rekursi , 10.000 baris pengocokan penyangga khusus digunakan. Kode yang ingin saya tulis secara harfiah tidak mungkin dilakukan dengan proto-buffer.

Meskipun ini adalah satu kasus, itu tidak unik. Karena sifat keras pembuatan kode, manifestasi proto-buffer dalam bahasa tidak akan pernah menjadi idiomatis, dan mereka tidak dapat dibuat demikian - kecuali Anda menulis ulang pembuat kode.

Tetapi meskipun begitu, Anda masih memiliki masalah dengan menanamkan sistem tipe jelek di bahasa target Anda. Karena sebagian besar fungsi Protobuffers tidak dipikirkan dengan baik, sifat meragukan ini bocor ke dalam basis kode kami. Ini berarti bahwa kita dipaksa untuk tidak hanya mengimplementasikan, tetapi juga menggunakan ide-ide buruk ini dalam setiap proyek yang berharap untuk berinteraksi dengan Protobuffers.

Pada dasar yang kuat, mudah untuk menyadari hal-hal yang tidak berarti, tetapi jika Anda pergi ke arah yang berbeda, paling baik Anda akan menghadapi kesulitan, dan paling buruk, dengan kengerian kuno yang nyata.

Secara umum, berikan harapan kepada siapa pun yang mengimplementasikan Protobuffer dalam proyek mereka.



1. Sampai hari ini, ada diskusi hangat di Google tentang proto2 dan apakah bidang harus ditandai sebagaimana required . Manifes " optional dianggap berbahaya" dan " required dianggap berbahaya" didistribusikan pada saat yang sama. Semoga berhasil, kawan.

Source: https://habr.com/ru/post/id427265/


All Articles