العمل مع البيانات غير المتوقعة في JavaScript





تتمثل إحدى المشكلات الرئيسية في اللغات المكتوبة ديناميكيًا في أنه لا يمكنك دائمًا ضمان صحة تدفق البيانات لأنه لا يمكنك فرض معلمة أو متغير على قيمة أخرى بخلاف القيمة الفارغة ، على سبيل المثال. في مثل هذه الحالات ، نميل إلى استخدام كود بسيط:



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


تكمن مشكلة هذا النهج في تلوث الكود ، حيث يتعين عليك اختبار المتغيرات في كل مكان ولا توجد طريقة لضمان أن جميع المطورين سيجرون هذا الاختبار فعليًا دائمًا ، خاصة في المواقف التي لا يمكن أن يكون فيها متغير أو معلمة خالية. في كثير من الأحيان لا نعرف حتى أن مثل هذه المعلمة يمكن أن يكون لها قيمة غير محددة أو فارغة - يحدث هذا غالبًا عندما يعمل متخصصون مختلفون على أجزاء العميل والخادم ، أي في الغالبية العظمى من الحالات.



لتحسين هذا السيناريو قليلاً ، بدأت في البحث عن الكيفية والاستراتيجيات الأفضل لتقليل عامل المفاجأة. هذا عندما صادفت مقالًا رائعًا بقلم إريك إليوت.... الغرض من هذا العمل ليس دحض مقالته تمامًا ، ولكن لإضافة معلومات مثيرة للاهتمام تمكنت من اكتشافها بمرور الوقت بفضل خبرتي في مجال تطوير JavaScript.



قبل أن أبدأ ، أود أن أتطرق إلى بعض النقاط التي تمت تغطيتها في هذه المقالة وأعطي رأيي كمطور لمكونات الخادم ، نظرًا لأن مقالة أخرى تركز بشكل أكبر على العميل.



كيف بدأ كل شيء



يمكن أن تكون مشكلة معالجة البيانات بسبب عدة عوامل. السبب الرئيسي ، بالطبع ، هو مدخلات المستخدم. ومع ذلك ، هناك مصادر أخرى للبيانات المشوهة بالإضافة إلى تلك المذكورة في مقال آخر:



  • سجلات قاعدة البيانات
  • الدالات التي تُرجع ضمنيًا بيانات فارغة
  • واجهات برمجة التطبيقات الخارجية


في جميع الحالات التي تم النظر فيها ، سيتم تطبيق حلول مختلفة ، وبعد ذلك سنقوم بتحليل كل منها بالتفصيل ، مع تذكر أن لا شيء هو الدواء الشافي. معظم المشاكل ناتجة عن خطأ بشري: في كثير من الحالات تكون اللغات على استعداد للعمل مع بيانات فارغة أو غير محددة (فارغة أو غير محددة) ، ولكن في عملية تحويل هذه البيانات ، قد تفقد القدرة على معالجتها.



أدخل المستخدم البيانات



في هذه الحالة ، لدينا فرص قليلة جدًا. إذا كانت المشكلة تكمن في مدخلات المستخدم ، فيمكن حلها بما يسمى الترطيب (بمعنى آخر ، علينا أن نأخذ المدخلات الأولية التي يرسلها المستخدم إلينا (على سبيل المثال ، كجزء من حمولة API) وتحويلها إلى شيء يمكن بواسطته يمكننا العمل بدون أخطاء).



على جانب الخادم ، عند استخدام خادم ويب مثل Express ، يمكننا إجراء جميع العمليات بإدخال المستخدم في جانب العميل باستخدام أدوات قياسية مثل مخطط JSON  أو Joi .



فيما يلي مثال لما يمكن فعله باستخدام Express أو AJV:



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)


انظر: نحن نتحقق من الجزء الرئيسي من الطريق. بشكل افتراضي ، هذا هو الكائن الذي نحصل عليه من حزمة body-parser كجزء من الحمولة. في هذه الحالة ، نقوم بتمريرها عبر مخطط JSON ، لذلك سيتم التحقق من صحتها إذا كانت إحدى هذه الخصائص من نوع أو تنسيق مختلف (في حالة البريد الإلكتروني).



مهم! لاحظ أننا نعيد HTTP 422 لكائن غير معالج . يفسر العديد من الأشخاص خطأ الاستعلام ، مثل نص غير صالح أو سلسلة استعلام ، كخطأ 400  استعلام غير صالح - هذا صحيح جزئيًا ، ولكن في هذه الحالة لم تكن المشكلة في الطلب نفسه ، ولكن في البيانات التي أرسلها المستخدم معه. لذا فإن الاستجابة المثلى للمستخدم ستكون الخطأ 422: هذا يعني أن الطلب صحيح ، لكن لا يمكن معالجته لأن محتواه ليس بالصيغة المتوقعة.



