معالجة الاستثناءات

أفضل الممارسات والأنماط المضادة

15 دقيقة الدرس 9 من 14

أفضل الممارسات والأنماط المضادة

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

١. لا تبتلع الاستثناءات أبدًا

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

// سيء — الاستثناء اختفى؛ لن تعرف أبدًا ما الذي حدث خطأ try { loadConfig("app.properties"); } catch (IOException e) { // متروك فارغًا عمدًا -- لا تفعل هذا }

لماذا هذا خطير؟ افترض أن loadConfig فشلت لأن الملف مفقود. يستمر البرنامج بقيم افتراضية (أو null)، وينتج مخرجات خاطئة، وينهار في مكان لا علاقة له بالمشكلة الأصلية — مما يجعل السبب الجذري يكاد يكون مستحيل الإيجاد.

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

البدائل الآمنة الدنيا:

// الخيار أ — سجّله ثم أعد رميه كاستثناء غير محقَّق try { loadConfig("app.properties"); } catch (IOException e) { System.err.println("فشل تحميل الإعدادات: " + e.getMessage()); throw new RuntimeException("لا يمكن البدء بدون إعدادات", e); } // الخيار ب — سجّل واسترد بشكل متعمّد (وثّق السبب) try { loadConfig("app.properties"); } catch (IOException e) { System.err.println("الإعدادات مفقودة، استخدام القيم الافتراضية: " + e.getMessage()); applyDefaults(); }

الخيار ب مقبول فقط عندما يكون لديك استراتيجية استرداد حقيقية وتوثّقها بوضوح.

٢. أفشل بسرعة

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

// سيء — لا يتحقّق من شيء؛ NullPointerException يظهر بعد 10 استدعاءات public void processOrder(Order order) { double tax = order.getTotal() * TAX_RATE; shipOrder(order); } // جيد — تحقّق عند نقطة الدخول، أفشل فورًا برسالة واضحة public void processOrder(Order order) { if (order == null) { throw new IllegalArgumentException("الطلب يجب ألا يكون null"); } if (order.getTotal() < 0) { throw new IllegalArgumentException("إجمالي الطلب لا يمكن أن يكون سالبًا: " + order.getTotal()); } double tax = order.getTotal() * TAX_RATE; shipOrder(order); }
استخدم Objects.requireNonNull للتحقّق من القيم الفارغة. يرمي NullPointerException برسالتك وهو معروف فورًا للمطوّرين الآخرين:

Objects.requireNonNull(order, "order must not be null");

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

٣. الق الأنواع المحددة — لا Exception أو Throwable

التقاط نوع عام يُخفي كل التفاصيل التي تحتاجها للاستجابة بشكل صحيح.

// سيء — يلتقط كل شيء، بما في ذلك OutOfMemoryError وأخطاء البرمجة try { processPayment(card, amount); } catch (Exception e) { System.out.println("حدث خطأ ما"); }

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

// جيد — تعامل مع كل وضع فشل بشكل متعمّد try { processPayment(card, amount); } catch (InsufficientFundsException e) { notifyUser("تم رفض بطاقتك: رصيد غير كافٍ."); } catch (NetworkTimeoutException e) { scheduleRetry(card, amount); log.warn("انتهت مهلة الدفع، تمت جدولة إعادة المحاولة", e); } catch (PaymentGatewayException e) { log.error("خطأ في بوابة الدفع", e); throw new ServiceUnavailableException("خدمة الدفع معطّلة", e); }

كل فرع يقوم بالشيء الصحيح لذلك الفشل المحدد. لا يمكنك فعل ذلك عندما يكون كل شيء مجرد Exception.

لا تلتقط Throwable أبدًا إلا إذا كنت تكتب بنية تحتية للإطار (مثل مشرف مجموعة الخيوط). يشمل Throwable أصنافًا فرعية من Error مثل OutOfMemoryError وStackOverflowError — حالات تكون فيها JVM نفسها في مشكلة. ابتلاع تلك الأخطاء يجعل الأمور أسوأ.

٤. سجّل بسياق كافٍ

عندما يقع استثناء، نادرًا ما تكون e.getMessage() الوحيدة كافية لتشخيصه في الإنتاج. أدرج دائمًا القيم التي أدّت إلى الفشل.

