Rake saya: dari kain menjadi kekayaan

Latar belakang


Saya telah bekerja sebagai pengembang front-end selama satu tahun sekarang. Proyek pertama saya adalah backend "musuh". Itu terjadi bahwa ini bukan masalah besar ketika komunikasi dibuat.


Tetapi dalam kasus kami tidak demikian.


Kami mengembangkan kode, yang mengandalkan fakta bahwa backend mengirimi kami data tertentu, struktur tertentu, dan format tertentu. Sementara backend dianggap normal untuk mengubah isi tanggapan - tanpa peringatan. Akibatnya, kami butuh berjam-jam untuk menentukan mengapa bagian tertentu dari situs berhenti bekerja.


Kami menyadari bahwa kami perlu memeriksa apa yang dikembalikan backend sebelum mengandalkan data yang dikirimkan kepada kami. Kami membuat tugas untuk penelitian tentang masalah validasi data dari ujung depan.


Penelitian ini ditugaskan kepada saya.


Saya telah membuat daftar apa yang saya inginkan dalam alat yang ingin saya gunakan untuk validasi data.


Poin seleksi paling penting adalah poin berikut:


  • deskripsi (skema) validasi deklaratif, yang diubah menjadi fungsi validator yang mengembalikan true / false (valid, tidak valid)
  • ambang masuk rendah;
  • kesamaan data yang divalidasi dengan deskripsi validasi;
  • kemudahan integrasi validasi khusus;
  • kemudahan integrasi pesan kesalahan khusus.

Akibatnya, saya menemukan banyak perpustakaan validasi, setelah meninjau TOP-5 (ajv, joi, roi ...). Mereka semua sangat baik. Tetapi bagi saya, untuk menyelesaikan 5% kasus rumit - mereka menganggap 95% kasus paling umum adalah bertele-tele dan tebal.


Jadi saya berpikir: mengapa tidak mengembangkan sesuatu sendiri yang cocok untuk saya.
Empat bulan kemudian, versi ketujuh dari perpustakaan validasi kuartet saya keluar.
Itu adalah versi stabil, sepenuhnya teruji, unduhan 11k pada npm. Kami menggunakannya pada tiga proyek dalam kampanye selama tiga bulan.


Tiga bulan ini telah memainkan peran yang sangat berguna. kuartet telah menunjukkan semua kelebihannya. Tidak ada masalah data dari backend. Setiap kali mereka mengubah jawaban, kami segera melakukan kesalahan. Waktu yang dihabiskan untuk menemukan penyebab bug telah menurun secara dramatis. Praktis tidak ada bug data yang tersisa.


Tetapi juga kelemahan diidentifikasi.


Oleh karena itu, saya memutuskan untuk menganalisisnya dan merilis versi baru dengan koreksi semua kesalahan yang dibuat selama pengembangan.
Saya akan berbicara tentang kesalahan arsitektur ini dan solusinya di bawah.


Rake arsitektur


"Stroko" - bahasa khas skema


Saya akan memberikan contoh skema versi lama untuk objek orang tersebut.


const personSchema = { name: 'string', age: 'number', linkedin: ['string', 'null'] } 

Skema ini memvalidasi objek dengan tiga properti: nama - harus berupa string, usia - harus berupa angka, tautan ke akun di LinkedIn - harus berupa nol (jika tidak ada akun) atau string (jika ada akun).


Skema ini memenuhi persyaratan saya untuk keterbacaan, kesamaan dengan data yang divalidasi, dan saya pikir ambang masuk untuk belajar menulis skema seperti itu tidak tinggi. Selain itu, skema seperti itu dapat dengan mudah ditulis dengan definisi tipe dalam naskah:


 type Person = { name: string age: number linkedin: string | null } 

(Seperti yang kita lihat, perubahannya lebih cenderung kosmetik)


Ketika saya membuat keputusan, apa yang harus digunakan untuk opsi validasi yang paling sering (misalnya, yang digunakan di atas). Saya memilih untuk menggunakan - string, seolah-olah, nama-nama validator.