خيار آخر (إلى جانب استخدام AJV) هو استخدام المكتبة التي أنشأتها باستخدام Roz . أطلقنا عليه اسم Expresso ، وهو عبارة عن مجموعة من المكتبات التي تسهل تطوير واجهات برمجة التطبيقات التي تستخدم Express. إحدى هذه الأدوات هي  Expresso / Validator ، والتي تقوم أساسًا بما أوضحناه أعلاه ، ولكن يمكن تسليمها كبرنامج وسيط.



معلمات إضافية مع القيم الافتراضية



بالإضافة إلى ما فحصناه سابقًا ، اكتشفنا إمكانية تمرير قيمة فارغة لتطبيقنا في حالة عدم إرسالها في حقل اختياري. تخيل ، على سبيل المثال ، أن لدينا مسار ترقيم الصفحات يأخذ معلمتين ، الصفحة والحجم ، كسلاسل استعلام. ومع ذلك ، فهي اختيارية ويجب أن تكون افتراضيًا في حالة عدم استلامها.



من الناحية المثالية ، يجب أن يكون لوحدة التحكم الخاصة بنا وظيفة تقوم بشيء مثل هذا:



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


ملحوظة. تمامًا كما هو الحال مع الخطأ 422 الذي قمنا بإعادته ردًا على طلبات الترحيل ، من المهم إرجاع رمز الخطأ الصحيح ، 206 محتوى غير مكتمل ، عندما نرد على طلب تكون كمية البيانات التي تم إرجاعها جزءًا من الكل ، نرجع 206. عندما يصل المستخدم إلى الصفحة الأخيرة ولا توجد بيانات أخرى ، يمكننا إرجاع رمز 200 ، وعندما يحاول المستخدم العثور على صفحة خارج نطاق الصفحات الإجمالي ، نعيد الرمز 204 لا يوجد محتوى .



هذا من شأنه أن يحل المشكلة عندما نحصل على قيمتين فارغتين ، لكن هذا جانب مثير للجدل للغاية في JavaScript بشكل عام. تأخذ المعلمات الاختيارية قيمة افتراضية فقط إذا كانت القيمة فارغة ، ولكن هذه القاعدة لا تعمل مع القيمة فارغة ، لذلك إذا قمنا بما يلي:



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


ونحتاج إلى التعامل مع المعلومات على أنها فارغة ، ولا يمكننا الاعتماد فقط على معلمات اختيارية لهذا الغرض. لذلك ، في مثل هذه الحالات ، لدينا طريقتان:



1. استخدم عبارات If في وحدة التحكم



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


لا يبدو جيدًا ، بل إنه غير مريح.



2. استخدم مخططات JSON  مباشرة على المسار



مرة أخرى ، يمكننا استخدام AJV أو @ expresso / Validator للتحقق من صحة هذه البيانات:



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)
    // ...
})


التعامل مع القيم الفارغة وغير المحددة



أنا شخصياً لست سعيدًا بفكرة استخدام كل من null و undefined في JavaScript لإثبات أن القيمة فارغة ، وذلك لعدة أسباب. بالإضافة إلى الصعوبات في جلب هذه المفاهيم إلى المستوى المجرد ، لا ينبغي لأحد أن ينسى المعلمات الاختيارية. إذا كان لا يزال لديك شكوك حول هذه المفاهيم ، دعني أقدم لك مثالًا رائعًا من الممارسة:







الآن بعد أن فهمنا التعريفات ، يمكننا القول أنه في عام 2020 ستكون هناك وظيفتان رئيسيتان في JavaScript: عامل الاندماج الصفري و تسلسل اختياري . لن أخوض في التفاصيل الآن ، لأنني  كتبت بالفعل مقالًا حول هذا الموضوع. (باللغة البرتغالية) ، لكن لاحظ أن هذين الابتكرين سيبسطان مهمتنا إلى حد كبير ، حيث يمكننا التركيز على هذين المفهومين ، وهما فارغان وغير معرفين باستخدام عامل التشغيل المناسب (؟؟) ، بدلاً من استخدام السلبيات المنطقية مثل! obj هذه أرض خصبة للأخطاء.



الدالات التي ترجع القيمة null بشكل ضمني



هذه المشكلة يصعب حلها بسبب طبيعتها الضمنية. تقوم بعض الوظائف بمعالجة البيانات على افتراض أنه سيتم توفيرها دائمًا ، ولكن في بعض الحالات ليس هذا هو الحال. لنفكر في مثال قياسي:



function foo (num) {
  return 23*num
}


إذا كانت قيمة num خالية ، فستكون نتيجة هذه الدالة 0 ، وهو ما لم يكن متوقعًا. في مثل هذه الحالات ، ليس لدينا خيار سوى اختبار الكود. هناك نوعان من الاختبارات التي يمكن إجراؤها. الأول هو استخدام عبارة if البسيطة:



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


