Bekerja dengan data tak terduga di JavaScript





Salah satu masalah utama dengan bahasa yang diketik secara dinamis adalah Anda tidak selalu dapat menjamin aliran data yang benar karena Anda tidak dapat memaksa parameter atau variabel untuk disetel ke nilai selain null, misalnya. Dalam kasus seperti itu, kami cenderung menggunakan kode sederhana:



function foo (mustExist) {
  if (!mustExist) throw new Error('Parameter cannot be null')
  return ...
}


Masalah dengan pendekatan ini adalah polusi kode, karena Anda harus menguji variabel di mana-mana dan tidak ada cara untuk menjamin bahwa semua pengembang akan benar-benar menjalankan pengujian ini selalu, terutama dalam situasi di mana variabel atau parameter tidak boleh nol. Seringkali kita bahkan tidak tahu bahwa parameter seperti itu dapat memiliki nilai tidak terdefinisi atau null - ini sering terjadi ketika spesialis yang berbeda bekerja pada bagian klien dan server, yaitu, dalam sebagian besar kasus.



Untuk sedikit mengoptimalkan skenario ini, saya mulai mencari bagaimana dan dengan strategi mana cara terbaik untuk meminimalkan faktor kejutan. Saat itulah saya menemukan artikel bagus oleh Eric Elliott.... Tujuan dari pekerjaan ini bukan untuk sepenuhnya menyangkal artikelnya, tetapi untuk menambahkan informasi menarik yang dapat saya temukan dari waktu ke waktu berkat pengalaman saya di bidang pengembangan JavaScript.



Sebelum saya mulai, saya ingin membahas beberapa poin yang dibahas dalam artikel ini dan menyatakan pendapat saya sebagai pengembang komponen server, karena artikel lain lebih berorientasi pada klien.



Bagaimana semua ini dimulai



Masalah pemrosesan data dapat disebabkan oleh beberapa faktor. Alasan utamanya, tentu saja, adalah input pengguna. Namun, ada sumber data rusak lainnya selain yang disebutkan di artikel lain:



  • Catatan database
  • Fungsi yang secara implisit mengembalikan data nol
  • API Eksternal


Dalam semua kasus yang dipertimbangkan, solusi yang berbeda akan diterapkan, dan kemudian kita akan menganalisis masing-masing secara mendetail, mengingat bahwa tidak ada obat mujarab. Sebagian besar masalah disebabkan oleh kesalahan manusia: dalam banyak kasus bahasa disiapkan untuk bekerja dengan data null atau tidak terdefinisi (null atau undefined), tetapi dalam proses mengubah data ini, kemampuan untuk memprosesnya bisa hilang.



Pengguna memasukkan data



Dalam kasus ini, kami memiliki sangat sedikit peluang. Jika masalahnya adalah input pengguna, itu dapat diselesaikan dengan apa yang disebut hidrasi (dengan kata lain, kita harus mengambil input mentah yang dikirim pengguna kepada kita (misalnya, sebagai bagian dari muatan API) dan mengubahnya menjadi sesuatu yang dengannya kami dapat bekerja tanpa kesalahan).



Di sisi server, saat menggunakan server web seperti Express, kami dapat melakukan semua operasi dengan input pengguna di sisi klien menggunakan alat standar seperti skema JSON  atau Joi .



Contoh dari apa yang dapat dilakukan menggunakan Express atau AJV diberikan di bawah ini:



const Ajv = require('ajv')
const Express = require('express')
const bodyParser = require('body-parser')
 
const app = Express()
const ajv = new Ajv()
 
app.use(bodyParser.json())
 
app.get('/foo', (req, res) => {
  const schema = {
    type: 'object',
    properties: {
      name: { type: 'string' },
      password: { type: 'string' },
      email: { type: 'string', format: 'email' }
    },
    additionalProperties: false
    required: ['name', 'password', 'email']
  }
 
  const valid = ajv.validate(schema, req.body)
    if (!valid) return res.status(422).json(ajv.errors)
    // ...
})
 
app.listen(3000)


Lihat: kami sedang memeriksa bagian utama dari rute tersebut. Secara default, ini adalah objek yang kita dapatkan dari paket body-parser sebagai bagian dari payload. Dalam kasus ini, kami meneruskannya melalui skema JSON , sehingga akan divalidasi jika salah satu properti ini memiliki jenis atau format yang berbeda (dalam kasus email).



Penting! Perhatikan bahwa kami mengembalikan HTTP 422 untuk objek yang belum diproses . Banyak orang menafsirkan kesalahan kueri, seperti tubuh yang tidak valid atau string kueri, sebagai kesalahan 400  Kueri tidak valid - ini sebagian benar, tetapi dalam kasus ini masalahnya bukan pada permintaan itu sendiri, tetapi pada data yang dikirim pengguna bersamanya. Jadi, respons optimal kepada pengguna adalah kesalahan 422: ini berarti permintaan tersebut benar, tetapi tidak dapat diproses karena isinya tidak dalam format yang diharapkan.