// سيء — "User not found" لا يخبرك بأي مستخدم أو ما الذي استدعى هذا } catch (UserNotFoundException e) { log.error("المستخدم غير موجود"); } // جيد — أدرج المعرّف والعملية واحتفظ بتتبّع المكدس } catch (UserNotFoundException e) { log.error("فشل تحميل المستخدم ذو id={} أثناء معالجة الطلب", userId, e); }

مرّر دائمًا كائن الاستثناء كآخر وسيط للمسجّل حتى يُسجَّل تتبّع المكدس الكامل. تتبّعات المكدس تُريك السطر الدقيق من الكود الذي فشل — حذفها يشبه حذف تقرير الانهيار.

استخدم إطار تسجيل حقيقي — SLF4J مع Logback أو Log4j2 — لا System.out.println. سجلات الإنتاج تحتاج إلى مستويات (DEBUG/INFO/WARN/ERROR) وطوابع زمنية وأسماء خيوط وتدوير ملفات. println لا يوفّر أيًا من ذلك.

جمع كل شيء معًا

إليك طريقة واقعية تطبّق المبادئ الأربعة:

import java.util.Objects; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class OrderService { private static final Logger log = LoggerFactory.getLogger(OrderService.class); public void submitOrder(String userId, Order order) { // أفشل بسرعة — تحقّق عند الحدود Objects.requireNonNull(userId, "userId must not be null"); Objects.requireNonNull(order, "order must not be null"); if (order.getTotal() < 0) { throw new IllegalArgumentException("إجمالي الطلب سالب: " + order.getTotal()); } try { // الق الأنواع المحددة paymentGateway.charge(userId, order.getTotal()); } catch (InsufficientFundsException e) { // سجّل بسياق — أدرج userId والمبلغ log.warn("تم رفض الشحن للمستخدم={} المبلغ={}", userId, order.getTotal(), e); throw new OrderDeclinedException("تم رفض الدفع للمستخدم " + userId, e); } catch (GatewayUnavailableException e) { log.error("بوابة الدفع معطّلة للمستخدم={}", userId, e); throw new ServiceException("خدمة الدفع غير متاحة", e); } // لا ابتلاع — كل المسارات إما تسترد بشكل صريح أو تعيد الرمي } }

مرجع سريع: الأنماط المضادة التي يجب تجنّبها

  • كتلة catch فارغة — تختفي الاستثناءات بصمت.
  • التقاط Exception أو Throwable — يُخفي الأخطاء البرمجية وأخطاء JVM.
  • التسجيل بدون كائن الاستثناء — يضيع تتبّع المكدس.
  • التسجيل وإعادة الرمي بدون تغليف — يُسجَّل نفس الخطأ مرتين في كل طبقة.
  • استخدام الاستثناءات للتحكّم في التدفق الطبيعي — مثل رمي استثناء عندما تكون قائمة فارغة بدلًا من إعادة قائمة فارغة. الاستثناءات بطيئة وغير صحيحة دلاليًا للنتائج المتوقعة.
  • التقاط استثناء محقَّق فقط لإرضاء المُجمِّع — لفّه في استثناء غير محقَّق ذي معنى بدلًا من ابتلاعه.
ازدواجية التسجيل وإعادة الرمي: إذا سجّلت استثناءً ثم أعدت رميه، تفعل كل طبقة فوقه نفس الشيء، وينتهي ملف السجل بنفس تتبّع المكدس خمس مرات. القاعدة: سجّل مرة واحدة، في الطبقة التي تعالجه. جميع الطبقات الأخرى فقط تعيد الرمي (ملفوفًا إذا لزم الأمر).

الخلاصة

المعالجة الجيدة للاستثناءات لا تتعلّق بكتابة المزيد من كتل try/catch — بل تتعلّق بكتابة كتل متعمّدة. لا تبتلع الاستثناءات أبدًا؛ اسمح دائمًا للإخفاقات بأن تكون مرئية. أفشل بسرعة عن طريق التحقّق من المدخلات عند الحدود. الق النوع الأكثر تحديدًا الذي يتطابق مع الفشل حتى تتمكّن من الاستجابة بشكل صحيح. سجّل بسياق كافٍ — بما في ذلك كائن الاستثناء — حتى يتمكّن المطوّر من تشخيص المشكلة من السجل وحده. هذه العادات تُميّز الكود القوي والمحافظ عليه من الكود الهشّ الذي يُفسد الحالة بصمت وينتج أخطاءً محيّرة بعد أشهر.