يا مساء الخير يا جماعة، معكم أخوكم أبو عمر.
خليني أحكيلكم قصة صارت معي ومع فريقي قبل فترة، قصة علّمتنا درس مهم عن النظام والترتيب في عالم البرمجة. كنا شغالين على نظام أتمتة ضخم، نظام فيه عشرات المهام اللي بتعتمد على بعضها: مهمة بتجهّز البيانات، ومهمة بتدرب النموذج، ومهمة بتعمل اختبار، ومهمة بتنشر النتائج… وهكذا. كل شي كان ماشي زي الساعة، لحد ما في يوم، وبدون سابق إنذار، النظام “علّق”.
المهام صارت تاخد وقت أطول من اللازم، وبعدين تفشل بسبب انتهاء المهلة (Timeout). حاولنا نعيد تشغيلها، نفس المشكلة. قعدنا يومين كاملين، والفريق كله على أعصابه. واحد بقول المشكلة من الشبكة، والثاني بقول في تغيير جديد عمله فلان، والكل صار يرمي التهم على بعض. الأجواء كانت مشحونة، والقهوة ما عادت تعمل مفعولها.
في ليلة من الليالي، وأنا براجع سجلات التنفيذ (Logs) للمرة المليون، لاحظت نمط غريب: المهمة A تبدأ، ثم تنتظر المهمة B. المهمة B تبدأ، وتنتظر المهمة C. والمهمة C… كانت تنتظر المهمة A! وقتها صرخت (بيني وبين حالي طبعاً): “لقيتها! هاي اعتمادية دائرية (Circular Dependency)!”. مهامنا كانت بتلف وبتدور في حلقة مفرغة، كل مهمة بتستنى اللي قبلها تخلص، وما حدا راح يخلص. هون بالزبط تذكرت صديق قديم اسمه “الفرز الطوبولوجي”، وعرفت إنه هو المنقذ الوحيد من هالجحيم.
ما هو الجحيم الذي كنا فيه؟ (فهم الاعتماديات)
قبل ما نحكي عن الحل، خلينا نفهم أصل المشكلة. في أي نظام معقد، المهام أو المكونات بتعتمد على بعضها. عشان تبني بيت، لازم تحفر الأساسات قبل ما ترفع الأعمدة. وعشان تطبخ منسف، لازم تجهّز اللبن والرز قبل ما تطبقهم مع بعض. هاي اسمها “اعتماديات” (Dependencies).
بنقدر نمثل هاي الاعتماديات على شكل “مخطط موجّه” أو (Directed Graph). كل مهمة هي “عقدة” (Node)، وكل اعتمادية هي “سهم” (Edge) من مهمة لأخرى. فإذا كانت المهمة A تعتمد على المهمة B، بنرسم سهم من B إلى A (يعني B لازم تخلص قبل A).
مثال بسيط: تحضير الشاي
- غلي الماء (مهمة 1)
- وضع الشاي في الكوب (مهمة 2)
- صب الماء المغلي فوق الشاي (مهمة 3)
الاعتماديات: المهمة 3 تعتمد على 1 و 2. يعني لازم 1 و 2 يخلصوا قبل ما تبدأ 3.
المشكلة بتصير لما هاي الأسهم تشكّل “دائرة مغلقة”. يعني A تعتمد على B، و B تعتمد على C، و C ترجع تعتمد على A. هذا اسمه “اعتمادية دائرية” (Circular Dependency)، وهو وضع مستحيل منطقياً. كيف تبدأ A إذا كانت تنتظر C، وC تنتظر B، وB تنتظر A؟ هذا بالزبط اللي صار معنا.
المنقذ: الفرز الطوبولوجي (Topological Sort)
الفرز الطوبولوجي هو ببساطة عملية إيجاد ترتيب خطي لجميع العقد (المهام) في مخطط موجّه، بحيث لو كان في سهم من العقدة U إلى العقدة V، فإن U تأتي دائماً قبل V في الترتيب.
شو يعني هالحكي؟ يعني هو خوارزمية بتعطينا الترتيب الصحيح والمنطقي لتنفيذ المهام. لو طبقنا الفرز الطوبولوجي على مثال الشاي، ممكن يعطينا الترتيب: [غلي الماء، وضع الشاي، صب الماء] أو [وضع الشاي، غلي الماء، صب الماء]. المهم إن المهمة الأخيرة (صب الماء) تيجي بعد المهمتين الأولانيات.
والأهم من هذا كله: الفرز الطوبولوجي لا يمكن تطبيقه إلا على مخطط موجّه لا يحتوي على دوائر (Directed Acyclic Graph – DAG). إذا حاولت تطبقه على مخطط فيه دائرة، الخوارزمية راح تكتشف هاي الدائرة وتخبرك عنها. وهذا هو السلاح السري اللي كنا محتاجينه!
كيف يعمل الفرز الطوبولوجي؟ (خوارزمية كان – Kahn’s Algorithm)
واحدة من أشهر الطرق لعمل الفرز الطوبولوجي هي خوارزمية “كان”. فكرتها بسيطة ومنطقية جداً، وبتشبه الطريقة اللي بنفكر فيها طبيعياً:
- ابحث عن المهام التي لا تعتمد على أي شيء: في مخططنا، هاي هي العقد اللي ما بوصلها أي سهم. بنسمي عدد الأسهم اللي بتدخل على العقدة “الدرجة الداخلية” (In-degree). فإحنا بندور على العقد اللي درجتها الداخلية تساوي صفر.
- نفّذ هذه المهام: بما إنها ما بتعتمد على إشي، بنقدر نبدأ فيها. بنحطها في قائمة انتظار (Queue).
- حدّث الاعتماديات: كل ما نخلص مهمة من قائمة الانتظار، بنعتبر إنها “انتهت”. بنروح على كل المهام اللي كانت تعتمد عليها (جيرانها) وبنقلل درجة اعتماديتها (in-degree) بمقدار واحد. كأننا بنقول: “يا مهمة X، المهمة Y اللي كنتي تستنيها خلصت، هسا ناقصك اعتمادية واحدة أقل”.
- أضف المهام الجديدة الجاهزة: إذا أي مهمة من الجيران صارت درجة اعتماديتها صفر، معناها صارت جاهزة للتنفيذ. بنضيفها على طول على قائمة الانتظار.
- كرر العملية: بنضل نكرر الخطوات 3 و 4 حتى تفضى قائمة الانتظار.
الترتيب اللي بنطلع فيه المهام من قائمة الانتظار هو نفسه الترتيب الطوبولوجي!
وماذا عن الدوائر؟
الجميل في خوارزمية كان إنها بتكشف الدوائر تلقائياً. كيف؟ بعد ما تخلص الخوارزمية، بنقارن عدد المهام اللي ترتبت معنا بعدد المهام الكلي. إذا كان العدد أقل، فهذا يعني أن هناك مهام لم تدخل الترتيب أبداً. ليش؟ لأنها ظلت عالقة في دائرة، ودرجتها الداخلية (in-degree) لم تصل إلى الصفر أبداً. وبهيك بنكون كشفنا المشكلة وعرفنا مكانها.
مثال برمجي بلغة Python
الحكي النظري حلو، بس الكود أحلى. هاي نسخة مبسطة من خوارزمية كان باستخدام Python:
from collections import deque
def topological_sort(graph):
# graph هو قاموس يمثل المخطط
# المفتاح هو العقدة، والقيمة هي قائمة بالعقد التي تعتمد عليها
# مثال: {'C': ['A', 'B']} تعني C تعتمد على A و B
# 1. حساب الدرجة الداخلية (in-degree) لكل عقدة
in_degree = {node: 0 for node in graph}
# بناء المخطط العكسي لتسهيل الوصول للجيران
adj = {node: [] for node in graph}
for node, dependencies in graph.items():
for dep in dependencies:
adj[dep].append(node)
in_degree[node] += 1
# 2. إنشاء قائمة انتظار ووضع العقد التي درجتها صفر
queue = deque([node for node in graph if in_degree[node] == 0])
sorted_order = []
# 3. بدء المعالجة
while queue:
node = queue.popleft()
sorted_order.append(node)
# 4. تحديث الجيران
if node in adj:
for neighbor in adj[node]:
in_degree[neighbor] -= 1
# 5. إضافة الجيران الجاهزين
if in_degree[neighbor] == 0:
queue.append(neighbor)
# 6. التحقق من وجود دائرة
if len(sorted_order) == len(graph):
return sorted_order # نجح الفرز
else:
# يمكن هنا إضافة منطق لإيجاد العقد التي في الدائرة
return "Error: Graph has a cycle!"
# --- مثال الاستخدام ---
# مهامنا ومين بيعتمد على مين
tasks = {
'build_app': ['install_deps'],
'install_deps': [],
'run_tests': ['build_app'],
'deploy': ['run_tests', 'push_image'],
'push_image': ['build_app']
}
# لو أضفنا اعتمادية دائرية
# tasks_with_cycle = tasks.copy()
# tasks_with_cycle['install_deps'].append('deploy') # install_deps تعتمد على deploy -> دائرة!
sorted_tasks = topological_sort(tasks)
print("ترتيب التنفيذ الصحيح:", sorted_tasks)
# الناتج المتوقع: ['install_deps', 'build_app', 'push_image', 'run_tests', 'deploy'] (أو ترتيب آخر صحيح)
# sorted_tasks_cycle = topological_sort(tasks_with_cycle)
# print(sorted_tasks_cycle) # الناتج: Error: Graph has a cycle!
نصائح عملية من أخوكم أبو عمر
بعد ما حلينا المشكلة بفضل الله ثم الفرز الطوبولوجي، تعلمت شوية دروس بحب أشاركها معكم:
- فكّر بالرسوم البيانية (Graphs): كثير من المشاكل في البرمجة، خصوصاً اللي فيها علاقات وترتيب، يمكن تبسيطها لو فكرت فيها على شكل عقد وأسهم. ارسمها على ورقة أو على سبورة، بتلاقي الحل صار أوضح.
- التحقق من الدوائر هو الأهم: لا تكتفِ بتطبيق الفرز. الأهم هو بناء آلية قوية لاكتشاف الدوائر والإبلاغ عنها بشكل واضح. بدل ما تطبع رسالة خطأ عامة، حاول تطبع مسار الدائرة نفسه (مثلاً: A -> B -> C -> A). هذا بيوفر ساعات من التنقيب والبحث على فريقك.
- اجعل الاعتماديات واضحة: في مشاريعك، حاول يكون ملف الإعدادات أو الكود اللي بيعرف الاعتماديات واضح ومقروء. كل ما كان النظام غامض، زادت فرصة حدوث أخطاء مثل الاعتماديات الدائرية.
- هناك طرق أخرى: خوارزمية كان ممتازة، ولكن هناك طريقة أخرى مشهورة تعتمد على البحث بالعمق أولاً (Depth-First Search – DFS). هذه الطريقة قوية جداً أيضاً في اكتشاف الدوائر. تعلم الطريقتين واختر الأنسب لمشكلتك.
الخلاصة: النظام أساس كل شيء
مشكلة الاعتماديات الدائرية كانت درس قاسي ولكنه مفيد. علمتنا إن الفوضى في تعريف العلاقات بين مكونات النظام تؤدي حتماً إلى الانهيار. الفرز الطوبولوجي لم يكن مجرد خوارزمية طبقناها، بل كان فلسفة ومنهجية أجبرتنا على التفكير بترتيب ومنطق سليم.
في المرة القادمة اللي بتلاقي فيها نفسك أو فريقك في حلقة مفرغة، ومهمة بتستنى مهمة إلى ما لا نهاية، تذكر قصة أبو عمر. خذ نفس عميق، ارسم مخطط الاعتماديات، ودع الفرز الطوبولوجي يكون دليلك للخروج من الجحيم. 😉
يلا، شدّوا حيلكم ورجوني إبداعاتكم! 💪