Pilihan lain (selain menggunakan AJV) adalah menggunakan perpustakaan yang saya buat dengan Roz . Kami menyebutnya Expresso , dan ini adalah sekumpulan pustaka yang membuatnya sedikit lebih mudah untuk mengembangkan API yang menggunakan Express. Salah satu alat tersebut adalah  @ expresso / validator , yang pada dasarnya melakukan apa yang kami tunjukkan di atas, tetapi dapat diserahkan sebagai middleware.



Parameter tambahan dengan nilai default



Selain apa yang kami periksa sebelumnya, kami menemukan kemungkinan untuk meneruskan nilai null ke aplikasi kami jika tidak dikirim dalam bidang opsional. Bayangkan, misalnya, kita memiliki rute penomoran halaman yang menggunakan dua parameter, halaman dan ukuran, sebagai string kueri. Namun, mereka opsional dan harus default ke default jika tidak diterima.



Idealnya, pengontrol kami harus memiliki fungsi yang melakukan sesuatu seperti ini:



function searchSomething (filter, page = 1, size = 10) {
  // ...
}


Catatan. Sama seperti kesalahan 422 yang kami kembalikan sebagai tanggapan atas permintaan paging, penting untuk mengembalikan kode kesalahan yang benar, 206 Konten tidak lengkap , setiap kali kami menanggapi permintaan yang jumlah data yang dikembalikan adalah bagian dari keseluruhan, kami mengembalikan 206. Ketika pengguna telah mencapai halaman terakhir dan tidak ada data lagi, kami dapat mengembalikan kode 200, dan ketika pengguna mencoba menemukan halaman di luar rentang halaman total, kami mengembalikan kode 204 Tidak ada konten .



Ini akan menyelesaikan masalah ketika kita mendapatkan dua nilai null, tetapi ini adalah aspek JavaScript yang sangat kontroversial secara umum. Parameter opsional mengambil nilai default hanya jika nilainya kosong, namun aturan ini tidak berfungsi untuk nilai null, jadi jika kita melakukan hal berikut:



function foo (a = 10) {
  console.log(a)
}
 
foo(undefined) // 10
foo(20) // 20
foo(null) // null


dan kami memerlukan informasi agar diperlakukan sebagai null, kami tidak dapat hanya mengandalkan parameter opsional untuk ini. Oleh karena itu, dalam kasus seperti itu, kami memiliki dua cara:



1. Gunakan pernyataan If di pengontrol



function searchSomething (filter, page = 1, size = 10) {
  if (!page) page = 1
  if (!size) size = 10
  // ...
}


Kelihatannya tidak terlalu bagus, dan agak merepotkan.



2. Gunakan skema JSON  langsung pada rute



Sekali lagi, kita dapat menggunakan AJV atau @ expresso / validator untuk memvalidasi data ini:



app.get('/foo', (req, res) => {
  const schema = {
    type: 'object',
    properties: {
      page: { type: 'number', default: 1 },
      size: { type: 'number', default: 10 },
    },
    additionalProperties: false
  }
 
<a href=""></a>  const valid = ajv.validate(schema, req.params)
    if (!valid) return res.status(422).json(ajv.errors)
    // ...
})


Bekerja dengan Nilai Null dan Undefined



Saya pribadi tidak senang dengan gagasan menggunakan null dan undefined di JavaScript untuk membuktikan bahwa nilainya kosong, karena beberapa alasan. Selain kesulitan membawa konsep-konsep ini ke tingkat abstrak, kita tidak boleh melupakan parameter opsional. Jika Anda masih ragu tentang konsep-konsep ini, izinkan saya memberi Anda contoh yang bagus dari latihan:







Sekarang setelah kita memahami definisi, kita dapat mengatakan bahwa pada tahun 2020 akan ada dua fungsi utama dalam JavaScript: operator penggabungan nol dan rantai opsional . Saya tidak akan membahas detailnya sekarang, karena saya  sudah menulis artikel tentang ini. (dalam bahasa Portugis), tetapi perhatikan bahwa kedua inovasi ini akan sangat menyederhanakan tugas kita, karena kita dapat berkonsentrasi pada dua konsep ini, null dan undefined dengan operator yang sesuai (??), daripada menggunakan negatif logis seperti! obj itulah lahan subur untuk kesalahan.



Fungsi yang mengembalikan null secara implisit



Masalah ini jauh lebih sulit dipecahkan karena sifatnya yang implisit. Beberapa fungsi memproses data dengan asumsi bahwa data akan selalu tersedia, tetapi dalam beberapa kasus tidak demikian. Mari pertimbangkan contoh standar:



function foo (num) {
  return 23*num
}


Jika num adalah nol, hasil dari fungsi ini akan menjadi 0, yang tidak diharapkan. Dalam kasus seperti itu, kami tidak punya pilihan selain menguji kode. Ada dua jenis pengujian yang bisa dilakukan. Yang pertama adalah menggunakan pernyataan if sederhana:



function foo (num) {
  if (!num) throw new Error('Error')
  return 23*num
}


Cara kedua adalah dengan menggunakan monad Either , yang dibahas secara rinci di artikel yang saya sebutkan. Ini adalah cara yang bagus untuk menangani data yang ambigu, yaitu data yang mungkin nihil atau tidak. Ini karena JavaScript sudah memiliki fungsi bawaan yang mendukung dua aliran tindakan, Janji:



