كلنا نقرأ عن Clean Code ونُعجب بمبادئه، لكن عندما نفتح مشروعاً حقيقياً نجد دوالاً طولها ٣٠٠ سطر ومتغيرات اسمها x. لماذا يحدث هذا؟ وكيف ننتقل من النظرية إلى التطبيق العملي دون أن نضحي بالسرعة؟
في أحد المشاريع الكبيرة التي عملت عليها، كان لدينا ملف واحد يحتوي على دالة تحوي ٨٠٠ سطر من الكود. نعم، ثمانمائة. وعندما قررت إعادة كتابتها، اكتشفنا أنها كانت تُعالج ٧ حالات مختلفة، وكل حالة لها ٣ مسارات فرعية على الأقل. المشكلة لم تكن في طول الدالة فقط، بل في أن كل مبرمج جديد كان يضيف شرطاً جديداً دون أن يلمس الكود القديم خوفاً من «كسر شيء ما». النتيجة؟ بعد عامين، أصبح هذا الملف «منطقة محظورة» لا يجرؤ أحد على فتحه. هذا هو الفارق بين الكود النظري والكود الحقيقي: المبادئ تُكتب في الكتب وتُشرح في المؤتمرات، لكن التطبيق يتطلب شجاعة ومهارة لا يمتلكها الكثيرون.
الحقيقة هي أن Clean Code ليس مجرد مجموعة من القواعد التي نضعها في ملف README وننساها. إنه أسلوب تفكير يتطلب منا فهم كيف يعمل الكود خلف الكواليس: كيف تُدار الذاكرة، كيف يتعامل المعالج مع الـ Loops، وكيف تؤثر الـ I/O Operations على أداء السيرفر. عندما تفهم هذه التفاصيل، ستدرك لماذا يُعتبر الكود السيئ «قاتلاً للصيانة» وليس مجرد «كود قبيح». مثلاً، دالة طولها ٢٠٠ سطر لا تُعاني فقط من صعوبة القراءة، بل تُسبب أيضاً مشاكل في الـ Call Stack إذا كانت تحتوي على تكرار متداخل، وقد تؤدي إلى Memory Leaks إذا لم تُحرر الموارد بشكل صحيح.
الكثيرون يعتقدون أن تقسيم الدوال إلى وحدات صغيرة هو مجرد «تجميل» للكود. لكن الحقيقة أعمق من ذلك بكثير. الدوال الصغيرة تُقلل من التعقيد المعرفي (Cognitive Complexity) الذي يواجهه المبرمج عند قراءة الكود. عندما ترى دالة اسمها getUserOrders بدلاً من دالة اسمها processUserData التي تفعل كل شيء، فإن عقلك يُركز على مهمة واحدة بدلاً من محاولة تتبع ١٠ مسارات مختلفة. هذا ليس مجرد رأي شخصي، بل مدعوم بأبحاث في علم النفس المعرفي تُظهر أن البشر لا يستطيعون معالجة أكثر من ٧±٢ عناصر في وقت واحد.
لكن كيف نُحدد حجم الدالة المناسب؟ القاعدة التي أتبعها شخصياً هي: إذا اضطررت للتمرير أكثر من مرتين لرؤية كامل الدالة، فهي طويلة جداً. وإذا احتجت إلى تعليقات لتشرح ما يفعله جزء معين من الدالة، فهذا يعني أن هذا الجزء يجب أن يكون دالة مستقلة. مثلاً، في مشروع سابق، كانت لدينا دالة تُسمى validateAndProcessOrder تتعامل مع التحقق من صحة الطلب ومعالجته وإرسال الإشعارات. بعد إعادة البناء، أصبح لدينا ثلاث دوال: validateOrder، processOrder، و sendOrderNotification. النتيجة؟ انخفض معدل الأخطاء بنسبة ٤٠٪ لأن كل دالة أصبحت مسؤولة عن مهمة واحدة فقط، وسهُل اختبارها بشكل مستقل.
// قبل: دالة واحدة تفعل كل شيء
async function validateAndProcessOrder(order) {
// التحقق من صحة الطلب
if (!order.userId) throw new Error('Invalid user');
if (!order.items || order.items.length === 0) throw new Error('No items');
// معالجة الطلب
const total = order.items.reduce((sum, item) => sum + item.price, 0);
if (total > order.user.balance) throw new Error('Insufficient balance');
// خصم المبلغ
await db.users.updateOne(
{ _id: order.userId },
{ $inc: { balance: -total } }
);
// إنشاء الطلب
const newOrder = await db.orders.insertOne({
userId: order.userId,
items: order.items,
total,
status: 'processing'
});
// إرسال إشعار
await notificationService.send({
userId: order.userId,
message: `Your order #${newOrder.insertedId} is being processed`
});
return newOrder;
}
// بعد: تقسيم الدالة إلى مهام مستقلة
async function validateOrder(order) {
if (!order.userId) throw new Error('Invalid user');
if (!order.items || order.items.length === 0) throw new Error('No items');
const total = order.items.reduce((sum, item) => sum + item.price, 0);
if (total > order.user.balance) throw new Error('Insufficient balance');
return total;
}
async function processOrder(order, total) {
await db.users.updateOne(
{ _id: order.userId },
{ $inc: { balance: -total } }
);
return await db.orders.insertOne({
userId: order.userId,
items: order.items,
total,
status: 'processing'
});
}
async function sendOrderNotification(userId, orderId) {
await notificationService.send({
userId,
message: `Your order #${orderId} is being processed`
});
}
// الدالة الرئيسية التي تجمع المهام
async function handleOrder(order) {
const total = await validateOrder(order);
const newOrder = await processOrder(order, total);
await sendOrderNotification(order.userId, newOrder.insertedId);
return newOrder;
}إذا سألت أي مبرمج عن أسوأ جزء في الكود الذي يعمل عليه، فغالباً سيقول لك: «التسميات». المتغيرات والدوال التي تحمل أسماء مثل x، temp، data، أو process لا تُعطي أي إشارة عما تفعله. لكن المشكلة الأكبر هي أن التسميات السيئة تُضلل المبرمجين الآخرين (أو حتى نفسك بعد شهرين) وتجعلهم يتخذون قرارات خاطئة بناءً على فهم خاطئ. مثلاً، دالة اسمها getData قد تُعيد بيانات المستخدم، أو بيانات المنتج، أو حتى بيانات النظام. بدون سياق واضح، ستضطر لفتح الدالة وقراءة الكود لفهم ما تفعله، وهذا يُضيع وقتاً ثميناً.
القاعدة الذهبية التي أتبعها هي: إذا لم تستطع تسمية دالة أو متغير بشكل واضح، فهذا يعني أنك لم تفهم المشكلة جيداً بعد. التسميات الجيدة تُخبرك بما تفعله الدالة (أو المتغير) وليس كيف تفعلها. مثلاً، دالة اسمها isUserEligibleForDiscount أفضل بكثير من دالة اسمها checkDiscountRules لأن الأولى تُخبرك بالنتيجة المتوقعة، بينما الثانية تُخبرك بالعملية الداخلية. في مشروع سابق، كان لدينا متغير اسمه flag يُستخدم لتحديد ما إذا كان المستخدم نشطاً أم لا. بعد إعادة التسمية إلى isUserActive، انخفض عدد الأسئلة في الـ Code Review بنسبة ٦٠٪ لأن المبرمجين لم يعودوا بحاجة لطرح أسئلة مثل «ماذا يعني هذا المتغير بالضبط؟».
# قبل: تسميات غامضة وغير واضحة
class User:
def __init__(self, d):
self.d = d
self.x = False
self.t = None
def p(self):
if self.d['status'] == 'active' and self.d['last_login'] > datetime.now() - timedelta(days=30):
self.x = True
return self.x
# بعد: تسميات واضحة تُخبرك بما يفعله الكود
class User:
def __init__(self, user_data):
self.user_data = user_data
self.is_active = False
self.last_login_time = None
def check_user_activity(self):
if (self.user_data['account_status'] == 'active' and
self.user_data['last_login_at'] > datetime.now() - timedelta(days=30)):
self.is_active = True
return self.is_activeعندما نتحدث عن التسميات في الكود غير المتزامن، تصبح الأمور أكثر تعقيداً. الدوال التي تُعيد Promises أو تستخدم async/await تحتاج إلى تسميات تُخبر المبرمج بأنها غير متزامنة وأنها قد تُلقي استثناءات. مثلاً، دالة اسمها fetchUserData أفضل بكثير من دالة اسمها getUser لأن الأولى تُخبرك بأنها ستقوم بعملية I/O قد تستغرق وقتاً. في مشروع سابق، كان لدينا دالة اسمها loadConfig تُعيد Promise لكنها لم تكن تحمل أي إشارة إلى أنها غير متزامنة. عندما أضفنا البادئة async_ ليصبح اسمها async_loadConfig، أصبح من الواضح لأي مبرمج جديد أن هذه الدالة تحتاج إلى await.
// قبل: تسمية لا تُظهر الطبيعة غير المتزامنة
async function getUser(id: string) {
const resp await fetch(`/api/users/${id}`);
if (!response.ok) throw new Error('Failed to fetch user');
return await response.json();
}
// بعد: تسمية تُظهر أن الدالة غير متزامنة وقد تُلقي استثناءات
async function fetchUserData(userId: string): Promise<User> {
const response = await fetch(`/api/users/${${userId}}`);
if (!response.ok) throw new Error(`Failed to fetch user ${userId}`);
return await response.json();
}الكثير من المبرمجين يستخدمون التعليقات لتبرير الكود السيئ بدلاً من إصلاحه. مثلاً، تجد تعليقاً يقول «هذه الدالة تقوم بثلاثة أشياء لكنها تعمل بشكل جيد» بدلاً من تقسيم الدالة إلى ثلاث دوال مستقلة. الحقيقة هي أن التعليقات الجيدة نادرة، والتعليقات السيئة تُسبب ضرراً أكبر من فائدتها لأنها تُضلل المبرمجين وتجعلهم يعتمدون عليها بدلاً من قراءة الكود نفسه. في أحد المشاريع، كان لدينا ملف كامل يحتوي على تعليقات توضيحية لكل سطر تقريباً، وعندما قررنا إزالة هذه التعليقات وإعادة كتابة الكود ليكون أكثر وضوحاً، اكتشفنا أن نصف التعليقات كانت خاطئة أو قديمة ولم تُحدث منذ سنوات.
القاعدة التي أتبعها هي: إذا وجدت نفسك تكتب تعليقاً لتشرح ما يفعله الكود، فهذا يعني أن الكود يحتاج إلى إعادة كتابة. التعليقات يجب أن تُشرح لماذا يفعل الكود شيئاً معيناً، وليس ماذا يفعل. مثلاً، تعليق يقول «نستخدم خوارزمية X هنا لأنها أسرع بمقدار ٢٠٪ من الخوارزمية Y» مفيد، بينما تعليق يقول «هذه الحلقة تُكرر على عناصر المصفوفة» ليس له أي قيمة. في مشروع سابق، كان لدينا دالة تُسمى calculateDiscount تحتوي على تعليق يقول «هذه الدالة تحسب الخصم بناءً على قواعد معقدة». بعد إعادة الكتابة، أصبح لدينا دالة اسمها calculateDiscountBasedOnLoyaltyAndPurchaseHistory، ولم نكن بحاجة لأي تعليق لأن اسم الدالة يُخبرك بكل شيء.
// قبل: تعليقات تُشرح ما يفعله الكود (غير مفيدة)
public class DiscountCalculator {
// هذه الدالة تحسب الخصم
public double calculateDiscount(User user, Order order) {
double discount = 0.0;
// تحقق إذا كان المستخدم مؤهلاً للخصم
if (user.getLoyaltyPoints() > 1000) {
discount = 0.1; // خصم 10%
}
// تحقق إذا كان الطلب يحتوي على أكثر من 5 عناصر
if (order.getItems().size() > 5) {
discount += 0.05; // خصم إضافي 5%
}
return discount;
}
}
// بعد: كود واضح لا يحتاج إلى تعليقات
public class DiscountCalculator {
public double calculateDiscountBasedOnLoyaltyAndPurchaseHistory(User user, Order order) {
double loyaltyDiscount = calculateLoyaltyDiscount(user);
double bulkPurchaseDiscount = calculateBulkPurchaseDiscount(order);
return loyaltyDiscount + bulkPurchaseDiscount;
}
private double calculateLoyaltyDiscount(User user) {
return user.getLoyaltyPoints() > 1000 ? 0.1 : 0.0;
}
private double calculateBulkPurchaseDiscount(Order order) {
return order.getItems().size() > 5 ? 0.05 : 0.0;
}
}الـ Side Effects هي واحدة من أكبر مشاكل الكود الحقيقي. عندما تُعدل دالة ما متغيراً خارج نطاقها، أو تُحدث تغييراً في قاعدة البيانات، أو تُرسل رسالة عبر الشبكة، فإنك تُدخل عنصراً غير متوقع في الكود. هذه الـ Side Effects تجعل الكود صعب الاختبار، وصعب التنبؤ بسلوكه، وعرضة للأخطاء التي لا تظهر إلا في بيئة الإنتاج. مثلاً، في مشروع سابق، كانت لدينا دالة تُسمى updateUserProfile تقوم بتحديث بيانات المستخدم وإرسال إشعار عبر البريد الإلكتروني في نفس الوقت. عندما حاولنا اختبار هذه الدالة، وجدنا أننا لا نستطيع اختبار الجزء المتعلق بتحديث البيانات دون إرسال بريد إلكتروني حقيقي، مما جعل الاختبارات بطيئة وغير موثوقة.
الحل هو عزل الـ Side Effects قدر الإمكان. الدوال التي تُحدث تغييرات في العالم الخارجي يجب أن تكون منفصلة عن الدوال التي تُعالج البيانات. مثلاً، بدلاً من دالة واحدة تقوم بالتحديث والإرسال، يجب أن يكون لديك دالتان: الأولى تُحدث التغيير في قاعدة البيانات، والثانية تُرسل الإشعار. هذا الأسلوب يُعرف باسم Command-Query Separation (CQS)، وهو أحد المبادئ الأساسية في هندسة البرمجيات. في المثال السابق، بعد فصل الـ Side Effects، أصبح بإمكاننا اختبار دالة updateUserProfile دون الحاجة لإرسال بريد إلكتروني حقيقي، مما جعل الاختبارات أسرع وأكثر موثوقية.
// قبل: دالة تحتوي على Side Effects تجعل الاختبار صعباً
public class UserService {
private readonly IEmailService _emailService;
public UserService(IEmailService emailService) {
_emailService = emailService;
}
public void UpdateUserProfile(User user, string newName) {
user.Name = newName;
_emailService.Send(
user.Email,
"Profile Updated",
$"Your name has been updated to {newName}"
);
}
}
// بعد: فصل الـ Side Effects عن معالجة البيانات
public class UserService {
private readonly IEmailService _emailService;
public UserService(IEmailService emailService) {
_emailService = emailService;
}
public User UpdateUserProfile(User user, string newName) {
user.Name = newName;
return user;
}
public void NotifyProfileUpdate(User user) {
_emailService.Send(
user.Email,
"Profile Updated",
$"Your name has been updated to {user.Name}"
);
}
}
// الاستخدام:
var updatedUser = userService.UpdateUserProfile(user, "New Name");
userService.NotifyProfileUpdate(updatedUser);إذا كان الكود صعب الاختبار، فهو كود سيئ. هذه قاعدة بسيطة لكنها قوية. عندما تحاول كتابة اختبارات لوحدة معينة، ستكتشف بسرعة ما إذا كانت الدالة تقوم بأكثر من مهمة واحدة، أو تعتمد على موارد خارجية، أو تحتوي على تعقيد غير ضروري. مثلاً، دالة تعتمد على قاعدة بيانات حقيقية أو خدمة خارجية ستجعل الاختبارات بطيئة وغير موثوقة، بينما الدالة التي تعتمد على مدخلات ومخرجات واضحة ستجعل الاختبارات سريعة وسهلة الكتابة. في أحد المشاريع، كان لدينا دالة تُسمى calculateTotalPrice تعتمد على استدعاء API خارجي للحصول على أسعار الشحن. عندما حاولنا كتابة اختبارات لهذه الدالة، وجدنا أننا لا نستطيع التحكم في الاستجابة من الـ API، مما جعل الاختبارات غير متوقعة. بعد إعادة البناء، فصلنا الجزء المتعلق بحساب السعر عن الجزء المتعلق باستدعاء الـ API، وأصبح بإمكاننا اختبار الدالة بسهولة باستخدام بيانات وهمية.
الكود القابل للاختبار يُجبرك على اتباع مبادئ Clean Code بشكل تلقائي. عندما تُجبر نفسك على كتابة اختبارات، ستجد أنك تُقسم الدوال إلى وحدات صغيرة، وتستخدم التسميات الواضحة، وتقلل من الـ Side Effects. هذا ليس مجرد رأي شخصي، بل مدعوم بدراسات تُظهر أن المشاريع التي تحتوي على اختبارات جيدة لديها معدل أخطاء أقل بنسبة تصل إلى ٥٠٪. القاعدة التي أتبعها هي: إذا لم تستطع كتابة اختبار لوحدة معينة في أقل من ٥ دقائق، فهذا يعني أن الكود يحتاج إلى إعادة بناء.
# قبل: دالة تعتمد على مورد خارجي صعب الاختبار
import requests
def calculate_total_price(items, shipping_address):
subtotal = sum(item['price'] for item in items)
# استدعاء API للحصول على سعر الشحن
resp requests.get(
f"https://api.shipping.com/calculate?address={shipping_address}"
)
shipping_cost = response.json()['cost']
return subtotal + shipping_cost
# بعد: فصل الاعتماد على المورد الخارجي لجعل الكود قابلاً للاختبار
class ShippingService:
def get_shipping_cost(self, address):
response = requests.get(
f"https://api.shipping.com/calculate?address={address}"
)
return response.json()['cost']
def calculate_total_price(items, shipping_service, shipping_address):
subtotal = sum(item['price'] for item in items)
shipping_cost = shipping_service.get_shipping_cost(shipping_address)
return subtotal + shipping_cost
# اختبار الوحدة باستخدام Mock
from unittest.mock import Mock
def test_calculate_total_price():
mock_shipping = Mock()
mock_shipping.get_shipping_cost.return_value = 5.0
items = [{'price': 10}, {'price': 20}]
total = calculate_total_price(items, mock_shipping, "123 Main St")
assert total == 35.0في نهاية اليوم، Clean Code ليس مجرد مجموعة من القواعد التي تُطبق مرة واحدة ثم تُنسى. إنه عملية مستمرة تتطلب منك إعادة النظر في الكود الذي كتبته بالأمس، وتحدي نفسك لتحسينه اليوم. المبادئ التي تحدثنا عنها ليست مجرد نظريات، بل أدوات عملية تُساعدك على كتابة كود أسهل قراءة، وصيانة، واختباراً. لكن تذكر: لا يوجد كود مثالي، والكود الجيد اليوم قد يصبح سيئاً غداً عندما تتغير المتطلبات أو تتطور التقنيات. لذا، بدلاً من السعي للكمال، ركز على التحسين المستمر، وكن مستعداً لإعادة كتابة الكود عندما يصبح عائقاً بدلاً من أصل.
نصيحة أخيرة من مهندس إلى مهندس: عندما تفتح ملفاً قديماً وتجد نفسك تتساءل «من كتب هذا الكود السيئ؟»، لا تنسَ أن تنظر إلى التوقيع في أعلى الملف. غالباً ستجد اسمك هناك. هذا ليس فشلاً، بل دليل على أنك تتطور. Clean Code ليس هدفاً نهائياً، بل أسلوب حياة للمبرمج المحترف.