الفرز الطوبولوجي: كيف أنقذنا مهامنا من جحيم الاعتماديات الدائرية؟

يا جماعة الخير، السلام عليكم. اسمي أبو عمر، وأنا اليوم جاي أحكي لكم قصة صارت معي ومع فريقي قبل فترة، قصة علّمتنا درس ما رح ننساه أبداً عن أهمية فهم الخوارزميات الأساسية.

كنا شغالين على نظام كبير لأتمتة المهام (Task Automation System). النظام كان المفروض يكون ذكي: تعطيه مجموعة مهام، وكل مهمة بتعتمد على مهام أخرى، وهو لحاله يرتب التنفيذ. مثلاً، مهمة “نشر التطبيق” ما بتصير إلا بعد ما تخلص مهمة “الاختبار” و”بناء الحزمة البرمجية”. الأمور كانت ماشية تمام في البداية، والنظام شغال زي الحلاوة.

لحد ما في يوم… كل شي توقف. النظام علّق. المهام دخلت في قائمة الانتظار وما حدا فيها رضي يتنفذ. طلعنا على سجلات النظام (logs)، وما شفنا أي خطأ واضح، بس مجرد صمت رهيب. كل مهمة كانت “منتظرة”. منتظرة إيش؟ الله أعلم. بعد ساعات من الحفر والبحث، واحد من الشباب صاح: “يا جماعة، لقيتها!”. المشكلة كانت بسيطة في شكلها، كارثية في تأثيرها: مهمة (أ) كانت تنتظر مهمة (ب)، ومهمة (ب) كانت تنتظر مهمة (ج)، ومهمة (ج)… كانت تنتظر مهمة (أ)! دخلنا في حلقة مفرغة، أو ما أسميه أنا “جحيم الاعتماديات الدائرية”.

هون، تذكرت محاضرة من أيام الجامعة عن الرسوم البيانية وخوارزمية قديمة اسمها “الفرز الطوبولوجي”. كانت هي طوق النجاة اللي أنقذنا من هذا المأزق.

ما هو “جحيم الاعتماديات الدائرية”؟

قبل ما نحكي عن الحل، خلينا نفهم المشكلة منيح. الاعتمادية الدائرية (Circular Dependency) بتصير لما يكون عندك سلسلة من المهام أو الوحدات البرمجية، كل واحدة بتعتمد على اللي بعدها، لحد ما الأخيرة في السلسلة ترجع تعتمد على الأولى.

تخيلها هيك:

  • عشان أعمل المهمة (أ)، لازم أستنى المهمة (ب) تخلص.
  • عشان أعمل المهمة (ب)، لازم أستنى المهمة (ج) تخلص.
  • عشان أعمل المهمة (ج)، لازم أستنى المهمة (أ) تخلص.

النتيجة؟ ولا حدا رح يشتغل. الكل رح يضل يستنى بعض للأبد. زي اللي بستنوا بعض على باب الدار، وما حدا راضي يفوت قبل الثاني. هذا الوضع مش مجرد مشكلة نظرية، بل هو كابوس حقيقي في عالم البرمجة ويظهر في أماكن كثيرة:

  • أنظمة البناء (Build Systems): مثل Webpack أو Maven، لو وحدة (module) بتستورد (import) وحدة ثانية، والثانية بترجع تستورد الأولى، رح تحصل على خطأ.
  • جداول البيانات (Spreadsheets): لو الخلية A1 تعتمد على قيمة B1، و B1 تعتمد على A1، رح تشوف خطأ #REF!.
  • مخططات قواعد البيانات (Database Schemas): محاولة إنشاء علاقات مفاتيح خارجية (Foreign Keys) بشكل دائري.

الفرز الطوبولوجي: طوق النجاة

هنا يأتي دور البطل في قصتنا: الفرز الطوبولوجي (Topological Sort). الاسم ممكن يكون بخوّف شوي، بس الفكرة بسيطة جداً والله.

ما هو الفرز الطوبولوجي ببساطة؟

الفرز الطوبولوجي هو خوارزمية لترتيب عناصر (عُقد) في رسم بياني موجّه (Directed Graph) بطريقة خطّية، بحيث لو في مسار من العقدة (أ) إلى العقدة (ب)، فالعقدة (أ) لازم تظهر قبل العقدة (ب) في الترتيب النهائي.

