كيفية العمل مع استثناءات في DDD

الصورة

كجزء من مؤتمر DotNext 2018 الأخير ، تم عقد BoF في مجال تصميم النطاقات. تناولت مسألة العمل مع استثناءات ، والتي تسببت في نقاش ساخن ، لكنها لم تتلق مناقشة مفصلة ، لأنها ليست الموضوع الرئيسي.

أيضًا ، من خلال دراسة الكثير من الموارد ، بدءًا من الأسئلة حول تدفق stackover وتنتهي بدورات الهندسة المعمارية المدفوعة ، يمكنك ملاحظة أن مجتمع تكنولوجيا المعلومات لديه موقف غامض فيما يتعلق بالاستثناءات وكيفية استخدامها.

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

هناك آراء مختلفة حول ما إذا كنت تريد إنشاء أنواع الاستثناءات الخاصة بك أو استخدام الأنواع القياسية المتوفرة في .NET.

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

سوف أتحدث في هذه المقالة عن الممارسات التي اعتمدها فريقنا (باختصار - نستخدم جميع الأساليب وليس أي منها عقيدة).

سنتحدث عن تطبيق مؤسسة مبني على أساس ASP.NET MVC + WebAPI. تم بناء التطبيق العمارة البصل ، ويتواصل مع قاعدة البيانات وسيط الرسالة. ويستخدم تسجيل منظم إلى مكدس ELK ويتم تكوين المراقبة باستخدام Grafana.

سننظر في العمل مع استثناءات من ثلاث وجهات نظر:

  1. قواعد الاستثناء العامة
  2. الاستثناءات والأخطاء وهندسة البصل
  3. حالات خاصة لتطبيقات الويب

قواعد الاستثناء العامة


  1. الاستثناءات والأخطاء ليست هي الشيء نفسه. بالنسبة للاستثناءات ، نستخدم الاستثناءات ، للأخطاء - النتيجة.
  2. الاستثناءات هي فقط للحالات الاستثنائية ، والتي بحكم تعريفها لا يمكن أن تكون كثيرة. لذلك أقل استثناءات ، كان ذلك أفضل.
  3. يجب أن تكون معالجة الاستثناء محببة قدر الإمكان. كما كتب ريختر في عمله الضخم.
  4. إذا كان يجب تسليم الخطأ للمستخدم في شكله الأصلي - استخدم النتيجة.
  5. يجب ألا يترك الاستثناء حدود النظام في شكله الأصلي. هذه ليست سهلة الاستخدام وتمنح المهاجم طريقة لاستكشاف نقاط الضعف المحتملة في النظام.
  6. إذا تم معالجة الاستثناء الذي تم طرحه بواسطة تطبيقنا ، فنحن لا نستخدم استثناء ، ولكن النتيجة. سيتم إخفاء التنفيذ على الاستثناءات بواسطة مشغل goto ، وكلما كان الأمر أسوأ ، كلما زاد رمز المعالجة عن رمز رمي الاستثناء. النتيجة تعلن بوضوح عن احتمال وجود خطأ وتسمح فقط بمعالجتها "الخطية".

الاستثناءات والأخطاء وهندسة البصل


في الأقسام التالية ، سننظر في المسؤوليات والقواعد الخاصة برمي / معالجة الاستثناءات / الأخطاء للطبقات التالية:

  • تستضيف التطبيق
  • البنية التحتية
  • خدمات التطبيق
  • المجال الأساسية

مضيف التطبيق


ما هو المسؤول عن

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

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

كيفية التعامل مع الأخطاء من النتيجة

البث إلى العالم الخارجي ، والتحويل إلى التنسيق المناسب (على سبيل المثال ، في استجابة http).

كيف يولد النتيجة

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

كيفية التعامل مع الاستثناءات

  1. إخفاء التفاصيل وتحويلها إلى تنسيق مناسب للإرسال إلى العالم الخارجي
  2. يسجل في.

كيفية رمي الاستثناءات

بأي حال من الأحوال ، هذه الطبقة هي الخارجية ولا تحتوي على منطق - لا يوجد أحد لرمي استثناء لها.

البنية التحتية


ما هو المسؤول عن

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

كيفية التعامل مع الأخطاء من النتيجة

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

كيف يولد النتيجة

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

كيفية التعامل مع الاستثناءات

في الحالة العامة ، فإنه يرميها أبعد من ذلك ، إذا لزم الأمر ، بتأمين التفاصيل. إذا كان المنفذ الجاري تنفيذه يتيح إرجاع النتيجة في العقد ، فسوف تتحول البنية التحتية إلى نتيجة تلك الأنواع من الاستثناءات التي يمكن معالجتها.

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

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

باستخدام هذا النهج ، نحصل على ميزتين:

  1. تعلن صراحة عن احتمال وجود أخطاء في العقد.
  2. نتخلص من الموقف عندما تعرف خدمة التطبيقات كيفية التعامل مع الخطأ ، لكن لا تعرف نوع الاستثناء ، لأنه مستخلص من وسيط رسائل محدد. لإنشاء كتلة catch على قاعدة System.Exception يعني التقاط جميع أنواع الاستثناءات ، وليس فقط تلك التي يمكن لخدمة التطبيق التعامل معها.