function exists (value) {
  return x != null ? Promise.resolve(value) : Promise.reject(`Invalid value: ${value}`)
}
 
async function foo (num) {
  return exists(num).then(v => 23 * v)
}


Ini adalah bagaimana Anda dapat mendelegasikan pernyataan catch dari sudah ada ke fungsi yang disebut foo:



function init (n) {
  foo(n)
    .then(console.log)
    .catch(console.error)
}
 
init(12) // 276
init(null) // Invalid value: null


API eksternal dan catatan database



Ini adalah kasus yang sangat umum, terutama bila ada sistem yang dikembangkan dari database yang telah dibuat atau diisi sebelumnya. Misalnya, produk baru yang menggunakan database yang sama dengan pendahulunya yang berhasil, sehingga mengintegrasikan pengguna dari sistem yang berbeda, dan seterusnya.



Masalah besar dengan ini bukanlah fakta bahwa database tidak diketahui - sebenarnya, inilah alasannya, karena kami tidak tahu apa yang telah dilakukan di tingkat database, dan kami tidak dapat memastikan apakah kami akan menerima data dengan nilai null atau tidak ditentukan atau tidak ... Kami tidak bisa tidak mengatakan tentang dokumentasi berkualitas buruk ketika database tidak didokumentasikan dengan benar dan kami menghadapi masalah yang sama seperti sebelumnya.



Praktis tidak ada yang dapat kami lakukan di sini, dan secara pribadi saya lebih suka memeriksa status data untuk memastikan saya dapat bekerja dengannya. Namun, Anda tidak dapat memvalidasi semua data, karena banyak dari objek yang dikembalikan mungkin terlalu besar. Oleh karena itu, sebelum melakukan operasi apa pun, disarankan untuk memeriksa data yang terlibat dalam pengoperasian fungsi, seperti peta atau filter, untuk memastikan apakah itu tidak ditentukan atau tidak.



Menghasilkan kesalahan



Merupakan praktik yang baik untuk menggunakan fungsi pernyataan  untuk database dan API eksternal. Pada dasarnya, fungsi-fungsi ini mengembalikan data, jika ada, dan kesalahan dihasilkan sebaliknya. Kasus penggunaan paling umum untuk jenis fungsi ini adalah saat kita memiliki API, misalnya untuk mencari tipe data tertentu dengan pengenal, findById yang terkenal:



async function findById (id) {
  if (!id) throw new InvalidIDError(id)
 
  const result = await entityRepository.findById(id)
  if (!result) throw new EntityNotFoundError(id)
  return result
}


Ganti Entitas dengan nama entitas Anda, seperti UserNotFoundError.



Ini bagus, karena kita dapat memiliki fungsi dalam pengontrol yang sama untuk menemukan pengguna berdasarkan ID dan fungsi lain yang menggunakan pengguna ini untuk menemukan data lain, misalnya, profil pengguna ini di kumpulan database lain. Saat memanggil fungsi pencarian profil, kami menggunakan pernyataan untuk memastikan bahwa pengguna benar-benar ada di database kami. Jika tidak, fungsi tersebut bahkan tidak akan dijalankan dan Anda dapat mencari kesalahan secara langsung pada rute:



async function findUser (id) {
  if (!id) throw new InvalidIDError(id)
 
  const result = await userRepository.findById(id)
  if (!result) throw new UserNotFoundError(id)
  return result
}
 
async function findUserProfiles (userId) {
  const user = await findUser(userId)
 
  const profile = await profileRepository.findById(user.profileId)
  if (!profile) throw new ProfileNotFoundError(user.profileId)
  return profile
}


Perhatikan bahwa kami tidak akan membuat panggilan database jika pengguna tidak ada, karena fungsi pertama memastikan bahwa itu ada. Sekarang kita bisa melakukan sesuatu seperti ini di rute:



app.get('/users/{id}/profiles', handler)
 
// --- //
 
async function handler (req, res) {
  try {
    const userId = req.params.id
    const profile = await userService.getProfile(userId)
    return res.status(200).json(profile)
  } catch (e) {
    if (e instanceof UserNotFoundError || e instanceof ProfileNotFoundError) return res.status(404).json(e.message)
    if (e instanceof InvalidIDError) return res.status(400).json(e.message)
  }
}


Kita dapat mengetahui jenis kesalahan yang dikembalikan hanya dengan memeriksa nama instance dari kelas kesalahan yang ada.



Kesimpulan



Ada beberapa cara untuk memproses data untuk memastikan aliran informasi yang berkelanjutan dan dapat diprediksi. Apakah Anda tahu tips lainnya ?! Tinggalkan di kolom komentar.



Suka materinya?! Ingin memberi saran, mengutarakan pendapat, atau sekadar menyapa? Inilah cara menemukan saya di media sosial:








Artikel ini pertama kali tayang di dev.to oleh Lucas Santos. Jika Anda memiliki pertanyaan atau komentar tentang topik artikel, posting di bawah artikel asli di dev.to



All Articles