لماذا يقرأ المطورون عن Clean Code لكنهم يكتبون دوالاً طولها ٢٠٠ سطر؟ اكتشف المبادئ الحقيقية التي تُطبق في الشركات الكبرى وكيف تحول الكود الفوضوي إلى نظام قابل للصيانة دون إهدار الوقت.
في أحد المشاريع الكبيرة لشركة سعودية للتجارة الإلكترونية، كان هناك ملف واحد اسمه utils.js يحتوي على ٣٥٠٠ سطر من الكود. داخل هذا الملف، كانت هناك دالة وحيدة اسمها processData تستقبل ١٢ باراميتر، وتتعامل مع ٤ أنواع مختلفة من البيانات، وتستخدم ٣ مكتبات خارجية، وتكتب في ملفين مختلفين. عندما طلب مني المدير تعديل ميزة الدفع السريع، وجدت نفسي أضيع ٣ ساعات فقط في محاولة فهم ما تفعله هذه الدالة قبل أن أجرؤ على تغيير سطر واحد. هذه ليست مبالغة — إنها حقيقة يومية في معظم فرق التطوير التي تتحدث عن Clean Code لكنها لا تطبقه فعلياً.
المشكلة ليست في جهل المطورين بمبادئ Clean Code، بل في الفجوة بين النظرية والتطبيق. الكل يعرف أن الدوال يجب أن تكون قصيرة، لكن كم مطوراً يكتب دوالاً لا تتجاوز ٢٠ سطراً؟ الكل يعرف أن المتغيرات يجب أن تكون ذات أسماء واضحة، لكن كم مشروعاً يستخدم i وj وk في loops معقدة؟ الحقيقة هي أن تطبيق Clean Code يتطلب أكثر من مجرد قراءة كتاب — يتطلب تغييراً في العقلية، وفهماً عميقاً لكيفية عمل الكود خلف الكواليس، واستعداداً لدفع ثمن الوقت الإضافي في البداية مقابل توفير أضعافه لاحقاً.
كثيراً ما أسمع مطورين يقولون: "أنا أكتب كود clean لأنني أستخدم Prettier وESLint وأجعل الأكواد منظمة بشكل جميل". لكن الحقيقة هي أن جمال الشكل هو مجرد طبقة سطحية من Clean Code. الكود النظيف الحقيقي يبدأ من الذاكرة والمعالج، وليس من شكل الأقواس أو المسافات. مثلاً، دالة بسيطة مثل هذه قد تبدو جميلة الشكل لكنها كارثة من حيث الأداء والصيانة:
// مثال على كود "جميل الشكل" لكنه غير clean
function processOrders(orders) {
let result = [];
for (let i = 0; i < orders.length; i++) {
if (orders[i].status === 'completed') {
let total = 0;
for (let j = 0; j < orders[i].items.length; j++) {
total += orders[i].items[j].price * orders[i].items[j].quantity;
}
if (total > 1000) {
result.push({
orderId: orders[i].id,
total: total,
discount: total * 0.1
});
} else {
result.push({
orderId: orders[i].id,
total: total
});
}
}
}
return result;
}هذه الدالة تبدو منظمة ومرتبة، لكنها تنتهك عدة مبادئ أساسية من Clean Code. أولاً، إنها تقوم بأكثر من مهمة واحدة: فلترة الطلبات المكتملة، حساب الإجمالي، وتطبيق الخصم. ثانياً، عمق الـ Nesting يصل إلى ٤ مستويات، مما يجعل تتبع مسار التنفيذ صعباً للغاية. ثالثاً، المتغيرات المؤقتة مثل total وi وj ليس لها أسماء ذات معنى، مما يجبر القارئ على تذكر ما تمثله في كل مستوى من الـ Loop. خلف الكواليس، هذا الكود يسبب ضغطاً على الـ Event Loop في Node.js لأن كل عملية حسابية تتم بشكل متزامن داخل Loop واحد، مما قد يؤدي إلى تجميد السيرفر عند معالجة آلاف الطلبات.
القاعدة الذهبية في Clean Code هي أن الدوال يجب أن تفعل شيئاً واحداً فقط، وأن تفعله جيداً. لكن المشكلة هي أن معظم المطورين لا يعرفون كيف يحددون "شيئاً واحداً". هل حساب الإجمالي هو شيء واحد؟ أم أنه يجب تقسيمه إلى خطوات أصغر؟ في تجربتي، أفضل طريقة لتحديد ما إذا كانت الدالة تفعل شيئاً واحداً هي محاولة وصف ما تفعله بجملة واحدة تحتوي على فعل واحد فقط. مثلاً، إذا احتجت إلى استخدام كلمات مثل "و" أو "ثم" في وصف الدالة، فمن المرجح أنها تفعل أكثر من شيء واحد.
لنعد كتابة الدالة السابقة بشكل clean. أولاً، سنقسمها إلى ثلاث دوال صغيرة، كل منها تفعل شيئاً واحداً فقط:
// تقسيم الدالة إلى مهام صغيرة ومحددة
function isOrderCompleted(order) {
return order.status === 'completed';
}
function calculateOrderTotal(order) {
return order.items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
}
function applyDiscountIfEligible(orderTotal) {
return orderTotal > 1000 ? { total: orderTotal, discount: orderTotal * 0.1 } : { total: orderTotal };
}
function processOrders(orders) {
return orders
.filter(isOrderCompleted)
.map(order => {
const total = calculateOrderTotal(order);
return { orderId: order.id, ...applyDiscountIfEligible(total) };
});
}الآن، كل دالة تفعل شيئاً واحداً فقط، ويمكن اختبارها بشكل مستقل. مثلاً، دالة calculateOrderTotal يمكن اختبارها بسهولة باستخدام بيانات وهمية، دون الحاجة إلى إنشاء طلب كامل. أيضاً، عمق الـ Nesting أصبح صفراً، مما يجعل الكود أسهل في القراءة والفهم. خلف الكواليس، هذا الكود أكثر كفاءة لأننا نستخدم دوالاً نقية (pure functions) لا تعتمد على حالة خارجية، مما يجعلها قابلة للتخزين المؤقت (caching) والتشغيل المتوازي (parallel execution).
الكثير من المطورين يقعون في فخ تقسيم الدوال بشكل مبالغ فيه، مما يؤدي إلى كود مليء بالدوال الصغيرة التي لا تضيف قيمة حقيقية. مثلاً، كتابة دالة منفصلة لكل عملية حسابية بسيطة مثل جمع رقمين هو مبالغة. القاعدة التي أتبعها هي: إذا كانت الدالة لا تزيد عن ٥ أسطر ولا يمكن إعادة استخدامها في مكان آخر، فمن الأفضل دمجها مع الدالة الرئيسية. أيضاً، تجنب تقسيم الدوال إذا كان ذلك يؤدي إلى تمرير نفس الباراميترات بين عدة دوال بشكل متكرر، فهذا يجعل الكود أكثر تعقيداً بدلاً من تبسيطه.
في أحد المشاريع التي عملت عليها لشركة ألمانية لتصنيع السيارات، كان هناك متغير اسمه data يستخدم في أكثر من ٢٠ مكاناً في الكود. في بعض الأماكن كان يمثل قائمة المستخدمين، وفي أماكن أخرى كان يمثل تفاصيل منتج، وفي أماكن ثالثة كان يمثل استجابة من API. عندما حاول فريق جديد الانضمام إلى المشروع، اضطروا إلى تتبع كل استخدام لهذا المتغير لفهم ما يمثله في كل سياق. هذا ليس مجرد سوء تسمية — إنه فشل في التواصل.
الأسماء في الكود ليست مجرد تسميات عشوائية، بل هي أول وأهم شكل من أشكال التوثيق. اسم جيد يخبرك بما يمثل المتغير أو الدالة دون الحاجة إلى قراءة الكود الذي يستخدمه. مثلاً، مقارنة بين هذين الاسمين:
لكن الأسماء الجيدة ليست مجرد أسماء طويلة ومعقدة. في الواقع، الأسماء الطويلة جداً قد تكون أسوأ من القصيرة جداً لأنها تجعل الكود صعب القراءة. مثلاً، compareUserAccountCreationDateWithCurrentDate هو اسم طويل جداً ويمكن اختصاره إلى isUserAccountOld دون فقدان المعنى. القاعدة التي أتبعها هي: إذا احتجت إلى شرح الاسم باستخدام تعليق، فمن المرجح أن الاسم غير جيد.
أولاً، استخدم أسماء تعكس الغرض وليس التنفيذ. مثلاً، إذا كان لديك دالة تحسب عدد المستخدمين النشطين، فاستخدم اسم countActiveUsers بدلاً من getUsersLength. الثاني، تجنب الأسماء التي تعتمد على السياق الخارجي. مثلاً، إذا كان لديك متغير يمثل سعر المنتج داخل دالة تعالج الطلبات، فلا تسميه price لأن السياق واضح، بل استخدم productPrice لتمييزه عن أي سعر آخر قد يظهر في الدالة. ثالثاً، استخدم اصطلاحات الفريق أو اللغة. مثلاً، في جافاسكريبت، من الشائع استخدام is للدلالة على القيم البوليانية، مثل isActive بدلاً من activeStatus.
# مثال على أسماء سيئة وجيدة في نفس السياق
# أسماء سيئة
users = get_data()
for i in range(len(users)):
if users[i]['status'] == 1:
x = users[i]['date']
if x > '2023-01-01':
print(users[i]['name'])
# أسماء جيدة
active_users = fetch_active_users()
for user in active_users:
if user.is_active:
account_creati user.registration_date
if account_creation_date > datetime.date(2023, 1, 1):
print(user.full_name)في أحد المراجعات الكودية التي أجريتها لمطور مبتدئ، وجدت تعليقاً يقول: "// زيادة العداد بمقدار واحد". هذا التعليق لا يضيف أي قيمة لأن الكود نفسه يقول i++ وهو واضح تماماً. لكن في نفس المشروع، وجدت دالة معقدة بدون أي تعليقات، وعندما سألته عن سبب ذلك، قال: "الكود واضح ولا يحتاج إلى تعليقات". الحقيقة هي أن التعليقات ليست جيدة أو سيئة بحد ذاتها — إنها إما تغطي على كود سيء، أو تشرح سبب وجود كود جيد.
المشكلة الأكبر مع التعليقات هي أنها تكذب. الكود يتغير باستمرار، لكن التعليقات نادراً ما يتم تحديثها. مثلاً، قد تجد تعليقاً يقول "هذه الدالة تُرجع قائمة المستخدمين النشطين" بينما الكود الفعلي يُرجع قائمة المستخدمين غير النشطين لأن أحدهم غيّر الكود ولم يغير التعليق. لهذا السبب، أفضل قاعدة هي: إذا كنت بحاجة إلى تعليق لشرح ما يفعله الكود، فمن المرجح أن الكود نفسه غير واضح ويجب إعادة كتابته. لكن إذا كنت بحاجة إلى شرح لماذا تم كتابة الكود بهذه الطريقة، فالتعليق قد يكون مفيداً.
// مثال على تعليقات سيئة وجيدة
// تعليق سيء: يشرح ما يفعله الكود
// حساب الخصم بناءً على السعر الإجمالي
if (total > 1000) {
discount = total * 0.1;
}
// تعليق جيد: يشرح لماذا تم كتابة الكود بهذه الطريقة
// نطبق خصم 10% على الطلبات التي تزيد عن 1000 ريال
// وفقاً لقرار الإدارة في اجتماع 2023-05-15
if (total > 1000) {
discount = total * 0.1;
}لكن هناك حالات يجب فيها استخدام التعليقات. مثلاً، في الكود الذي يتعامل مع خوارزميات معقدة أو منطق تجاري غير واضح، قد يكون من الضروري إضافة تعليقات تشرح الخطوات. أيضاً، في واجهات البرمجة (APIs)، التعليقات ضرورية لتوثيق الباراميترات والقيم المرجعة. لكن حتى في هذه الحالات، يجب أن تكون التعليقات موجزة ودقيقة، وتجنب تكرار ما يقوله الكود بالفعل.
في معظم المشاريع التي أراجعها، أجد أن التعامل مع الأخطاء هو آخر شيء يفكر فيه المطورون. مثلاً، دالة بسيطة مثل قراءة ملف قد تبدو هكذا:
def read_config_file(file_path):
with open(file_path, 'r') as file:
return file.read()هذه الدالة تبدو بسيطة، لكنها تحتوي على ثلاث مشاكل رئيسية: أولاً، إذا لم يكن الملف موجوداً، ستُرمى استثناء FileNotFoundError دون أي معالجة. ثانياً، إذا كان الملف موجوداً لكنه غير قابل للقراءة (مثلاً بسبب أذونات غير كافية)، ستُرمى استثناء آخر. ثالثاً، إذا كان الملف كبيراً جداً، قد يستهلك الكثير من الذاكرة. في بيئات الإنتاج، هذه المشاكل تؤدي إلى توقف التطبيق بالكامل بدلاً من التعامل مع الخطأ بشكل أنيق.
التعامل مع الأخطاء ليس مجرد إضافة try-catch هنا وهناك. إنه جزء أساسي من تصميم الدوال. مثلاً، الدالة السابقة يجب أن تُعيد قيمة افتراضية إذا فشل القراءة، أو تُرجع خطأ يمكن التعامل معه بسهولة من قبل الكود الذي يستدعيها. أيضاً، يجب أن تُحدد بوضوح ما هي الأخطاء التي تتوقعها وما هي الأخطاء التي لا تتوقعها. مثلاً، في بايثون، من الأفضل التعامل مع الأخطاء المتوقعة (مثل FileNotFoundError) وترك الأخطاء غير المتوقعة (مثل MemoryError) تُرمى كما هي لأنها تشير إلى مشاكل أكبر.
def read_config_file(file_path, default_cNone):
"""
يقرأ ملف التكوين ويعيد محتوياته كسلسلة نصية.
إذا فشل القراءة لأي سبب، تُرجع القيمة الافتراضية.
Args:
file_path (str): مسار ملف التكوين.
default_config (str, optional): القيمة التي تُرجع إذا فشل القراءة.
Returns:
str: محتويات الملف أو القيمة الافتراضية.
"""
try:
with open(file_path, 'r') as file:
return file.read()
except (FileNotFoundError, PermissionError, IOError) as e:
print(f"Warning: Failed to read config file: {e}")
return default_configفي هذا المثال، الدالة تتعامل مع ثلاثة أنواع من الأخطاء الشائعة عند قراءة الملفات، وتعيد قيمة افتراضية بدلاً من التسبب في توقف البرنامج. أيضاً، أضفنا توثيقاً يشرح بوضوح ما تفعله الدالة وماذا تُرجع في حالة الخطأ. هذا يجعل الكود أكثر مرونة ويمكن استخدامه في سياقات مختلفة دون القلق من توقف التطبيق بسبب خطأ بسيط.
أولاً، تجنب استخدام try-catch بشكل مفرط. مثلاً، كتابة try-catch داخل loop قد يخفي الأخطاء الحقيقية ويجعل التصحيح صعباً. ثانياً، لا تستخدم الاستثناءات للتحكم في سير البرنامج. مثلاً، استخدام استثناء لوقف loop هو فكرة سيئة لأن الاستثناءات مكلفة من حيث الأداء. ثالثاً، تجنب إخفاء الأخطاء تماماً. مثلاً، كتابة except: pass هو أسوأ شيء يمكنك فعله لأنه يخفي الأخطاء التي قد تكون مهمة لتصحيح البرنامج.
في بداية مسيرتي المهنية، كنت أعتقد أن الكود النظيف هو شيء تصل إليه مرة واحدة وتبقى عليه للأبد. لكن مع الوقت، أدركت أن الكود النظيف ليس وجهة — إنه عملية مستمرة من التحسين والمراجعة. مثلاً، في مشروع لشركة أمريكية للتكنولوجيا المالية، كنا نكتب كوداً نظيفاً جداً في البداية، لكن بعد ستة أشهر من التطوير المستمر، وجدنا أن بعض الدوال أصبحت طويلة ومعقدة مرة أخرى بسبب إضافة ميزات جديدة. الحل لم يكن إعادة كتابة الكود بالكامل، بل كان اعتماد عملية تسمى "Refactoring المستمر".
الـ Refactoring ليس شيئاً تفعله مرة واحدة قبل إصدار المنتج. إنه جزء من دورة حياة التطوير اليومية. مثلاً، في كل مرة تضيف ميزة جديدة أو تصلح خطأ، يجب أن تسأل نفسك: هل يمكنني تحسين الكود الموجود بدلاً من إضافة كود جديد؟ هل يمكنني تقسيم هذه الدالة الكبيرة إلى دوال أصغر؟ هل يمكنني إزالة هذا التعليق عن طريق إعادة تسمية المتغير؟ هذه الأسئلة الصغيرة، عندما تُطرح باستمرار، تحافظ على نظافة الكود على المدى الطويل.
لكن الـ Refactoring ليس مجرد تحسين الكود — إنه أيضاً تحسين العمليات. مثلاً، في أحد الفرق التي عملت معها، كنا نخصص ٢٠٪ من وقت كل سبرينت للـ Refactoring. هذا يعني أننا كنا نخصص يومين من كل أسبوعين لتحسين الكود الموجود بدلاً من إضافة ميزات جديدة. في البداية، كان المديرون قلقين من أن هذا سيؤخر المشروع، لكن بعد ثلاثة أشهر، وجدنا أن سرعة التطوير زادت بنسبة ٣٠٪ لأن الكود أصبح أسهل في الفهم والتعديل. هذا يثبت أن الاستثمار في الكود النظيف ليس ترفاً — إنه ضرورة لبقاء المشروع قابلاً للصيانة على المدى الطويل.
في نهاية اليوم، الكود النظيف ليس مجرد مجموعة من القواعد التي يجب اتباعها — إنه عقلية. عقلية ترى أن كل سطر من الكود هو استثمار في وقتك المستقبلي ووقت فريقك. عقلية ترى أن الدوال القصيرة والأسماء الواضحة والتعامل مع الأخطاء ليست مجرد "مبادئ نظيفة" بل هي الأساس الذي يبني عليه المشاريع الناجحة. في تجربتي، الفرق التي تكتب كوداً نظيفاً ليست فقط أسرع في التطوير — بل هي أيضاً أكثر سعادة لأن المطورين يقضون وقتاً أقل في محاولة فهم الكود القديم وأكثر وقت في بناء ميزات جديدة.
إذا كان هناك شيء واحد تأخذه من هذا المقال، فليكن هذا: ابدأ اليوم. لا تنتظر حتى "يصبح المشروع نظيفاً" — ابدأ بكودك الحالي. اختر دالة واحدة طويلة ومعقدة، وقم بتقسيمها إلى دوال أصغر. اختر متغيراً واحداً باسم غامض، وأعد تسميته باسم واضح. أضف معالجة خطأ واحدة إلى دالة مهمة. هذه الخطوات الصغيرة، عندما تُكرر باستمرار، ستغير طريقة كتابتك للكود إلى الأبد. تذكر: الكود النظيف لا يُكتب مرة واحدة — بل يُبنى سطراً بسطر، ودالة بدالة، ومراجعة بمراجعة.