كيفية رمي الاستثناءات

يعتمد على تفاصيل النظام.

على سبيل المثال ، عبارات LINQ Single و First رمي InvalidOperationException عند طلب بيانات غير موجودة. لكن هذا النوع من الاستثناء يستخدم في كل مكان في .NET ، مما يجعل من المستحيل معالجته بشكل محبب.

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

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

خدمات التطبيق


ما هو المسؤول عن

  1. التحقق من صحة إدخال البيانات.
  2. تنسيق وتنسيق الخدمات - بداية ونهاية المعاملات ، وتنفيذ البرامج النصية الموزعة ، إلخ.
  3. قم بتنزيل كائنات المجال والبيانات الخارجية عبر منافذ إلى البنية التحتية ، استدعاء لاحق للأوامر في المجال الأساسي.

كيفية التعامل مع الأخطاء من النتيجة

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

كيف يولد النتيجة

يمكن تنفيذ التحقق من الصحة نتيجة.

يمكن أن تولد إخطارات النجاح الجزئي للعملية. على سبيل المثال ، تم إرسال رسائل إلى مستخدم مثل "تم تقديم طلبك بنجاح ، ولكن حدث خطأ أثناء التحقق من عنوان التسليم. سيتصل بك أحد المتخصصين قريبًا لتوضيح تفاصيل التسليم. "

كيفية التعامل مع الاستثناءات

على افتراض أن استثناءات البنية الأساسية التي يستطيع التطبيق التعامل معها قد تم تحويلها بالفعل بواسطة طبقة البنية التحتية إلى نتيجة ، فإنه لا يعالجها على الإطلاق.

كيفية رمي الاستثناءات

بشكل عام ، لا مفر. لكن هناك خيارات حدودية موصوفة في القسم الأخير من المقالة.

المجال الأساسية


ما هو المسؤول عن

تنفيذ منطق الأعمال ، "جوهر" النظام والمعنى الرئيسي لوجوده.

كيفية التعامل مع الأخطاء من النتيجة

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

كيف يولد النتيجة

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

كيفية التعامل مع الاستثناءات

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

كيفية رمي الاستثناءات

عادة ما تعمل قاعدة عامة هنا: الاستثناءات الأقل - كلما كان ذلك أفضل.

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

كقاعدة عامة ، نحن نتحدث عن تنفيذ أوامر غير صالحة للحالة الحالية للكائن.

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

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

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

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

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

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

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

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

حالات خاصة لتطبيقات الويب


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

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

لتنفيذ هذا السلوك ، تم بناء معالجة الطلبات في الأنظمة الأساسية للويب في شكل أنابيب. أولاً ، تتم معالجة الطلب بالتتابع (طلب) ، ثم يتم إعداد الاستجابة.

يمكننا استخدام الوسيطة أو مرشح الإجراءات أو معالج HTTP أو مرشح ISAPI (حسب النظام الأساسي) والاندماج في خط الأنابيب هذا في أي مرحلة. وفي أي مرحلة من مراحل معالجة الطلب ، يمكننا مقاطعة المعالجة وسيسير خط الأنابيب لتشكيل استجابة.

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

ما علاقة كل هذا بمعالجة الاستثناء ، تسأل؟

الحقيقة هي أن قواعد العمل مع الاستثناءات الموضحة في الأجزاء السابقة من المقالة لا تتوافق بشكل جيد مع هذا السيناريو.

الاستثناءات سيئة الاستخدام لأنها دلالات goto.

يؤدي الاستخدام الواسع النطاق للنتائج إلى حقيقة قيامنا بسحبها (النتيجة) عبر جميع طبقات التطبيق ، وعند تشكيل الإجابة ، نحتاج إلى تحليل النتيجة بطريقة أو بأخرى من أجل فهم كود الحالة المراد إرجاعه. يُنصح أيضًا بتعميم ودفع رمز التحليل هذا إلى Middleware أو ActionFilter ، والتي تصبح مغامرة منفصلة. وهذا هو ، النتيجة ليست أفضل بكثير من الاستثناءات.

ماذا تفعل في مثل هذه الحالة؟

لا تبني مطلقة. وضعنا القواعد لمصلحتنا الخاصة ، وليس على حساب.

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

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

في وقت سابق ، ذكرنا نوعين مخصصين نستخدمهما: ItemNotFoundException (التحويل إلى 404) و CorruptedInvariant (التحويل إلى 500).

إذا قمت بفحص حقوق المستخدمين ، لأنهم لا يندرجون في نموذج الدور أو المطالبة ، فيجوز إنشاء ForbiddenException مخصص (رمز الحالة 403).

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

هذا كل شيء. كيف تعمل مع استثناءات؟

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


All Articles