شرط أساسي: الفرز الطوبولوجي لا يعمل إلا على نوع معين من الرسوم البيانية اسمه “الرسم البياني الموجه غير الدائري” (Directed Acyclic Graph – DAG). كلمة “غير الدائري” (Acyclic) هي مفتاح كل شي.

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

فكر فيها كأنك بتعمل طبخة. ما بتقدر تضيف البهارات بعد ما تكون خلصت طبخ وطفيت النار. لازم تقطّع الخضار قبل ما تحطها في المقلى. الفرز الطوبولوجي بيعطيك الترتيب الصحيح لخطوات الطبخة.

كيف يعمل الفرز الطوبولوجي؟ (خوارزمية كان – Kahn’s Algorithm)

هناك طريقتان مشهورتان لتطبيق الفرز الطوبولوجي، لكن الطريقة الأسهل للفهم والتطبيق في رأيي هي خوارزمية “كان” (Kahn’s Algorithm)، وهي التي استخدمناها لحل مشكلتنا. تعتمد على فكرة بسيطة اسمها “درجة الدخول” (In-degree).

الخطوات خطوة بخطوة

  1. حساب درجة الدخول (In-degree): لكل مهمة (عقدة) في نظامنا، نحسب عدد المهام الأخرى التي تعتمد عليها هذه المهمة (يعني عدد الأسهم اللي داخلة عليها). على سبيل المثال، إذا كانت المهمة “ج” تعتمد على “أ” و “ب”، فإن درجة الدخول لـ “ج” هي 2.
  2. تحديد نقطة البداية: نبحث عن كل المهام اللي درجة الدخول تبعتها تساوي صفر. هذه هي المهام التي لا تعتمد على أي شيء آخر، وهي نقاط البداية الطبيعية. نضع كل هذه المهام في طابور (Queue).
  3. المعالجة والترتيب:
    • طالما الطابور ليس فارغاً:
      1. نسحب مهمة من الطابور (لنسميها “المهمة الحالية”).
      2. نضيف “المهمة الحالية” إلى قائمتنا النهائية المرتبة.
      3. لكل مهمة “مجاورة” (مهمة تعتمد على “المهمة الحالية”):
        • نطرح 1 من درجة الدخول الخاصة بها (لأننا “أكملنا” أحد متطلباتها).
        • إذا أصبحت درجة الدخول للمهمة المجاورة تساوي صفر، فهذا يعني أنها جاهزة للتنفيذ. نضيفها إلى الطابور.

ماذا لو بقيت هناك عُقد؟

هنا تكمن عبقرية الخوارزمية في كشف الحلقات. بعد انتهاء الخوارزمية (عندما يصبح الطابور فارغاً)، نقارن عدد المهام في قائمتنا النهائية المرتبة مع العدد الإجمالي للمهام في النظام.

  • إذا كان العددان متساويين: ممتاز! لا توجد حلقات، والقائمة النهائية تحتوي على الترتيب الصحيح للتنفيذ.
  • إذا كان عدد المهام في القائمة النهائية أقل من العدد الإجمالي: هذا يعني أن هناك حلقة دائرية! المهام المتبقية التي لم تتم إضافتها إلى القائمة هي التي تشكل هذه الحلقة اللعينة.

تطبيق عملي: لنكتب الكود!

الكلام النظري جميل، لكن “فرجيني الكود يا أبو عمر!”. حاضر. إليك مثال بسيط بلغة Python يوضح كيفية تطبيق خوارزمية “كان” لاكتشاف الحلقات وترتيب المهام.


from collections import deque