Tetapi masalah dengan string adalah bahwa mereka tidak tersedia untuk kompilator atau penganalisa kesalahan. String 'angka' untuk mereka tidak jauh berbeda dari 'numder'.


Solusi


Versi baru dari kuartet 8.0.0. Saya memutuskan untuk menghapus dari kuartet - penggunaan string sebagai nama validator di dalam skema.


Diagram sekarang terlihat seperti ini:


 const personSchema = { name: v.string age: v.number, linkedin: [v.string, null] } 

Perubahan ini memiliki dua keuntungan besar:


  • kompiler atau penganalisa kesalahan - akan dapat mendeteksi bahwa nama metode dieja dengan kesalahan.
  • Garis - tidak lagi digunakan sebagai elemen skema. Ini berarti bagi mereka Anda dapat memilih fungsi baru di perpustakaan, yang akan dijelaskan di bawah ini.

Dukungan TypeScript


Secara umum, tujuh versi pertama dikembangkan dalam Javascript murni. Ketika beralih ke proyek dengan Typecript, ada kebutuhan untuk mengadaptasi perpustakaan untuknya. Oleh karena itu, ketik deklarasi untuk perpustakaan ditulis.


Tapi ini minus - saat menambahkan fungsionalitas, atau ketika mengubah beberapa elemen perpustakaan, selalu mudah untuk lupa memperbarui deklarasi tipe.


Ada juga ketidaknyamanan kecil seperti ini:


 const checkPerson = v(personSchema) // (0) // ... const person: any = await axios.get('https://myapi.com/person/42') if (!checkPerson(person)) {// (1) throw new TypeError('Invalid person response') } console.log(person.name) // (2) 

Ketika kami membuat validator objek on line (0). Kami ingin setelah memeriksa jawaban sebenarnya dari backend on line (1) dan menangani kesalahan. On line (2) sehingga person bertipe Person. Tetapi ini tidak terjadi. Sayangnya, cek semacam itu bukan tipe penjaga.


Solusi


Saya memutuskan untuk menulis ulang seluruh pustaka kuartet ke dalam TypeScript sehingga kompiler terlibat dalam memeriksa korespondensi perpustakaan dengan jenis. Sepanjang jalan, kami menambahkan ke fungsi yang mengembalikan validator yang dikompilasi, parameter tipe yang akan menentukan tipe penjaga tipe validator ini.


Contohnya terlihat seperti ini:


 const checkPerson = v<Person>(personSchema) // (0) // ... const person: any = await axios.get('https://myapi.com/person/42') if (!checkPerson(person)) {// (1) throw new TypeError('Invalid person response') } console.log(person.name) // (2) 

Sekarang on line (2) person bertipe Person .


Keterbacaan


Ada juga dua kasus di mana kode itu dibaca dengan buruk: memeriksa kepatuhan dengan set nilai tertentu (memeriksa enum) dan memeriksa properti lain dari objek.


a) Periksa enum
Awalnya, ada ide, menurut saya bagus. Kami akan menunjukkannya dengan menambahkan bidang "gender" ke objek kami.
Versi lama sirkuit tampak seperti ini:


 const personSchema = { name: 'string', age: 'number', linkedin: ['null', 'string'], sex: v.enum('male', 'female') } 

Opsi ini sangat mudah dibaca. Tapi seperti biasa, semuanya berjalan agak tidak sesuai rencana.
Memiliki enum yang dinyatakan dalam program, misalnya, ini:


 enum Sex { Male = 'male', Female = 'female' } 

Secara alami saya ingin menggunakannya di dalam sirkuit. Sehingga ketika mengubah salah satu nilai (misalnya, 'laki-laki' -> 'm', 'perempuan' -> 'f'), skema validasi juga harus berubah.


