مقالات معمّقة حول التكنولوجيا التي تشكّل المستقبل.

قواعد Rob Pike في البرمجة ما زالت صحيحة

كتب Rob Pike خمس قواعد للبرمجة عام 1989. وهي اليوم أكثر صلة مما كانت عليه، خاصة تلك التي نستمر في تجاهلها.

ورقة فهرسة قديمة مثبتة بجانب مكتب حديث مزدحم تحت مصباح واحد

في عام 1989، كتب Rob Pike، الذي شارك لاحقًا في إنشاء لغة Go وترميز UTF-8 ونظام Plan 9، خمس قواعد للبرمجة. هي قصيرة بما يكفي لتُكتب على بطاقة فهرسة، وعميقة لدرجة أن عالم البرمجة ما زال يتجادل حولها منذ 37 عامًا. معظم المطورين رأوا بعضها مقتبسًا في مقالات المدونات أو محاضرات المؤتمرات، لكن قلّة منهم استوعبتها فعلًا. وهذا مؤسف، لأنها كانت ستوفر الكثير من الجهد الضائع.

القواعد تبدو بسيطة بشكل خادع. فهي لا تتعلق بالصياغة ولا بأنماط المعمارية، بل تتعلق بالمكان الذي يُهدر فيه المبرمجون وقتهم باستمرار، وكيف يتوقفون عن ذلك.

القواعد الخمس

دعني أذكرها بوضوح قبل أن نفككها:

  1. لا يمكنك معرفة المكان الذي سيستهلك فيه البرنامج وقته. عنق الزجاجة يظهر في أماكن مفاجئة، لذا لا تحاول التخمين وتضيف حيلة لتحسين السرعة قبل أن تثبت أن هذا هو موضع الاختناق.
  2. قِس أولًا. لا تحسّن السرعة قبل أن تقيس، وحتى بعد القياس، لا تفعل ذلك إلا إذا كان جزء واحد من الكود يطغى على بقية الأجزاء.
  3. الخوارزميات المعقدة بطيئة عندما تكون n صغيرة، وn غالبًا ما تكون صغيرة. الخوارزميات المعقدة لها ثوابت كبيرة. ما لم تعرف أن n ستكون كبيرة بشكل متكرر، لا تتعقّد.
  4. الخوارزميات المعقدة أكثر عرضة للأخطاء من البسيطة، وتطبيقها أصعب بكثير. استخدم الخوارزميات البسيطة وهياكل البيانات البسيطة.
  5. البيانات هي المهيمنة. إذا اخترت هياكل البيانات الصحيحة ونظّمت الأمور جيدًا، ستكون الخوارزميات واضحة في الغالب بذاتها. هياكل البيانات، لا الخوارزميات، هي جوهر البرمجة.

القاعدتان 1 و2 تتعلقان بالتحسين، والقاعدتان 3 و4 تتعلقان بالتعقيد، والقاعدة 5 تتعلق بالتصميم. معًا تشكّل فلسفة تقوم في جوهرها على التواضع: الاعتراف بأن حدسنا حول الأداء خاطئ، وأن للتعقيد تكاليف نقلل من تقديرها، وأن هياكل البيانات الجيدة أهم من الكود الذكي.

القاعدة 1: لا تعرف أين يقع عنق الزجاجة

هذه هي القاعدة التي يخالفها المطورون بثقة أكبر من غيرها. «هذه الدالة بطيئة لأن فيها حلقة متداخلة». «يجب أن أستخدم جدول تجزئة هنا لأن البحث فيه O(1)». «سأحجز مساحة مسبقًا لهذه المصفوفة لأن التخصيص مكلف». تبدو هذه الأفكار منطقية، وغالبًا ما تكون خاطئة.

لقد حللت أداء أنظمة إنتاج كافية لأجمع مجموعة من الأمثلة التي لم يكن فيها عنق الزجاجة الحدسي هو عنق الزجاجة الفعلي. نظام افترض فيه الجميع أن قاعدة البيانات هي المشكلة، لكن التحليل أظهر أن تسلسل JSON كان يستهلك 60% من زمن الطلبات. وخط معالجة بيانات كان فيه ضرب المصفوفات «المكلف» يستغرق 5% من زمن التشغيل، بينما استغرق تحليل CSV 70%. وتطبيق ويب قضى فريقه شهورًا في تحسين استعلامات قاعدة البيانات، بينما كان عنق الزجاجة الحقيقي هو تحليل DNS في كل طلب HTTP صادر.

الدماغ البشري أداة تحليل أداء سيئة. نبالغ في تقدير العمليات التي تبدو مكلفة مفهوميًا، مثل استعلامات قواعد البيانات واستدعاءات الشبكة، ونقلل من تقدير العمليات التي تبدو رخيصة، مثل دمج النصوص وتحليل JSON وتخصيص الذاكرة. وتزيد أجهزة اليوم الأمر سوءًا، فذاكرة CPU المؤقتة وتنبؤ الفروع والتنفيذ خارج الترتيب تجعل العلاقة بين تعقيد الكود وزمن التنفيذ غير بديهية إلى حد كبير.