def topological_sort(tasks_with_deps):
    """
    يقوم بترتيب المهام باستخدام الفرز الطوبولوجي (خوارزمية كان)
    ويكتشف الاعتماديات الدائرية.

    :param tasks_with_deps: قاموس يمثل المهام واعتمادياتها.
                           الشكل: {'task_name': ['dep1', 'dep2']}
    :return: قائمة بالمهام مرتبة ترتيباً طوبولوجياً.
    :raises ValueError: في حال وجود حلقة دائرية.
    """
    
    # 1. بناء الرسم البياني وحساب درجة الدخول (in-degree)
    graph = {task: [] for task in tasks_with_deps}
    in_degree = {task: 0 for task in tasks_with_deps}
    all_tasks = set(tasks_with_deps.keys())

    for task, deps in tasks_with_deps.items():
        for dep in deps:
            # التأكد من أن الاعتمادية موجودة كـ مهمة
            if dep not in all_tasks:
                 raise ValueError(f"الاعتمادية '{dep}' للمهمة '{task}' غير موجودة كـ مهمة مستقلة.")
            graph[dep].append(task)
            in_degree[task] += 1
            
    # 2. تحديد نقاط البداية (المهام التي ليس لها اعتماديات)
    # أي المهام التي درجة دخولها صفر
    queue = deque([task for task, degree in in_degree.items() if degree == 0])
    
    sorted_tasks = []
    
    # 3. المعالجة والترتيب
    while queue:
        current_task = queue.popleft()
        sorted_tasks.append(current_task)
        
        # لكل مهمة تعتمد على المهمة الحالية
        for neighbor in graph[current_task]:
            in_degree[neighbor] -= 1
            # إذا أصبحت المهمة المجاورة جاهزة، أضفها للطابور
            if in_degree[neighbor] == 0:
                queue.append(neighbor)
                
    # 4. التحقق من وجود حلقات دائرية
    if len(sorted_tasks) == len(all_tasks):
        return sorted_tasks
    else:
        # تحديد العقد التي تسببت في الحلقة
        cyclic_nodes = {task for task, degree in in_degree.items() if degree > 0}
        raise ValueError(f"تم اكتشاف حلقة دائرية! المهام المتورطة: {cyclic_nodes}")

# --- مثال 1: نظام سليم بدون حلقات ---
sane_system = {
    'تحضير البيانات': [],
    'تدريب النموذج': ['تحضير البيانات'],
    'تقييم النموذج': ['تدريب النموذج'],
    'نشر النموذج': ['تقييم النموذج', 'تدريب النموذج']
}

try:
    ordered_tasks = topological_sort(sane_system)
    print("الترتيب الصحيح للمهام:")
    print(ordered_tasks)
    # المخرجات المتوقعة (قد يختلف الترتيب بين المهام المتوازية):
    # ['تحضير البيانات', 'تدريب النموذج', 'تقييم النموذج', 'نشر النموذج']
except ValueError as e:
    print(f"خطأ: {e}")


# --- مثال 2: نظام فيه حلقة دائرية (جحيم الاعتماديات) ---
cyclic_system = {
    'المهمة أ': ['المهمة ج'], # أ تعتمد على ج
    'المهمة ب': ['المهمة أ'], # ب تعتمد على أ
    'المهمة ج': ['المهمة ب']  # ج تعتمد على ب (وهنا تكتمل الحلقة)
}

try:
    ordered_tasks = topological_sort(cyclic_system)
    print("الترتيب الصحيح للمهام:")
    print(ordered_tasks)
except ValueError as e:
    print(f"nخطأ في النظام الدائري:")
    print(e)
    # المخرجات المتوقعة:
    # خطأ في النظام الدائري:
    # تم اكتشاف حلقة دائرية! المهام المتورطة: {'المهمة ج', 'المهمة أ', 'المهمة ب'}

نصائح من أبو عمر

بعد ما مرينا بهاي التجربة، طلعنا بكم درس عملي بحب أشاركم إياها:

  • الوقاية خير من العلاج: أفضل طريقة للتعامل مع الاعتماديات الدائرية هي منعها من الحدوث. عند تصميم أي نظام فيه اعتماديات، فكر دائماً: “هل يمكن للمستخدم أن ينشئ حلقة بالخطأ؟”. إذا كان الجواب نعم، يجب أن يكون لديك منطق برمجي للكشف عنها، مثل الفرز الطوبولوجي.
  • اجعل الخطأ مفيداً: لما تكتشف حلقة دائرية، لا تكتفي بإظهار رسالة خطأ غامضة مثل “Circular Dependency Detected”. حاول أن تكون محدداً. رسالة مثل “تم اكتشاف حلقة بين: المهمة أ -> المهمة ب -> المهمة ج -> المهمة أ” هي أفضل ألف مرة للمستخدم النهائي أو للمبرمج الذي يحاول إصلاح المشكلة.
  • ليست فقط للمهام: هذه الخوارزمية لها تطبيقات أوسع بكثير مما تتخيل. من تحميل الوحدات البرمجية (modules) في لغات البرمجة، إلى تنفيذ خطوات بناء البرمجيات، وحتى تحديد ترتيب تحديث خلايا جداول البيانات. فهمك لها يفتح عينيك على حلول أنيقة لمشاكل تبدو معقدة.
  • البديل الآخر (DFS): للمعلوماتية، هناك طريقة أخرى لتطبيق الفرز الطوبولوجي باستخدام خوارزمية البحث بالعمق أولاً (Depth-First Search). هي قوية أيضاً، ولكن شخصياً أجد أن خوارزمية “كان” أوضح وأسهل للفهم عند التعامل مع اكتشاف الحلقات بشكل مباشر.

