DSL على Kotlin: كيفية إنشاء لغة البرمجة الخاصة بك في أمسية

مدرب بايثون أونلاين للمبتدئين

تعلم Python بسهولة دون إرهاق نظري. حل مهام عملية مع التحقق التلقائي واكتب الكود مباشرة في المتصفح.

ابدأ الدورة

ما هو DSL ولماذا تحتاجه؟

\n\n

تخيل أنك لا تكتب كودًا، بل تعليمات بلغة مفهومة تتحول بنفسها إلى برنامج يعمل. هذه هي بالضبط طريقة عمل اللغات المخصصة للمجال (DSL — Domain Specific Language). على عكس اللغات العامة (Java, Python, C#)، يتم إنشاء DSL لحل مهمة واحدة محددة — سواء كانت تكوين خادم، أو وصف قواعد الأعمال، أو بناء استعلامات SQL معقدة.

\n\n

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

\n\n

لماذا Kotlin هو ملك DSL؟

\n\n

كسب Kotlin حب المطورين ليس فقط بسبب توافقه مع Java. قدراته النحوية تسمح بإنشاء DSL داخلي — لغات تعيش داخل اللغة المضيفة وتستخدم بناء جملتها. فيما يلي الركائز الثلاث التي يقوم عليها أي DSL في Kotlin:

\n\n
    \n
  • اللامبدا مع المستقبل — تسمح بالوصول إلى طرق الكائن دون ذكر اسمه صراحة. هذا هو أساس نمط البناء.
  • \n
  • الامتدادات — تضيف وظائف جديدة لأي فئة، حتى لو لم تتمكن من تعديلها.
  • \n
  • الدوال الوسيطة — تسمح بكتابة كود بأسلوب اللغة الطبيعية: should be equalTo 5.
  • \n
\n\n

هذه الإمكانيات تحول كتابة DSL إلى لعبة بناء Lego: تأخذ كتلًا جاهزة — لامبدا، دوال، خصائص — وتجمع منها لغة مفهومة وآمنة.

\n\n

إنشاء DSL بسيط للاختبارات: خطوة بخطوة

\n\n

لنكتب DSL لكتابة الاختبارات. الهدف هو أن تبدو الاختبارات كوصف قابل للقراءة، وليس كمجموعة من استدعاءات assert:

\n\n
test("يجب على الآلة الحاسبة جمع الأرقام") {\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 متاحة:

\n\n
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 متاحة ضمنيًا.

\n\n

الخطوة 2. تنفيذ المطابقات

\n\n

الآن لننشئ مطابقات بسيطة. beEqualTo و beNull هما دالتان تعيدان كائنات Matcher:

\n\n
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

المدونات

توصيات الكتب