القاعدة 2: قِس أولًا ثم حسّن

هذه هي النتيجة العملية للقاعدة 1. لا تحسّن بناءً على الحدس. حلّل الأداء، واعثر على النقطة الساخنة الفعلية، ثم حسّنها وحدها.

النصف الثاني من القاعدة، «لا تحسّن إلا إذا كان جزء من الكود يطغى على الباقي»، لا يقل أهمية، لكنه يُقتبس أقل. إذا أظهر المحلل أن زمن التشغيل موزع بالتساوي على 20 دالة، كل منها 5%، فلا يوجد عنق زجاجة واحد يستحق التحسين. جعل دالة واحدة أسرع بمرتين يوفر 2.5% من زمن التشغيل الإجمالي، وهذا نادرًا ما يستحق التعقيد الإضافي. ستحتاج إلى نهج مختلف جذريًا بدلًا من تحسين الدوال واحدة تلو الأخرى.

# Profile first. Always.
# Python
python -m cProfile -s cumulative my_script.py
# Node.js
node --prof app.js
node --prof-process isolate-*.log > profile.txt
# Go
go test -bench=. -cpuprofile=cpu.prof
go tool pprof cpu.prof
# Rust
cargo flamegraph
# General (Linux)
perf record ./my_program && perf report
# The flamegraph (brendangregg.com/flamegraphs) is the most
# informative visualization. It shows you exactly where time
# is spent, in a format that makes bottlenecks visually obvious.

القاعدة 3: الخوارزميات المعقدة بطيئة عندما تكون N صغيرة

هذه هي القاعدة التي يقلبها تعليم علوم الحاسوب رأسًا على عقب. نُعلّم أن O(n log n) أفضل من O(n²)، وهذا صحيح من الناحية التقاربية. لكن عند n = 20، فإن ترميز الإدراج (insertion sort) المنفذ جيدًا أسرع من فرز الدمج O(n log n) بسبب الثوابت وسلوك الذاكرة المؤقتة والتكاليف الإضافية.

أمثلة من الواقع كثيرة. البحث الخطي في مصفوفة مرتبة من 50 عنصرًا أسرع من البحث الثنائي، لأن البحث الخطي يتمتع بسلوك مثالي للذاكرة المؤقتة ولا يسبب أخطاء تنبؤ في الفروع. وقائمة مترابطة بسيطة تتفوق على شجرة ثنائية متوازنة للمجموعات التي تقل عن نحو 100 عنصر، لأن التنقل بين المؤشرات عبر الشجرة يدمر محلية الذاكرة المؤقتة. أما جداول التجزئة فلها بحث O(1) متوسط، لكن ثابتها مرتفع لدرجة أن البحث الخطي في مصفوفة أسرع للمجموعات التي تقل عن نحو 30-50 عنصرًا.

مكتبات المعايير تدرك ذلك. دالة sorted() في بايثون تستخدم Timsort، الذي يعود إلى ترميز الإدراج للمقاطع الجزئية الصغيرة. أما std::sort في C++ فينتقل إلى ترميز الإدراج تحت حد معين، عادةً بين 16 و32 عنصرًا. وsort_unstable في Rust يجمع بين Quicksort وترميز الإدراج. الخوارزمية «المعقدة» تُستخدم فقط عندما يكون n كبيرًا بما يكفي لتفوز.

الدرس الأوسع: اعرف حجم n. إذا كنت تختار بين خوارزمية بسيطة O(n²) وأخرى معقدة O(n log n)، فاسأل نفسك كم ستكون n فعليًا في الممارسة. إذا كانت أقل من بضع مئات، فالخوارزمية البسيطة مناسبة على الأرجح، وستكون أسهل في الكتابة والتصحيح والصيانة.

القاعدة 4: البساطة أفضل من الذكاء المفرط

القاعدة 4 تمتد من القاعدة 3 إلى ما هو أبعد من الأداء. الخوارزميات المعقدة ليست بطيئة فقط مع n الصغيرة، بل هي أكثر عرضة للأخطاء أيضًا. فشجرة red-black فيها حالات حدّية أكثر من مصفوفة مرتبة، وهيكل البيانات المتزامن بلا أقفال له أنماط فشل أدق من النسخة المحمية بـ mutex، ومخصص الذاكرة المخصص لديه طرق لإفساد الذاكرة أكثر من مخصص النظام.

رأيت فرقًا تقضي أسابيع في تنفيذ وتصحيح ذاكرة تخزين LRU مخصصة بعمليات O(1)، بينما كان بالإمكان كتابة مصفوفة محدودة بسيطة مع إخلاء خطي في فترة بعد الظهر، وتكون صحيحة من المحاولة الأولى، وسريعة بما يكفي لحمولتهم، وهي لم تتجاوز بضع مئات من مدخلات الذاكرة.

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

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

القاعدة 5: البيانات هي المهيمنة