الخلاصة: لا تخف من الرسوم البيانية! ✨

يا جماعة، الخوارزميات وهياكل البيانات مش مجرد نظريات معقدة في الكتب الأكاديمية. هي أدوات حقيقية في صندوق عُدّتنا كمبرمجين ومطورين، قادرة على حل مشاكل حقيقية ومؤلمة مثل “جحيم الاعتماديات الدائرية”.

في قصتنا، الفرز الطوبولوجي لم يكن مجرد خوارزمية، بل كان المنقذ الذي أعاد نظامنا إلى الحياة. علّمنا أن نفكر في الترتيب، والاعتماديات، والأهم من ذلك، كيف نكتشف الفوضى قبل أن تحدث.

نصيحتي الأخيرة لكم: لا تهملوا الأساسيات. اقضوا بعض الوقت في مراجعة الخوارزميات الكلاسيكية مثل هذه. قد لا تستخدمها كل يوم، ولكن في اليوم الذي ستحتاجها فيه، ستكون ممتناً جداً لأنك تعرفها. بالتوفيق في مشاريعكم!

أبو عمر

سجل دخولك لعمل نقاش تفاعلي

كافة المحادثات خاصة ولا يتم عرضها على الموقع نهائياً

آراء من النقاشات

لا توجد آراء منشورة بعد. كن أول من يشارك رأيه!

آخر المدونات

نصائح برمجية

كانت بياناتنا مجرد أرقام ونصوص: كيف أنقذتنا الكائنات القيمية (Value Objects) من جحيم الأخطاء الصامتة؟

أشارككم قصة من قلب المعركة البرمجية، كيف كاد خطأ بسيط بسبب البيانات العشوائية أن يكلفنا الكثير. سنتعلم معًا كيف تنقذنا "الكائنات القيمية" (Value Objects) من...

16 مايو، 2026 قراءة المزيد
تجربة المستخدم والابداع البصري

كان كل زر في تطبيقنا قصة مختلفة: كيف أنقذنا ‘نظام التصميم’ (Design System) من جحيم الفوضى البصرية؟

أروي لكم قصة من قلب المعركة البرمجية، كيف انتقلنا من تطبيق مليء بالفوضى البصرية والتضارب إلى واجهة متناغمة وموحدة. هذا ليس مجرد مقال تقني، بل...

16 مايو، 2026 قراءة المزيد
برمجة وقواعد بيانات

كانت عملياتنا تفشل في منتصف الطريق: كيف أنقذتنا ‘معاملات قاعدة البيانات’ من جحيم البيانات غير المتسقة؟

أشارككم قصة حقيقية عن كارثة بيانات كادت أن تدمر مشروعنا، وكيف كانت 'معاملات قاعدة البيانات' (Transactions) ومبادئ ACID هي طوق النجاة. تعلم كيف تحمي تطبيقاتك...

16 مايو، 2026 قراءة المزيد
الحوسبة السحابية

كانت فاتورة السحابة تلتهم ميزانيتنا: كيف أنقذتنا استراتيجيات FinOps من جحيم الإنفاق الضائع؟

أتذكر ذلك اليوم جيدًا، فاتورة سحابية بقيمة عشرات الآلاف كادت أن توقف مشروعنا. في هذه المقالة، أشارككم قصة حقيقية وكيف كانت مبادئ FinOps طوق النجاة...

16 مايو، 2026 قراءة المزيد
التوظيف وبناء الهوية التقنية

كانت هويتي التقنية شبحاً: كيف أنقذتني الكتابة عن رحلة التعلم من جحيم السيرة الذاتية الصامتة؟

لسنوات، كانت سيرتي الذاتية مجرد قائمة صامتة من المهارات التي لا يراها أحد. في هذه المقالة، أسرد لكم كيف تحولت من مطور "شبح" إلى خبير...

16 مايو، 2026 قراءة المزيد
البودكاست