عندما نقوم بتطوير شيء ما، غالبًا ما نحتاج إلى فئات الأخطاء الخاصة بنا لتعكس أشياء محددة قد تسوء في مهامنا. بالنسبة للأخطاء في عمليات الشبكة، قد نحتاج إلى HttpError
، لعمليات قاعدة البيانات DbError
، لعمليات البحث NotFoundError
وما إلى ذلك.
يجب أن تدعم أخطائنا خصائص الخطأ الأساسية مثل message
name
، ويفضل أن stack
. ولكن قد يكون لها أيضًا خصائص أخرى خاصة بها، على سبيل المثال قد تحتوي كائنات HttpError
على خاصية statusCode
بقيمة مثل 404
أو 403
أو 500
.
تسمح JavaScript باستخدام throw
مع أي وسيطة، لذلك من الناحية الفنية لا تحتاج فئات الأخطاء المخصصة لدينا إلى أن ترث من Error
. ولكن إذا ورثنا، يصبح من الممكن استخدام obj instanceof Error
لتحديد كائنات الخطأ. لذا فالأفضل أن ترث منه.
مع نمو التطبيق، تشكل أخطائنا بشكل طبيعي تسلسلًا هرميًا. على سبيل المثال، قد يرث HttpTimeoutError
من HttpError
، وهكذا.
على سبيل المثال، دعونا نفكر في وظيفة readUser(json)
التي يجب أن تقرأ JSON مع بيانات المستخدم.
فيما يلي مثال لكيفية ظهور json
صالح:
Let json = `{ "name": "John"، "age": 30 }`;
داخليًا، سنستخدم JSON.parse
. إذا استقبل json
مشوهًا، فإنه يرمي SyntaxError
. ولكن حتى لو كان json
صحيحًا من الناحية النحوية، فهذا لا يعني أنه مستخدم صالح، أليس كذلك؟ قد تفوت البيانات الضرورية. على سبيل المثال، قد لا يحتوي على خصائص name
age
الضرورية لمستخدمينا.
لن تقوم وظيفتنا readUser(json)
بقراءة JSON فحسب، بل ستتحقق ("التحقق من صحة") البيانات. إذا لم تكن هناك حقول مطلوبة، أو كان التنسيق خاطئًا، فهذا خطأ. وهذا ليس SyntaxError
، لأن البيانات صحيحة من الناحية النحوية، ولكنه خطأ من نوع آخر. سوف نسميها ValidationError
ونقوم بإنشاء فئة لها. يجب أن يحمل خطأ من هذا النوع أيضًا معلومات حول المجال المخالف.
يجب أن ترث فئة ValidationError
الخاصة بنا من فئة Error
.
فئة Error
مدمجة، ولكن إليك رمزها التقريبي حتى نتمكن من فهم ما نقوم بتوسيعه:
// "الكود الزائف" لفئة الخطأ المضمنة التي تحددها JavaScript نفسها خطأ في الفئة { منشئ (رسالة) { this.message = message; this.name = "خطأ"; // (أسماء مختلفة لفئات الأخطاء المضمنة المختلفة) this.stack = <call stack>; // غير قياسي، لكن معظم البيئات تدعمه } }
والآن لنرث ValidationError
منه ونجربه عمليًا:
خطأ في فئة ValidationError يمتد { منشئ (رسالة) { سوبر(رسالة); // (1) this.name = "ValidationError"; // (2) } } اختبار الوظيفة () { رمي ValidationError جديد("عفوا!"); } يحاول { امتحان()؛ } قبض (يخطئ) { تنبيه (err.message)؛ // عفوًا! تنبيه (err.name)؛ // خطأ التحقق تنبيه (err.stack)؛ // قائمة بالمكالمات المتداخلة مع أرقام الأسطر لكل منها }
يرجى ملاحظة: في السطر (1)
نسمي المنشئ الأصلي. تتطلب جافا سكريبت منا استدعاء super
في المُنشئ الفرعي، لذلك يعد ذلك إلزاميًا. يقوم المنشئ الأصلي بتعيين خاصية message
.
يقوم المنشئ الأصلي أيضًا بتعيين خاصية name
على "Error"
، لذلك في السطر (2)
نقوم بإعادة تعيينها إلى القيمة الصحيحة.
دعنا نحاول استخدامه في readUser(json)
:
خطأ في فئة ValidationError يمتد { منشئ (رسالة) { سوبر(رسالة); this.name = "ValidationError"; } } // الاستخدام وظيفة قراءة المستخدم (جسون) { دع المستخدم = JSON.parse(json); إذا (!user.age) { طرح خطأ التحقق الجديد ("لا يوجد حقل: العمر")؛ } إذا (! اسم المستخدم) { طرح خطأ التحقق الجديد ("لا يوجد حقل: الاسم")؛ } عودة المستخدم؛ } // مثال عملي مع Try..catch يحاول { Let user = readUser('{ "age": 25 }'); } مسك (يخطئ) { إذا (خطأ في التحقق من صحة الخطأ) { تنبيه ("بيانات غير صالحة:" + err.message)؛ // بيانات غير صالحة: لا يوجد حقل: الاسم } else if (خطأ في مثيل SyntaxError) { // (*) تنبيه ("خطأ في بناء جملة JSON:" + err.message)؛ } آخر { رمي خطأ. // خطأ غير معروف، أعد وضعه (**) } }
تتعامل كتلة try..catch
الموجودة في الكود أعلاه مع كل من ValidationError
و SyntaxError
المدمج من JSON.parse
.
يرجى إلقاء نظرة على كيفية استخدامنا instanceof
للتحقق من نوع الخطأ المحدد في السطر (*)
.
يمكننا أيضًا أن ننظر إلى err.name
، على النحو التالي:
// ... // بدلاً من (خطأ في مثيل SyntaxError) } else if (err.name == "SyntaxError") {// (*) // ...
نسخة instanceof
أفضل بكثير، لأننا سنقوم في المستقبل بتوسيع ValidationError
وإنشاء أنواع فرعية منه، مثل PropertyRequiredError
. وسيستمر instanceof
الفحص في العمل مع الفئات الموروثة الجديدة. لذلك هذا دليل على المستقبل.
ومن المهم أيضًا أنه في حالة catch
خطأ غير معروف، فإنه يعيد طرحه في السطر (**)
. تعرف كتلة catch
فقط كيفية التعامل مع أخطاء التحقق من الصحة وأخطاء بناء الجملة، أما الأنواع الأخرى (الناجمة عن خطأ مطبعي في التعليمات البرمجية أو لأسباب أخرى غير معروفة) فيجب أن تفشل.
فئة ValidationError
عامة جدًا. أشياء كثيرة قد تسوء. قد تكون الخاصية غير موجودة أو قد تكون بتنسيق خاطئ (مثل قيمة سلسلة age
بدلاً من الرقم). لنقم بإنشاء فئة أكثر تحديدًا PropertyRequiredError
، خصيصًا للخصائص الغائبة. وسوف تحمل معلومات إضافية حول الممتلكات المفقودة.
خطأ في فئة ValidationError يمتد { منشئ (رسالة) { سوبر(رسالة); this.name = "ValidationError"; } } فئة PropertyRequiredError تمتد ValidationError { منشئ (خاصية) { super("لا توجد خاصية:" + property); this.name = "PropertyRequiredError"; this.property = property; } } // الاستخدام وظيفة قراءة المستخدم (جسون) { دع المستخدم = JSON.parse(json); إذا (!user.age) { طرح PropertyRequiredError الجديد("age"); } إذا (! اسم المستخدم) { طرح PropertyRequiredError الجديد("name"); } عودة المستخدم؛ } // مثال عملي مع Try..catch يحاول { Let user = readUser('{ "age": 25 }'); } مسك (يخطئ) { إذا (خطأ في التحقق من صحة الخطأ) { تنبيه ("بيانات غير صالحة:" + err.message)؛ // بيانات غير صالحة: لا توجد خاصية: الاسم تنبيه (err.name)؛ // PropertyRequiredError تنبيه (err.property)؛ // اسم } وإلا إذا (خطأ في مثيل SyntaxError) { تنبيه ("خطأ في بناء جملة JSON:" + err.message)؛ } آخر { رمي خطأ. // خطأ غير معروف، أعد رميه } }
الفئة الجديدة PropertyRequiredError
سهلة الاستخدام: نحتاج فقط إلى تمرير اسم الخاصية: new PropertyRequiredError(property)
. يتم إنشاء message
التي يمكن قراءتها بواسطة الإنسان بواسطة المُنشئ.
يرجى ملاحظة أنه تم تعيين this.name
في مُنشئ PropertyRequiredError
يدويًا مرة أخرى. قد يصبح ذلك مملاً بعض الشيء - لتعيين this.name = <class name>
في كل فئة خطأ مخصصة. يمكننا تجنب ذلك عن طريق إنشاء فئة "الخطأ الأساسي" الخاصة بنا والتي تقوم بتعيين this.name = this.constructor.name
. ثم نرث منه جميع أخطائنا المخصصة.
دعنا نسميها MyError
.
إليك الكود الذي يحتوي على MyError
وفئات الأخطاء المخصصة الأخرى، بشكل مبسط:
يمتد فئة MyError خطأ { منشئ (رسالة) { سوبر(رسالة); this.name = this.constructor.name; } } فئة ValidationError تمتد MyError { } فئة PropertyRequiredError تمتد ValidationError { منشئ (خاصية) { super("لا توجد خاصية:" + property); this.property = property; } } // الاسم صحيح تنبيه (جديد PropertyRequiredError("field").name ); // PropertyRequiredError
الآن أصبحت الأخطاء المخصصة أقصر بكثير، خاصة ValidationError
، حيث تخلصنا من السطر "this.name = ..."
في المُنشئ.
الغرض من وظيفة readUser
في الكود أعلاه هو "قراءة بيانات المستخدم". قد تحدث أنواع مختلفة من الأخطاء في العملية. لدينا الآن SyntaxError
و ValidationError
، ولكن في المستقبل قد تنمو وظيفة readUser
وربما تولد أنواعًا أخرى من الأخطاء.
يجب أن يتعامل الكود الذي يستدعي readUser
مع هذه الأخطاء. يستخدم الآن عدة if
في كتلة catch
، والتي تتحقق من الفصل وتتعامل مع الأخطاء المعروفة وتعيد إلقاء الأخطاء غير المعروفة.
المخطط هو مثل هذا:
يحاول { ... readUser() // مصدر الخطأ المحتمل ... } مسك (يخطئ) { إذا (خطأ في التحقق من صحة الخطأ) { // التعامل مع أخطاء التحقق من الصحة } وإلا إذا (خطأ في مثيل SyntaxError) { // التعامل مع الأخطاء النحوية } آخر { رمي خطأ. // خطأ غير معروف، أعد رميه } }
في الكود أعلاه يمكننا رؤية نوعين من الأخطاء، ولكن يمكن أن يكون هناك المزيد.
إذا كانت وظيفة readUser
تولد عدة أنواع من الأخطاء، فيجب أن نسأل أنفسنا: هل نريد حقًا التحقق من جميع أنواع الأخطاء واحدًا تلو الآخر في كل مرة؟
غالبًا ما تكون الإجابة "لا": نود أن نكون "فوق كل ذلك بمستوى واحد". نريد فقط معرفة ما إذا كان هناك "خطأ في قراءة البيانات" - غالبًا ما يكون سبب حدوثه غير ذي صلة (توضحه رسالة الخطأ). أو الأفضل من ذلك، أننا نرغب في الحصول على طريقة للحصول على تفاصيل الخطأ، ولكن فقط إذا كنا بحاجة إلى ذلك.
التقنية التي وصفناها هنا تسمى "استثناءات التغليف".
سنقوم بإنشاء فئة ReadError
جديدة لتمثيل خطأ "قراءة البيانات" العام.
ستكتشف الدالة readUser
أخطاء قراءة البيانات التي تحدث بداخلها، مثل ValidationError
و SyntaxError
، وستنشئ ReadError
بدلاً من ذلك.
سيحتفظ كائن ReadError
بمرجع الخطأ الأصلي في خاصية cause
به.
بعد ذلك، سيتعين على الكود الذي يستدعي readUser
التحقق من ReadError
فقط، وليس من كل أنواع أخطاء قراءة البيانات. وإذا كان يحتاج إلى مزيد من التفاصيل حول الخطأ، فيمكنه التحقق من خاصية cause
.
إليك الكود الذي يحدد ReadError
ويوضح استخدامه في readUser
و try..catch
:
يمتد خطأ فئة ReadError { منشئ (رسالة، سبب) { سوبر(رسالة); this.cause = Cause; this.name = 'ReadError'; } } خطأ في التحقق من الصحة يمتد إلى الخطأ { /*...*/ } فئة PropertyRequiredError تمتد ValidationError { /* ... */ } وظيفة التحقق من صحة المستخدم (المستخدم) { إذا (!user.age) { طرح PropertyRequiredError الجديد("age"); } إذا (! اسم المستخدم) { طرح PropertyRequiredError الجديد("name"); } } وظيفة قراءة المستخدم (جسون) { السماح للمستخدم؛ يحاول { المستخدم = JSON.parse(json); } مسك (يخطئ) { إذا (خطأ مثيل SyntaxError) { رمي خطأ قراءة جديد ("خطأ في بناء الجملة"، يخطئ)؛ } آخر { رمي خطأ. } } يحاول { validateUser(user); } مسك (يخطئ) { إذا (خطأ في التحقق من صحة الخطأ) { رمي خطأ قراءة جديد ("خطأ في التحقق من الصحة"، خطأ)؛ } آخر { رمي خطأ. } } } يحاول { readUser('{bad json}'); } قبض (ه) { إذا (مثال خطأ القراءة) { تنبيه (ه)؛ // الخطأ الأصلي: SyntaxError: الرمز المميز غير المتوقع b في JSON في الموضع 1 تنبيه ("الخطأ الأصلي:" + e.cause)؛ } آخر { رمي ه. } }
في الكود أعلاه، يعمل readUser
تمامًا كما هو موضح - يلتقط أخطاء في بناء الجملة والتحقق من الصحة ويرمي أخطاء ReadError
بدلاً من ذلك (يتم إعادة طرح الأخطاء غير المعروفة كالمعتاد).
لذا فإن الكود الخارجي يتحقق instanceof ReadError
وهذا كل شيء. لا حاجة لسرد جميع أنواع الأخطاء المحتملة.
يُطلق على هذا النهج اسم "التفاف الاستثناءات"، لأننا نأخذ استثناءات "المستوى المنخفض" و"نغلفها" في ReadError
الأكثر تجريدًا. يتم استخدامه على نطاق واسع في البرمجة الشيئية.
يمكننا أن نرث من Error
وفئات الخطأ الأخرى المضمنة بشكل طبيعي. نحتاج فقط إلى الاهتمام بخاصية name
ولا تنس الاتصال super
.
يمكننا استخدام instanceof
للتحقق من وجود أخطاء معينة. كما أنه يعمل بالميراث. لكن في بعض الأحيان يكون لدينا كائن خطأ قادم من مكتبة تابعة لجهة خارجية ولا توجد طريقة سهلة للحصول على فئته. ثم يمكن استخدام خاصية name
لمثل هذه الفحوصات.
تعد استثناءات الالتفاف أسلوبًا شائعًا: حيث تتعامل الوظيفة مع الاستثناءات ذات المستوى المنخفض وتنشئ أخطاء ذات مستوى أعلى بدلاً من الأخطاء المتنوعة ذات المستوى المنخفض. أحيانًا تصبح الاستثناءات ذات المستوى المنخفض خصائص لهذا الكائن مثل err.cause
في الأمثلة أعلاه، ولكن هذا ليس مطلوبًا تمامًا.
الأهمية: 5
قم بإنشاء فئة FormatError
ترث من فئة SyntaxError
المضمنة.
يجب أن يدعم خصائص message
name
stack
.
مثال الاستخدام:
Let err = new FormatError("خطأ في التنسيق"); تنبيه (err.message)؛ // خطأ في التنسيق تنبيه (err.name)؛ // خطأ في التنسيق تنبيه (err.stack)؛ // كومة تنبيه (خطأ في مثيل FormatError)؛ // حقيقي تنبيه (خطأ في مثيل SyntaxError)؛ // صحيح (لأنه يرث من خطأ SyntaxError)
يمتد فئة FormatError إلى SyntaxError { منشئ (رسالة) { سوبر(رسالة); this.name = this.constructor.name; } } Let err = new FormatError("خطأ في التنسيق"); تنبيه (err.message)؛ // خطأ في التنسيق تنبيه (err.name)؛ // خطأ في التنسيق تنبيه (err.stack)؛ // كومة تنبيه (خطأ في مثيل SyntaxError)؛ // حقيقي