التصميم بموجب العقد باستخدام التأكيدات أو الاستثناءات؟[مغلق]

StackOverflow https://stackoverflow.com/questions/117171

سؤال

عند البرمجة بموجب عقد، تقوم وظيفة أو طريقة بالتحقق أولاً من استيفاء شروطها المسبقة، قبل البدء في العمل على مسؤولياتها، أليس كذلك؟أبرز طريقتين لإجراء هذه الفحوصات هما assert وبواسطة exception.

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

أيهما تعتقد أنه الأفضل؟

انظر السؤال ذو الصلة هنا

هل كانت مفيدة؟

المحلول

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

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

نصائح أخرى

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

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

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

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

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

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

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

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

ليس صحيحًا تمامًا أن "التأكيد يفشل فقط في وضع التصحيح".

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

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

هل يجب عليك دائمًا ترك عمليات التحقق من الشروط المسبقة ممكّنة؟هذا يعتمد.الأمر متروك لك.لا توجد إجابة عالمية.إذا كنت تصنع برنامجًا لأحد البنوك، فقد يكون من الأفضل إيقاف التنفيذ برسالة إنذار بدلاً من تحويل مليون دولار بدلاً من 1000 دولار.ولكن ماذا لو كنت تبرمج لعبة؟ربما تحتاج إلى كل السرعة التي يمكنك الحصول عليها، وإذا حصل شخص ما على 1000 نقطة بدلاً من 10 بسبب خطأ لم تتمكن الشروط المسبقة من اكتشافه (لأنه لم يتم تمكينه)، فسيكون الحظ سيئًا.

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

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

أنظر أيضا: متى يجب أن تبقى التأكيدات في كود الإنتاج؟

كان هناك ضخمة خيط فيما يتعلق بتمكين/تعطيل التأكيدات في الإصدار يعتمد على comp.lang.c++.moderated، والذي إذا كان لديك بضعة أسابيع يمكنك رؤية مدى تنوع الآراء حول هذا الأمر.:)

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

  1. يموت مع نوع من الفشل في نوع نظام التشغيل، مما يؤدي إلى استدعاء للإجهاض.(بدون تأكيد)
  2. يموت عبر مكالمة مباشرة للإجهاض.(مع التأكيد)

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

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

ومع ذلك، أنا شخصياً لست متأكداً من أنني سأختار استثناءات لهذه الحالة أيضاً.تعتبر الاستثناءات أكثر ملاءمة للمكان الذي يمكن أن يحدث فيه شكل مناسب من الاسترداد.على سبيل المثال، قد يكون الأمر أنك تحاول تخصيص الذاكرة.عند حدوث استثناء 'std::bad_alloc'، قد يكون من الممكن تحرير الذاكرة والمحاولة مرة أخرى.

وقد أوجزت وجهة نظري حول حالة الأمر هنا: كيف يمكنك التحقق من صحة الحالة الداخلية للكائن؟ .بشكل عام، قم بتأكيد ادعاءاتك ورفض انتهاك الآخرين.لتعطيل التأكيدات في إصدارات الإصدار، يمكنك القيام بما يلي:

  • تعطيل التأكيدات الخاصة بعمليات التحقق الباهظة الثمن (مثل التحقق مما إذا كان النطاق قد تم طلبه)
  • حافظ على تمكين عمليات التحقق التافهة (مثل التحقق من وجود مؤشر فارغ أو قيمة منطقية)

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

أنت تسأل عن الفرق بين أخطاء وقت التصميم وأخطاء وقت التشغيل.

التأكيدات عبارة عن إشعارات "مرحبًا أيها المبرمج، هذا معطل"، وهي موجودة لتذكيرك بالأخطاء التي لم تكن لتلاحظها عند حدوثها.

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

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

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

وأخيرًا، لن أستخدم أيًا منهما - فرموز الخطأ أفضل بكثير بالنسبة للأخطاء التي تعتقد أنها تحدث بانتظام.:)

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

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

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

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

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

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

#ifdef DEBUG
#define RETURN_IF_FAIL(expr)      do {                      \
 if (!(expr))                                           \
 {                                                      \
     fprintf(stderr,                                        \
        "file %s: line %d (%s): precondition `%s' failed.", \
        __FILE__,                                           \
        __LINE__,                                           \
        __PRETTY_FUNCTION__,                                \
        #expr);                                             \
     ::print_stack_trace(2);                                \
     return;                                                \
 };               } while(0)
#define RETURN_VAL_IF_FAIL(expr, val)  do {                         \
 if (!(expr))                                                   \
 {                                                              \
    fprintf(stderr,                                             \
        "file %s: line %d (%s): precondition `%s' failed.",     \
        __FILE__,                                               \
        __LINE__,                                               \
        __PRETTY_FUNCTION__,                                    \
        #expr);                                                 \
     ::print_stack_trace(2);                                    \
     return val;                                                \
 };               } while(0)
#else
#define RETURN_IF_FAIL(expr)
#define RETURN_VAL_IF_FAIL(expr, val)
#endif

إذا كنت بحاجة إلى التحقق من الوسائط في وقت التشغيل، فسأفعل هذا:

char *doSomething(char *ptr)
{
    RETURN_VAL_IF_FAIL(ptr != NULL, NULL);  // same as assert(ptr != NULL), but returns NULL if it fails.
                                            // Goes away when debug off.

    if( ptr != NULL )
    {
       ...
    }

    return ptr;
}

حاولت تجميع العديد من الإجابات الأخرى هنا مع آرائي الخاصة.

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

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

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

أنظر أيضا هذا السؤال:

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

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

عادةً ما أرمي استثناءًا في السترة لتولي دور التأكيد في حالة تعطيلها

assert(value>0);
if(value<=0) throw new ArgumentOutOfRangeException("value");
//do stuff

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

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

لفهم كيف تتلاءم التأكيدات مع برنامج ما ، فكر في روتين في برنامج C ++ الذي على وشك أن يكون مؤشرًا.الآن هل يجب أن يكون الاختبار الروتيني ما إذا كان المؤشر فارغًا قبل التخلص من الإخلاء ، أو هل يجب أن يؤكد أن المؤشر ليس فارغًا ثم يمضي قدماً ويصيبه بغض النظر؟

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

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

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

ملاحظة:قد ترغب في التحقق من سؤال مماثل: الاستثناء مقابل التأكيد.

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top