مقدمة: لماذا لا يكفي try/catch القياسي؟
\n\nالتطوير باستخدام Kotlin، خاصة في سياق تطبيقات الخادم أو Android، غالباً ما يواجه الحاجة إلى معالجة موثوقة للأخطاء. كتلة try/catch القياسية هي الأساس، لكنها عملياً تؤدي إلى "تشتت" المنطق وتعقيد الاختبار. على المستوى المتقدم، لا نريد فقط التقاط الاستثناءات، بل تصميم النظام بحيث تكون الأخطاء جزءاً من نوع البيانات، وليس تدفق تحكم غير متوقع.
في هذه المقالة، سنستعرض كيفية تجاوز try/catch واستخدام الأساليب الوظيفية: Result و Either (من المكتبات) و sealed class. ستتعلم كيفية جعل الكود قابلاً للتنبؤ، آمناً من حيث الأنواع، وسهل الصيانة.
1. try/catch التقليدي: متى لا يزال مفيداً؟
\n\nلا ينبغي التخلي تماماً عن try/catch. فهو لا غنى عنه للعمل مع الكود القديم (legacy)، والمكتبات الخارجية (مثل تحليل JSON)، واستدعاءات النظام (الإدخال/الإخراج). ومع ذلك، هناك فروق دقيقة مهمة.
الهيكل الصحيح والموارد
\nفي Kotlin، على عكس Java، لا توجد استثناءات مُتحقق منها (checked exceptions)، ولكن هناك بنية أنيقة use للإغلاق التلقائي للموارد:
fun readFile(path: String): String? = try {\n BufferedReader(FileReader(path)).use { it.readText() }\n} catch (e: IOException) {\n println("خطأ في قراءة الملف: ${e.message}")\n null\n}\n\nهنا، use يضمن إغلاق الملف، حتى في حالة حدوث استثناء داخلي. هذا بديل أنيق لـ try-with-resources.
المشكلة: فقدان السياق
\nالعيب الرئيسي لـ try/catch هو أنه لا يُعلم الكود المستدعي أن الدالة قد "تفشل". الاستثناء هو تأثير جانبي مخفي. استدعاء readFile() قد يُرجع null أو يلقي IOException، لكن توقيع الدالة لا يعكس ذلك. هذا يؤدي إلى أخطاء في وقت التشغيل.
الحل هو الانتقال إلى تمثيل صريح للأخطاء.
\n\n2. Result: أحادية (Monad) مدمجة للنجاح أو الفشل
\n\nتوفر Kotlin فئة قياسية Result<T>. وهي بنية جبرية بسيطة: إما نجاح (قيمة T) أو فشل (Throwable). هذا يجعل معالجة الأخطاء جزءاً من نظام الأنواع.
الاستخدام الأساسي
\nfun riskyDivision(a: Int, b: Int): Result<Int> = runCatching {\n a / b\n}\n\nfun main() {\n val result = riskyDivision(10, 0)\n result.onSuccess { println("النتيجة: $it") }\n .onFailure { println("خطأ: ${it.message}") }\n}\n\nrunCatching هو غلاف لـ try/catch يُرجع Result. تسمح الطريقتان onSuccess و onFailure بالتفاعل مع النتيجة دون مقاطعة سلسلة الاستدعاءات.
التحويل والسلاسل
\nيمكنك تحويل النتيجة دون القلق بشأن الاستثناءات داخل map:
\n\nval finalResult = riskyDivision(10, 2)\n .map { it * 2 } // إذا كان نجاحاً - نضرب\n .recover { error -> // إذا كان خطأ - نعيد قيمة احتياطية\n println("نستخدم قيمة احتياطية بسبب: ${error.message}")\n 0\n }\nprintln(finalResult.getOrDefault(-1)) // 10\n\nهام: Result غير مخصص للتخزين في المجموعات أو النقل بين الخيوط دون معالجة إضافية. لهذه الأغراض، من الأفضل استخدام sealed class أو مكتبات مثل Arrow.
3. Either: التمييز بين أنواع الأخطاء
\n\nفئة Result تلتقط فقط Throwable. لكننا غالباً ما نريد التمييز بين أنواع الأخطاء: "غير موجود"، "صلاحيات غير كافية"، "خطأ في التحقق". هنا تأتي Either<L, R> من المكتبات الوظيفية (مثل Arrow أو المكتوبة يدوياً