هذه أهم قواعد Pike وأكثرها إغفالًا من المطورين الذين يركزون على الخوارزميات وأنماط التصميم. الادعاء بسيط: إذا كانت هياكل بياناتك صحيحة، فإن الخوارزميات تأتي تلقائيًا. وإذا كانت هياكل بياناتك خاطئة، فلن تنقذك أي براعة خوارزمية.

قال Fred Brooks شيئًا مشابهًا: «أرِني مخططاتك الانسيابية وأخفِ عني جداولك، وسأظل محتارًا. أرِني جداولك، ولن أحتاج عادةً إلى مخططاتك الانسيابية.» وردّد Linus Torvalds المعنى نفسه: «المبرمجون السيئون يقلقون بشأن الكود. المبرمجون الجيدون يقلقون بشأن هياكل البيانات وعلاقاتها.»

هذا المبدأ يظهر باستمرار في الممارسة. قاعدة كود تخزن صلاحيات المستخدمين كقائمة مسطحة من نصوص الصلاحيات ستتراكم فيها منطق تحقق معقد وعرضة للأخطاء، موزع في كل مكان. أعد هيكلة البيانات كتسلسل هرمي للأدوار، وسيصبح منطق التحقق تافهًا. ونظام يخزن الأحداث كـ JSON blobs سيحتاج إلى تحليل وتحقق معقدين عند كل مستهلك. ابنِ الأحداث كسجلات مُنمّطة بمخططات صريحة، وسيتبسط المستهلكون بشكل كبير.

# Bad data structure → complex algorithm
class OrderSystem:
def __init__(self):
self.orders = []  # flat list of all orders
def get_user_orders(self, user_id):
return [o for o in self.orders if o.user_id == user_id]  # O(n)
def get_pending_orders(self):
return [o for o in self.orders if o.status == 'pending']  # O(n)
def get_user_pending_orders(self, user_id):
return [o for o in self.orders
if o.user_id == user_id and o.status == 'pending']  # O(n)
# Better data structure → algorithms become obvious
class OrderSystem:
def __init__(self):
self.orders_by_user = {}      # user_id → [orders]
self.orders_by_status = {}    # status → [orders]
def get_user_orders(self, user_id):
return self.orders_by_user.get(user_id, [])  # O(1)
def get_pending_orders(self):
return self.orders_by_status.get('pending', [])  # O(1)
# The right data structure makes the code self-evident.
# You don't need to think about algorithms at all.

كيف تُجسّد Go هذه القواعد

من الصعب النظر إلى قواعد Pike دون رؤية فلسفة تصميم Go في طور التكوين. فلغة Go، التي شارك Pike في إنشائها بعد 20 عامًا من كتابة هذه القواعد، تفضّل البساطة على الذكاء المفرط بشكل منهجي.

  • لا generics (في البداية)، لفرض هياكل بيانات بسيطة. (أُضيفت الـ generics في Go 1.18، لكن بعد سنوات من المقاومة حتى وُجد تصميم بسيط بما يكفي.)
  • لا overloading للعوامل، فالكود يعني ما يبدو عليه.
  • لا تحويلات ضمنية للأنواع، فالصراحة أفضل من الذكاء المفرط.
  • لا استثناءات، فتُعالج الأخطاء حيث تحدث.
  • خوارزميات مكتبة قياسية محدودة، فاستخدم الشرائح والخرائط بدلًا من هياكل البيانات المعقدة.
  • أداة تحليل أداء مدمجة (pprof)، فاقِس ولا تخمّن.

يُنتقد Go غالبًا بوصفه «مملًا» من قبل المطورين الذين يفضلون لغات أكثر تعبيرًا. وهذه بالضبط هي النقطة. قواعد Pike وصفة لكود مملّ، بسيط وقابل للقياس ومبني على هياكل بيانات جيدة بدلًا من خوارزميات ذكية. وGo هي ما يحدث عندما تحوّل هذه الوصفة إلى لغة.

متى لا تنطبق القواعد

لا توجد مجموعة قواعد شاملة، ولقواعد Pike استثناءات مشروعة. فالأنظمة الحساسة للأداء، مثل محركات الألعاب والبنية الداخلية لقواعد البيانات والمترجمات، تحتاج أحيانًا إلى خوارزميات معقدة لأن n لديها كبيرة فعلًا. وكود البنية التحتية الذي يعمل ملايين المرات في الثانية يبرر تحسينات لا تحتاجها تطبيقات الأعمال. وأحيانًا تكون الخوارزمية «البسيطة» ذات تعقيد O(n³) غير مقبول حتى مع n متواضعة.

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

بعد 37 عامًا من كتابة Pike لها، تظل هذه القواعد من أفضل نصائح البرمجة المنشورة على الإطلاق. ليس لأنها مفاجئة، فمعظم المطورين ذوي الخبرة يقرؤونها ويقولون «نعم، بديهي». القيمة تكمن في أنها مكتوبة بوضوح كافٍ لتطبيقها باستمرار. في المرة القادمة التي تمد فيها يدك إلى شجرة red-black، أو مخصص ذاكرة، أو «تحسين» لم تحلله بعد، تذكّر: قِس أولًا، وأبقِ الأمور بسيطة، واجعل هياكل البيانات صحيحة.