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

مشروع: معالجة المدخلات بشكل موثوق

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

مشروع: معالجة المدخلات بشكل موثوق

قضيت الدروس التسعة الماضية تتعلّم كل جانب من جوانب معالجة الاستثناءات في Java — من أساسيات try/catch وصولًا إلى فئات الاستثناء المخصّصة وtry-with-resources. هذا الدرس الأخير يجمع كل شيء في مشروع صغير واقعي: تطبيق وحدة طرفية يظل يطلب من المستخدم إدخال بيانات حتى يقدّم شيئًا صالحًا، ويُشير إلى الإدخال السيئ برمي استثناء مخصّص.

التطبيقات الحقيقية نادرًا ما تتعطّل بسبب إدخال خاطئ — بل تخبر المستخدم بما حدث وتطلب منه المحاولة مرة أخرى. هذا هو النمط الذي ستبنيه هنا.

ما الذي نبنيه

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

  1. يرمي InvalidInputException (استثناء مخصّص مُدقَّق تعرّفه بنفسك).
  2. يلتقطه، يطبع رسالة ودّية، ويعود للحلقة للطلب مرة أخرى.
  3. يخرج من الحلقة فقط عند استلام إدخال صالح.

الخطوة الأولى — تعريف الاستثناء المخصّص

من الدرس السابع تعلمت أن الاستثناء المخصّص هو ببساطة فئة تمتد من Exception (مُدقَّق) أو RuntimeException (غير مُدقَّق). نستخدم هنا استثناءً مُدقَّقًا لأن المُستدعِي يجب أن يعالج الإدخال السيئ — فهو جزء متوقّع من تدفق التنفيذ.

public class InvalidInputException extends Exception { private final String userInput; public InvalidInputException(String message, String userInput) { super(message); this.userInput = userInput; } public String getUserInput() { return userInput; } }
لماذا نخزّن الإدخال الخام؟ الاحتفاظ بالقيمة السيئة داخل الاستثناء يتيح لأي كود يلتقطه أن يُظهر للمستخدم بالضبط ما كتبه، دون الحاجة لتمريره كمتغير منفصل.

الخطوة الثانية — دالة التحقق التي ترمي الاستثناء

افصل منطق التحقق في دالة خاصة به. تقبل سلسلة نصية خام String، تحاول تحليلها، تتحقق من النطاق، وإما تُعيد عددًا صحيحًا int نظيفًا أو ترمي InvalidInputException. إعلان throws يجعل العقد صريحًا.

public static int parseAndValidate(String raw) throws InvalidInputException { int value; try { value = Integer.parseInt(raw.trim()); } catch (NumberFormatException e) { throw new InvalidInputException( "\"" + raw.trim() + "\" is not a whole number.", raw); } if (value < 1 || value > 100) { throw new InvalidInputException( value + " is out of range. Enter a number between 1 and 100.", raw); } return value; }
التغليف لا الابتلاع. لاحظ كيف يُلتقط NumberFormatException ثم يُرمى فورًا كـ InvalidInputException. الاستثناء منخفض المستوى يُمتَص، ويُرفع بدلًا منه استثناء على مستوى النطاق. يرى المستخدم "ليس عددًا صحيحًا"، لا تتبّع مكدس خام.

الخطوة الثالثة — حلقة الإدخال

الحلقة الرئيسية تعمل إلى ما لا نهاية (while (true)) وتنكسر فقط عند وصول إدخال صالح. في كل تكرار تطلب إدخالًا، تقرأ سطرًا، تستدعي المتحقِّق، وإما تخزّن النتيجة وتخرج، أو تلتقط الاستثناء وتطبع الخطأ قبل العودة للتكرار.

import java.util.Scanner; public class RobustInputDemo { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); int validNumber = 0; System.out.println("=== Robust Input Demo ==="); while (true) { System.out.print("Enter a number between 1 and 100: "); String line = scanner.nextLine(); try { validNumber = parseAndValidate(line); break; // يُصل إليه فقط عند النجاح } catch (InvalidInputException e) { System.out.println(" [Error] " + e.getMessage()); System.out.println(" You typed: \"" + e.getUserInput() + "\""); System.out.println(" Please try again.\n"); } } System.out.println("Great! You entered: " + validNumber); scanner.close(); } // تذهب parseAndValidate() هنا (راجع الخطوة الثانية) }

مثال على تشغيل البرنامج

هذا ما يبدو عليه البرنامج حين يرتكب المستخدم خطأين قبل النجاح:

=== Robust Input Demo === Enter a number between 1 and 100: hello [Error] "hello" is not a whole number. You typed: "hello" Please try again. Enter a number between 1 and 100: 150 [Error] 150 is out of range. Enter a number between 1 and 100. You typed: "150" Please try again. Enter a number between 1 and 100: 42 Great! You entered: 42

لماذا يعمل هذا التصميم

  • مسؤولية واحدة: parseAndValidate تتولى كل التحقق؛ main تتولى الحلقة والتفاعل مع المستخدم. كل قطعة تؤدي مهمة واحدة.
  • الاستثناء المُدقَّق يُجبر على المعالجة: لأن InvalidInputException تمتد من Exception، يُجبر المُجمِّع كل مُستدعٍ إما على التقاطه أو إعلان throws. لا يمكنك إهمال الإدخال السيئ عن طريق الخطأ.
  • لا فشل صامت: الحلقة لا تخرج حتى يُنتَج إدخال صالح. لا توجد قيمة return -1 تحرس لتنسى التحقق منها.
  • رسائل خطأ مفيدة: الاستثناء يحمل رسالة مقروءة للإنسان والإدخال الخام، لذا يمكن لكتلة الالتقاط أن تعطي المستخدم ملاحظات محددة.

تطوير المشروع (أفكار)

الآن بعد أن استقر النمط، جرّب هذه الإضافات الصغيرة بنفسك:

  • أضف حدًا أقصى لعدد المحاولات (مثلًا ثلاث محاولات)، ثم ارمِ استثناءً مختلفًا إذا تجاوز الحد.
  • تحقق من أن الإدخال ليس فارغًا قبل محاولة تحليله، وأعطِ رسالة محددة للإدخال الفارغ.
  • لفّ Scanner في كتلة try-with-resources (الدرس الثامن) لضمان إغلاقه دائمًا — حتى لو أفلت استثناء غير متوقع من الحلقة.
  • انقل المُتحقِّق إلى فئة مساعدة واكتب اختبار وحدة يفحص كل فرع خطأ.
لا تستخدم الاستثناءات أبدًا لتدفق التحكم العادي. حلقة إعادة المحاولة هنا هي النمط الصحيح لأن الإدخال السيئ استثنائي حقًا — ليس الحالة المتوقعة. لو وجدت نفسك ترمي استثناءات للإشارة إلى أن قائمة فارغة، أو للخروج من حلقات متداخلة، فذلك إساءة استخدام للآلية وسيربك كل قارئ لكودك.

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

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