Oleh karena itu, hampir selalu validasi enum ditulis seperti ini:


 const personSchema = { name: 'string', age: 'number', linkedin: ['null', 'string'], sex: v.enum(...Object.values(Sex)) } 

Yang terlihat cukup besar.


b) Validasi properti residual dari objek


Misalkan kita menambahkan karakteristik seperti itu ke objek kita - ia dapat memiliki bidang tambahan, tetapi semuanya harus berupa tautan ke jejaring sosial - itu berarti mereka harus berupa null atau string.


Skema lama akan terlihat seperti ini:


 const personSchema = { name: 'string', age: 'number', linkedin: ['null', 'string'], sex: v.enum(...Object.values(Sex)), ...v.rest(['null', 'string']) // Rest props are string | null } 

Entri ini menyoroti properti yang tersisa - dari yang sudah terdaftar. Penggunaan operator spread cenderung membingungkan orang yang ingin memahami skema ini.


Solusi


Seperti dijelaskan di atas, string tidak lagi menjadi bagian dari skema validasi. Hanya tiga jenis nilai Javascript yang tetap menjadi skema validasi. Objek - untuk menggambarkan skema validasi objek. Array untuk deskripsi - beberapa opsi untuk validitas. Fungsi (pustaka yang dibuat atau kustom) - untuk semua opsi validasi lainnya.


Ketentuan ini diizinkan untuk menambah fungsionalitas, yang memungkinkan untuk meningkatkan keterbacaan rangkaian berkali-kali.


Padahal, bagaimana jika kita ingin membandingkan nilainya dengan string 'pria'. Apakah kita benar-benar perlu mengetahui hal lain selain nilai itu sendiri dan string 'laki-laki'.


Oleh karena itu, diputuskan untuk menambahkan nilai tipe primitif sebagai elemen rangkaian. Oleh karena itu, di mana Anda memenuhi nilai primitif dalam skema, ini berarti bahwa ini adalah nilai valid yang harus diperiksa oleh validator sesuai dengan skema ini. Lebih baik saya memberi contoh:


Jika kita perlu memeriksa nomor untuk kesetaraan 42-pikiran. Lalu kita tulis seperti ini:


 const check42 = v(42) check42(42) // => true check42(41) // => false check42(43) // => false check42('42') // => false 

Mari kita lihat bagaimana ini memengaruhi skema orang (tanpa memperhitungkan properti tambahan akun):


 const personSchema = { name: v.string, age: v.number, linkedin: [null, v.string], // null is primitive value sex: ['male', 'female'] // 'male', 'female' are primitive values } 

Dengan menggunakan enum yang telah ditentukan, kita dapat menulis ulang seperti ini:


 const personSchema = { name: v.string, age: v.number, linkedin: [null, v.string], sex: Object.values(Sex) // same as ['male', 'female'] } 

Dalam hal ini, upacara yang tidak perlu dihapus dalam bentuk menggunakan metode enum dan menggunakan operator spread untuk memasukkan nilai yang valid dari objek sebagai parameter ke dalam metode ini.


Apa yang dianggap sebagai nilai primitif: angka, string, karakter, true , false , null , dan undefined .


Artinya, jika kita perlu membandingkan nilainya dengan mereka, kita cukup menggunakan nilai-nilai ini sendiri. Dan perpustakaan validasi - itu akan membuat validator yang secara ketat membandingkan nilai dengan yang ditentukan dalam skema.


Untuk memvalidasi properti residual, dipilih untuk menggunakan properti khusus untuk semua bidang objek lainnya:


 const personSchema = { name: v.string, age: v.number, linkedin: [null, v.string], sex: Object.values(Sex), [v.rest]: [null, v.string] } 

Dengan demikian, sirkuit terlihat lebih mudah dibaca. Dan lebih seperti iklan naskah.


Validator terkait dengan fungsi yang membuatnya


Dalam versi yang lebih lama, penjelasan kesalahan bukan bagian dari validator. Mereka ditambahkan ke array di dalam fungsi v .


Sebelumnya, untuk mendapatkan penjelasan tentang kesalahan validasi, Anda harus memiliki validator bersama Anda (untuk memeriksa) dan v (untuk mendapatkan penjelasan tentang ketidakabsahan). Semua ini tampak sebagai berikut:

a) Kami menambahkan penjelasan pada diagram


 const checkPerson = v({ name: v('string', 'wrong name') age: v('number', 'wrong age'), linkedin: v(['null', 'string'], 'wrong linkedin'), sex: v( v.enum(...Object.values(Sex)), 'wrong sex value' ), ...v.rest( v( ['null', 'string'], 'wrong social networks link' ) ) // Rest props are string | null }) 

Ke elemen mana pun dari rangkaian - Anda dapat menambahkan penjelasan tentang kesalahan menggunakan argumen kedua fungsi v compiler.


b) Kosongkan array penjelasan


Sebelum validasi, perlu untuk menghapus array global ini di mana semua penjelasan dicatat selama validasi.


 v.clearContext() // same as v.explanations = [] 

c) Validasi


 const isPersonValid = checkPerson(person) 

Selama pemeriksaan ini, jika validitas terdeteksi, dan pada tahap pembuatan sirkuit - penjelasan diberikan padanya, penjelasan ini ditempatkan dalam array global v.explanation .


d) Penanganan kesalahan


 if (!isPersonValid) { throw new TypeError('Invalid person response: ' + v.explanation.join('; ')) } // ex. Throws 'Invalid person response: wrong name; wrong age' 

Seperti yang Anda lihat di sini, ada masalah besar. Karena jika kita ingin menggunakan validator bukan di tempat pembuatannya. Kita harus melewati tidak hanya parameter, tetapi juga fungsi yang membuatnya. Karena di dalamnya array terletak di mana penjelasan akan ditambahkan.


Solusi


Masalah ini diselesaikan sebagai berikut: penjelasan menjadi bagian dari fungsi validasi itu sendiri. Apa yang bisa dipahami dari tipenya:
ketik Validator = (nilai: ada, penjelasan?: ada []) => boolean


Sekarang jika Anda membutuhkan penjelasan tentang kesalahan, Anda meneruskan array yang ingin Anda tambahkan penjelasan.


Dengan demikian, validator menjadi unit independen. Suatu metode juga telah ditambahkan yang dapat mengubah fungsi validasi menjadi fungsi yang mengembalikan null jika nilainya valid, dan mengembalikan array penjelasan jika nilainya tidak valid.


Sekarang validasi dengan penjelasannya terlihat seperti ini:


 const checkPerson = v<Person>({ name: v(v.string, 'wrong name'), age: v(v.number, 'wrong age'), linkedin: v([null, v.string], 'wrong linkedin') sex: v(Object.values(Sex), 'wrong sex') [v.rest]: v([null, v.string], 'wrong social network') }) // ... const explanations = [] if (!checkPerson(person, explanation)) { throw new TypeError('Wrong person: ' + explanations.join('; ')) } // OR const getExplanation = v.explain(checkPerson) const explanations = getExplanation(person) if (explanations) { throw new TypeError('Wrong person: ' + explanations.join('; ')) } 

Kata penutup


Saya menyoroti tiga tempat karena itu saya harus menulis ulang semuanya:


  • Harapannya agar orang tidak salah ketika menulis kalimat
  • Menggunakan variabel global (dalam hal ini, v.explanation array)
  • Pengujian dengan contoh kecil selama pengembangan - tidak menunjukkan masalah yang muncul ketika digunakan dalam kasus besar nyata.

Tapi saya senang saya melakukan analisis masalah ini, dan versi yang dirilis sudah digunakan dalam proyek kami. Dan saya berharap ini akan bermanfaat bagi kita tidak kurang dari yang sebelumnya.


Terima kasih sudah membaca, semoga pengalaman saya bermanfaat bagi Anda.

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


All Articles