ما هو DSL ولماذا تحتاجه؟
\n\nتخيل أنك لا تكتب كودًا، بل تعليمات بلغة مفهومة تتحول بنفسها إلى برنامج يعمل. هذه هي بالضبط طريقة عمل اللغات المخصصة للمجال (DSL — Domain Specific Language). على عكس اللغات العامة (Java, Python, C#)، يتم إنشاء DSL لحل مهمة واحدة محددة — سواء كانت تكوين خادم، أو وصف قواعد الأعمال، أو بناء استعلامات SQL معقدة.
\n\nKotlin هو المرشح المثالي لإنشاء DSL. بفضل اللامبدا مع المستقبل والامتدادات والمنشئين آمني النوع، يمكنك كتابة كود يبدو وكأنه بناء جملة أصلي. في هذه المقالة، لن نناقش النظرية فقط، بل سننشئ DSL حقيقيًا لإطار اختبار — وسترى كم هو سهل وممتع.
\n\nلماذا Kotlin هو ملك DSL؟
\n\nكسب Kotlin حب المطورين ليس فقط بسبب توافقه مع Java. قدراته النحوية تسمح بإنشاء DSL داخلي — لغات تعيش داخل اللغة المضيفة وتستخدم بناء جملتها. فيما يلي الركائز الثلاث التي يقوم عليها أي DSL في Kotlin:
\n\n- \n
- اللامبدا مع المستقبل — تسمح بالوصول إلى طرق الكائن دون ذكر اسمه صراحة. هذا هو أساس نمط البناء. \n
- الامتدادات — تضيف وظائف جديدة لأي فئة، حتى لو لم تتمكن من تعديلها. \n
- الدوال الوسيطة — تسمح بكتابة كود بأسلوب اللغة الطبيعية:
should be equalTo 5. \n
هذه الإمكانيات تحول كتابة DSL إلى لعبة بناء Lego: تأخذ كتلًا جاهزة — لامبدا، دوال، خصائص — وتجمع منها لغة مفهومة وآمنة.
\n\nإنشاء DSL بسيط للاختبارات: خطوة بخطوة
\n\nلنكتب DSL لكتابة الاختبارات. الهدف هو أن تبدو الاختبارات كوصف قابل للقراءة، وليس كمجموعة من استدعاءات assert:
\n\ntest("يجب على الآلة الحاسبة جمع الأرقام") {\n val result = Calculator.add(2, 3)\n result should beEqualTo 5\n result shouldNot beNull\n}\n\nأوافق، هذه تقريبًا لغة إنجليزية. الآن دعنا نفهم كيف يعمل هذا.
\n\nالخطوة 1. إنشاء سياق الاختبار
\n\nنحتاج إلى دالة test تأخذ اسمًا وكتلة كود. يتم تنفيذ الكتلة في سياق حيث تكون should و shouldNot متاحة:
class TestContext {\n fun Any.should(matcher: Matcher) {\n if (!matcher.matches(this)) {\n throw AssertionError("متوقع ${matcher.expected}، ولكن حصلت على $this")\n }\n }\n\n fun Any.shouldNot(matcher: Matcher) {\n if (matcher.matches(this)) {\n throw AssertionError("غير متوقع ${matcher.expected}، ولكن حصلت على $this")\n }\n }\n}\n\nfun test(name: String, block: TestContext.() -> Unit) {\n println("تشغيل الاختبار: $name")\n try {\n TestContext().block()\n println("✓ نجح")\n } catch (e: AssertionError) {\n println("✗ فشل: ${e.message}")\n }\n}\n\nالنقطة الأساسية: TestContext.() -> Unit هي لامبدا مع مستقبل. داخل الكتلة، يشير this إلى كائن TestContext، وبالتالي تصبح الطرق should و shouldNot متاحة ضمنيًا.
الخطوة 2. تنفيذ المطابقات
\n\nالآن لننشئ مطابقات بسيطة. beEqualTo و beNull هما دالتان تعيدان كائنات Matcher:
interface Matcher {\n val expected: String\n fun matches(value: Any?): Boolean\n}\n\nfun beEqualTo(expected: Any) = object : Matcher {\n override val expected = "مساوٍ لـ $expected"\n override fun matches(value: Any?) = value == expected\n}\n\nval beNull = object : Matcher {\n override val expected = "فارغ"\n override fun matches(value: Any?) = value == null\n}\n\n