الطريقة الثانية هي استخدام إما monad ، والتي تم تناولها بالتفصيل في المقالة التي ذكرتها. هذه طريقة رائعة للتعامل مع البيانات الغامضة ، أي البيانات التي قد تكون أو لا تكون فارغة. هذا لأن JavaScript يحتوي بالفعل على وظيفة مضمنة تدعم دفقين من الإجراءات - الوعد:



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)
}


هذه هي الطريقة التي يمكنك بها تفويض تعليمة catch من موجودة إلى الدالة التي تسمى foo:



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


واجهات برمجة التطبيقات الخارجية وسجلات قاعدة البيانات



هذه حالة شائعة جدًا ، خاصةً عندما تكون هناك أنظمة تم تطويرها من قواعد بيانات تم إنشاؤها أو ملؤها مسبقًا. على سبيل المثال ، منتج جديد يستخدم نفس قاعدة البيانات مثل سابقتها الناجحة ، وبالتالي يتم دمج المستخدمين من أنظمة مختلفة ، وما إلى ذلك.



المشكلة الكبيرة في هذا ليست حقيقة أن قاعدة البيانات غير معروفة - في الواقع ، هذا هو السبب ، لأننا لا نعرف ما تم القيام به على مستوى قاعدة البيانات ، ولا يمكننا تأكيد ما إذا كنا سنتلقى بيانات بقيمة خالية أم غير محددة أم لا ... لا يسعنا إلا أن نقول عن الوثائق ذات الجودة الرديئة عندما لا يتم توثيق قاعدة البيانات بشكل صحيح ونواجه نفس المشكلة كما كان من قبل.



لا يوجد شيء تقريبًا يمكننا القيام به هنا ، وأنا شخصياً أفضل التحقق من حالة البيانات للتأكد من أنه يمكنني العمل معها. ومع ذلك ، لا يمكنك التحقق من صحة جميع البيانات ، لأن العديد من الكائنات التي تم إرجاعها قد تكون كبيرة جدًا. لذلك ، قبل إجراء أي عمليات ، يوصى بفحص البيانات المتضمنة في تشغيل الوظيفة ، مثل الخريطة أو عامل التصفية ، لمعرفة ما إذا كانت غير محددة أم لا.



توليد الأخطاء



من الممارسات الجيدة استخدام وظائف التأكيد  لقواعد البيانات وواجهات برمجة التطبيقات الخارجية. بشكل أساسي ، تقوم هذه الوظائف بإرجاع البيانات ، إن وجدت ، ويتم إنشاء خطأ بخلاف ذلك. حالة الاستخدام الأكثر شيوعًا لهذا النوع من الوظائف هي عندما يكون لدينا واجهة برمجة تطبيقات ، على سبيل المثال للبحث عن نوع بيانات معين حسب المعرف ، فإن findById المعروفة:



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


استبدل الكيان باسم الكيان الخاص بك ، مثل UserNotFoundError.



هذا جيد ، حيث يمكن أن يكون لدينا وظيفة داخل نفس وحدة التحكم للعثور على المستخدمين حسب المعرف ووظيفة أخرى تستخدم هذا المستخدم للعثور على بيانات أخرى ، على سبيل المثال ، ملفات تعريف هذا المستخدم في مجموعة أخرى من قواعد البيانات. عند استدعاء وظيفة البحث عن ملف التعريف ، نستخدم التأكيد للتأكد من وجود المستخدم بالفعل في قاعدة البيانات الخاصة بنا. خلاف ذلك ، لن يتم تنفيذ الوظيفة حتى ويمكنك البحث عن الخطأ مباشرة على المسار:



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
}


لاحظ أننا لن نقوم باستدعاء قاعدة البيانات إذا لم يكن المستخدم موجودًا ، لأن الوظيفة الأولى تضمن وجود المستخدم. يمكننا الآن القيام بشيء كهذا في المسار:



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)
  }
}


يمكننا معرفة نوع الخطأ الذي تم إرجاعه بمجرد التحقق من اسم المثيل لفئة الخطأ الموجودة.



خاتمة



هناك عدة طرق لمعالجة البيانات لضمان التدفق المستمر والمتوقع للمعلومات. هل تعرف أي نصائح أخرى ؟! اتركها في التعليقات.



مثل المادة؟! هل تريد تقديم النصيحة أو إبداء الرأي أو إلقاء التحية فقط؟ إليك كيفية العثور علي على وسائل التواصل الاجتماعي:








تم نشر هذه المقالة في الأصل على dev.to بواسطة Lucas Santos. إذا كان لديك أي أسئلة أو تعليقات حول موضوع المقالة ، فقم بنشرها ضمن المقالة الأصلية على dev.to



All Articles