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

كلاسات الاستثناءات المخصصة

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

كلاسات الاستثناءات المخصصة

الاستثناءات التي تأتي مع Java — مثل IllegalArgumentException وIOException وNullPointerException — استثناءات عامة. حين تقرأ في stack trace رسالة مثل IllegalArgumentException: invalid value لا تزال مضطرًا لتخمين أيّ قيمة كانت خاطئة ولماذا. كلاسات الاستثناءات المخصصة تحلّ هذه المشكلة: تعطي اسمًا دقيقًا للمشكلة، وتنتمي إلى نطاق عملك (domain)، وتستطيع حمل سياق إضافي لا تستطيعه الاستثناءات العامة.

لماذا تُنشئ استثناءاتك الخاصة؟

  • أسماء ذات معنى. InsufficientFundsException يوصل المشكلة فورًا؛ بينما RuntimeException: balance too low لا يوصلها بنفس الوضوح.
  • التقاط انتقائي. يستطيع المُستدعي (caller) التقاط الفشل المحدد الذي يعرف كيف يتعامل معه دون أن يُخمد استثناءات أخرى غير ذات صلة.
  • حمل بيانات النطاق. يستطيع الكلاس المخصص تخزين رقم الحساب والمبلغ المطلوب والرصيد الحالي — لا مجرد رسالة نصية.
  • واجهات برمجية أنظف. توقيع دالة مثل throws InsufficientFundsException يوثّق القصد بشكل أفضل بكثير من throws Exception.

توسيع Exception مقابل RuntimeException

هذا هو أهمّ قرار في التصميم. تنطبق هنا القاعدة التي درستها في الدرس الرابع:

  • وسّع Exception (استثناء محقق) حين ينبغي للمُستدعي معالجة الفشل — ملف غير موجود، انتهاء مهلة الشبكة، انتهاكات قواعد عمل يمكن التعافي منها.
  • وسّع RuntimeException (استثناء غير محقق) حين يكون الفشل خطأ برمجيًا — وسيط غير صالح، حالة خاطئة، عقد مكسور — لا يجب على المُستدعي التقاطه في العادة.
أخطاء منطق النطاق عادةً محققة (checked). إذا كانت دالة التحويل البنكي قد تفشل فعلًا لأن رصيد المستخدم غير كافٍ، فتلك حالة قابلة للتعافي يجب على واجهة المستخدم التعامل معها — لذا InsufficientFundsException extends Exception هو الخيار الصحيح. أما إذا استقبلت أداة داخلية قيمة null لا ينبغي أن تصلها أبدًا، فـextends RuntimeException أنسب.

النمط الأدنى

يحتاج كل استثناء مخصص على الأقل إلى مُنشئ (constructor) يقبل رسالة ويمرّرها للكلاس الأب:

public class InsufficientFundsException extends Exception { public InsufficientFundsException(String message) { super(message); } }

هذا كل ما تحتاجه لرمي الاستثناء والتقاطه بالاسم:

public void withdraw(double amount) throws InsufficientFundsException { if (amount > balance) { throw new InsufficientFundsException( "Cannot withdraw " + amount + "; balance is " + balance ); } balance -= amount; }

إضافة مُنشئ للسبب (cause)

احرص دائمًا على توفير مُنشئ يقبل Throwable كسبب. يتيح ذلك تغليف استثناءات المستوى الأدنى دون فقدان stack trace الأصلي — وهي ممارسة تعلمتها في الدرس السادس (إعادة الرمي).

public class DataAccessException extends RuntimeException { public DataAccessException(String message) { super(message); } public DataAccessException(String message, Throwable cause) { super(message, cause); } }

تخزين بيانات خاصة بالنطاق

يمكن للكلاس المخصص الاحتفاظ بأي حقول تساعد المُستدعين على الاستجابة بذكاء للفشل:

public class InsufficientFundsException extends Exception { private final double requested; private final double available; public InsufficientFundsException(double requested, double available) { super("Requested " + requested + " but only " + available + " available"); this.requested = requested; this.available = available; } public double getRequested() { return requested; } public double getAvailable() { return available; } public double getShortfall() { return requested - available; } }

يستطيع المُستدعي الآن عرض رسالة مفيدة للمستخدم دون الحاجة إلى تحليل نص:

try { account.withdraw(500.00); } catch (InsufficientFundsException e) { System.out.println("Transfer failed. You need " + e.getShortfall() + " more to complete this transaction."); }

بناء تسلسل هرمي صغير للاستثناءات

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

// الكلاس الأساسي لجميع أخطاء الدفع public class PaymentException extends Exception { public PaymentException(String message) { super(message); } public PaymentException(String message, Throwable cause) { super(message, cause); } } // الأنواع المحددة public class InsufficientFundsException extends PaymentException { public InsufficientFundsException(double shortfall) { super("Short by " + shortfall); } } public class CardDeclinedException extends PaymentException { public CardDeclinedException(String reason) { super("Card declined: " + reason); } }

يستطيع معالج بوابة الدفع التقاط جميع مشاكل الدفع بـcatch (PaymentException e)، بينما تستطيع وحدة الاسترداد التي تهتم فقط بالبطاقات المرفوضة التقاط CardDeclinedException تحديدًا.

أبقِ التسلسل الهرمي ضحلًا. مستوى واحد من التخصص (قاعدة + أنواع فرعية محددة) يكفي في غالب الأحيان. التسلسلات الهرمية العميقة يصعب التنقل فيها ونادرًا ما تضيف قيمة.

اتفاقية التسمية

يجب أن ينتهي اسم كل كلاس استثناء مخصص بـException — مثل ValidationException وOrderNotFoundException وRateLimitExceededException. يتوقع مطوّرو Java ذلك؛ كسر الاتفاقية يُربك القارئ.

لا توسّع Error أو Throwable مباشرة. Error محجوز لأخطاء JVM (نفاد الذاكرة، تجاوز stack). توسيع Throwable مباشرة يتجاوز التمييز بين المحقق وغير المحقق ويكسر افتراضات اللغة. وسّع دائمًا Exception أو RuntimeException.

الخلاصة

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