“يا زلمة، الكود فيه جنّي!”
أذكرها وكأنها البارحة. كنا نعمل على نظام معقد لإدارة محتوى إحدى المنصات الكبيرة. فريق كامل من المبرمجين، وأكواب الشاي بالنعنع لا تفارق مكاتبنا، والليل يمتزج بالنهار ونحن نلاحق خطأً (bug) غريباً وعنيداً.
القصة كانت كالتالي: كان لدينا “كائن” (Object) يمثل بيانات المستخدم الحالية، بما في ذلك صلاحياته ومقالاته والإعدادات الخاصة به. هذا الكائن يتم تمريره بين عدة وحدات (modules) في النظام. وحدة تعرض معلومات الملف الشخصي، وأخرى تتحقق من الصلاحيات، وثالثة تحفظ التغييرات المؤقتة.
المشكلة؟ كانت صلاحيات المستخدم تتغير بشكل عشوائي! أحياناً يفقد المستخدم قدرته على تعديل مقال في منتصف جلسة العمل، وأحياناً أخرى تظهر له خيارات لا يفترض أن يراها. قضينا أياماً، بل أسابيع، في وضع نقاط توقف (breakpoints) ومراجعة سجلات الأخطاء (logs) حتى كدنا نفقد عقولنا. كلما ظننا أننا حاصرنا المشكلة في دالة معينة، نكتشف أنها بريئة، وأن “الجريمة” حدثت في مكان آخر.
في ليلة من تلك الليالي، وبعد أن استنفدنا كل الاحتمالات، جلست أحدق في الكود بذهن شبه فارغ. خطرت لي فكرة بسيطة: ماذا لو أن إحدى الدوال التي “تقرأ” بيانات المستخدم، تقوم بتعديلها “عن طريق الخطأ”؟ كانت فرضية تبدو سخيفة، فمن المفترض أن تكون دوال القراءة بريئة. ولكن اليأس يدفعك للتفكير خارج الصندوق.
بدأنا رحلة تفتيش مضنية، ووجدنا الجاني مختبئاً في دالة بريئة جداً. دالة كانت مهمتها إنشاء نسخة “مؤقتة” من صلاحيات المستخدم لمنح صلاحية إضافية بشكل مؤقت لواجهة معينة. المبرمج الذي كتبها، بدلاً من أن ينشئ كائناً جديداً، قام بتعديل الكائن الأصلي مباشرة. كان يعتقد أنه يعدّل على نسخة، لكنه في الحقيقة كان يطعن البيانات الأصلية في الظهر! وبما أن أجزاء النظام كلها تشير إلى نفس الكائن في الذاكرة، كان هذا التعديل البسيط ينتشر كالنار في الهشيم، مسبباً فوضى عارمة.
تلك اللحظة كانت نقطة تحول لنا كفريق. أدركنا أن مشكلتنا لم تكن في تلك الدالة بحد ذاتها، بل في فلسفتنا البرمجية بأكملها. كنا نسمح للبيانات بأن تتغير من تحت أقدامنا. هنا، دخلت “الثباتية” (Immutability) لتكون المنقذ.
ما هي الثباتية (Immutability) وليش هي مهمة؟
ببساطة شديدة، الثباتية هي مبدأ يقول: “ما إن يتم إنشاء شيء ما، لا يمكن تغييره أبداً”.
تخيل أن لديك كتاباً مطبوعاً. يمكنك قراءته، نسخه، أو حتى كتابة كتاب جديد مستوحى منه، لكنك لا تستطيع تغيير الكلمات المطبوعة على صفحاته. هذا هو الكائن الثابت (Immutable Object).
في المقابل، تخيل أن لديك دفتر ملاحظات. يمكنك الكتابة فيه، المسح، التعديل، وشطب الصفحات. هذا هو الكائن القابل للتغيير (Mutable Object). معظم لغات البرمجة، بشكل افتراضي، تتعامل مع الكائنات (Objects) والمصفوفات (Arrays) على أنها دفاتر ملاحظات قابلة للتعديل.
قد يبدو الأمر مقيداً في البداية، لكن هذا القيد هو مصدر قوة هائلة. عندما تضمن أن بياناتك لن تتغير بشكل غير متوقع، يصبح الكود الخاص بك:
- أكثر قابلية للتنبؤ (Predictable): يمكنك تمرير البيانات إلى أي دالة وأنت مطمئن أنها لن تعود إليك وقد تغيرت.
- أسهل في التصحيح (Debugging): عندما يحدث خطأ، لن تضطر للبحث في كل مكان قد تم فيه “لمس” هذه البيانات. مصدر التغيير يصبح واضحاً ومحدوداً.
- أكثر أماناً في البيئات المتوازية (Concurrent): عندما تعمل عدة عمليات (threads) في نفس الوقت، فإن محاولة تعديل نفس البيانات قد تؤدي إلى فوضى وفساد للبيانات (Race Conditions). مع البيانات الثابتة، هذه المشكلة تختفي تماماً لأن الجميع يقرأ فقط.
جحيم التغيير: مخاطر البيانات القابلة للتعديل (Mutable Data)
دعنا نتعمق في المخاطر التي واجهناها والتي قد تواجهها أنت أيضاً إذا لم تنتبه لهذا المبدأ.
الآثار الجانبية (Side Effects) الخفية
الأثر الجانبي هو عندما تقوم دالة ما، بالإضافة إلى إرجاع قيمة، بتغيير شيء ما خارج نطاقها الخاص. التعديل المباشر للبيانات هو المثال الكلاسيكي والأكثر خطورة.
لنأخذ مثالاً بسيطاً في JavaScript:
// بيانات المستخدم الأصلية
let user = {
name: "Abu Omar",
role: "user",
loginCount: 5
};
// دالة "بريئة" من المفترض أن ترقي المستخدم لمشرف "مؤقتاً"
function grantAdminAccess(userProfile) {
userProfile.role = "admin"; // 🚨 هنا الكارثة! تعديل مباشر للكائن الأصلي
return userProfile;
}
console.log("الدور قبل الترقية:", user.role); // "user"
// لنقم باستدعاء الدالة
grantAdminAccess(user);
console.log("الدور بعد الترقية:", user.role); // "admin"
لاحظ كيف أن المتغير user الأصلي تغير! الآن تخيل أن هناك جزءاً آخر من النظام يعتمد على أن user.role هي “user”. لقد قمت للتو بكسر ذلك الجزء من النظام دون أن تدري. هذه هي الآثار الجانبية التي تسبب الكوابيس للمبرمجين.
صعوبة التتبع والتصحيح (Debugging Hell)
عندما تكون بياناتك قابلة للتغيير، وتمر عبر 10 أو 20 دالة، ويظهر فيها خطأ، من أين تبدأ البحث؟ أي واحدة من هذه الدوال العشرين هي التي أفسدت البيانات؟ ستضطر إلى تتبع رحلة البيانات خطوة بخطوة، وهو أمر أشبه بالبحث عن إبرة في كومة قش. هذه هي التجربة التي عشناها في قصتنا في البداية.
كيف نطبق الثباتية في مشاريعنا؟ دليل عملي
حسناً يا أبو عمر، أقنعتنا. كيف نبدأ؟ الخبر الجيد أن الأمر أسهل مما تتوقع، وهو يبدأ بتغيير في العقلية قبل تغيير الكود.
1. العقلية أولاً: فكّر كـ “خالق” لا “مُعدِّل”
التحول الذهني هو الأهم. بدلاً من أن تسأل نفسك “كيف أغير هذه الخاصية في الكائن؟”، اسأل “كيف أنشئ كائناً جديداً يحتوي على كل القيم القديمة بالإضافة إلى القيمة الجديدة؟”.
لا تعدّل… بل استبدل. أنشئ نسخة جديدة مع التغييرات المطلوبة.
2. أدوات اللغة الأساسية (JavaScript كمثال)
لغات البرمجة الحديثة توفر أدوات تساعدنا على تطبيق هذا المبدأ بسهولة.
للـ Objects: استخدم الـ Spread Syntax (…)
هذا هو الحل السحري للمثال السابق. بدلاً من التعديل المباشر، ننشئ نسخة جديدة.
let user = {
name: "Abu Omar",
role: "user",
loginCount: 5
};
// الطريقة الصحيحة (الثابتة)
function grantAdminAccess(userProfile) {
// أنشئ كائناً جديداً...
const newUserProfile = {
...userProfile, // 1. انسخ كل الخصائص القديمة
role: "admin" // 2. قم بتجاوز (override) الخاصية التي تريد تغييرها
};
return newUserProfile;
}
console.log("الدور الأصلي قبل:", user.role); // "user"
const adminUser = grantAdminAccess(user);
console.log("الدور الأصلي بعد:", user.role); // "user" ✅ لم يتغير!
console.log("دور النسخة الجديدة:", adminUser.role); // "admin" ✅ حصلنا على ما نريد
للـ Arrays: استخدم الـ Spread Syntax ودوال مثل map, filter, reduce
تجنب الدوال التي تعدل المصفوفة الأصلية مثل push, pop, splice. استخدم بدائلها التي ترجع مصفوفة جديدة.
const numbers = [10, 20, 30];
// ❌ الطريقة الخطأ (Mutable)
// numbers.push(40); // تعدل المصفوفة الأصلية
// ✅ الطريقة الصح (Immutable)
// إضافة عنصر
const newNumbers = [...numbers, 40]; // [10, 20, 30, 40]
// حذف عنصر (مثلاً حذف الرقم 20)
const filteredNumbers = numbers.filter(num => num !== 20); // [10, 30]
// تعديل عنصر (مثلاً مضاعفة كل الأرقام)
const doubledNumbers = numbers.map(num => num * 2); // [20, 40, 60]
// `numbers` الأصلية لم تتغير في كل الحالات السابقة.
3. استخدام المكتبات المساعدة (Libraries)
عندما تتعقد البيانات وتصبح متداخلة (nested objects)، قد يصبح استخدام الـ Spread Syntax مزعجاً بعض الشيء. هنا تأتي مكتبات مثل Immer.js لتبسيط حياتنا.
Immer تسمح لك بكتابة كود يبدو وكأنه يعدل البيانات مباشرة، لكنها في الخفاء تقوم بإنشاء نسخة جديدة بشكل آمن. إنها تجمع أفضل ما في العالمين: سهولة الكتابة وأمان الثباتية.
import produce from "immer";
const baseState = {
user: {
name: "Abu Omar",
social: {
twitter: "@abu_omar_dev"
}
},
posts: []
};
// مع Immer، الكود يصبح بسيطاً جداً
const nextState = produce(baseState, draftState => {
// هذا الكود يبدو وكأنه تعديل مباشر...
draftState.user.social.twitter = "@aboumar_codes";
// لكن Immer تضمن أن baseState الأصلي لن يتغير.
});
console.log(baseState.user.social.twitter); // "@abu_omar_dev" (الأصلي لم يلمس)
console.log(nextState.user.social.twitter); // "@aboumar_codes" (النسخة الجديدة)
نصائح من خبرة أبو عمر
- ابدأ بالتدريج: ليس عليك إعادة كتابة مشروعك بالكامل. ابدأ بتطبيق مبدأ الثباتية في الأجزاء الجديدة أو الحساسة من نظامك، مثل إدارة حالة الواجهة الأمامية (State Management).
- اجعلها قاعدة للفريق: الثباتية تكون أكثر فعالية عندما يلتزم بها الفريق بأكمله. اتفقوا عليها كقاعدة أساسية، واستخدموا أدوات مثل ESLint لفرض بعض هذه القواعد آلياً.
- لا تخف من الأداء إلا عند الحاجة: قد يقول قائل إن إنشاء كائنات جديدة في كل مرة أبطأ من تعديلها. هذا صحيح تقنياً، لكن هذا الفارق في الأداء لا يذكر في 99% من الحالات. الفائدة التي تجنيها من حيث سهولة الصيانة وتقليل الأخطاء تفوق هذا الاعتبار بكثير. لا تقم بالتحسين المسبق (Premature Optimization) إلا إذا أثبتت القياسات وجود مشكلة حقيقية.
- الثباتية هي قلب إدارة الحالة الحديثة: إذا كنت تعمل مع مكتبات مثل React, Redux, Vuex، فأنت بالفعل تتعامل مع مبدأ الثباتية. React، على سبيل المثال، تعتمد على إنشاء حالة جديدة لتحديد متى يجب إعادة رسم الواجهة. فهمك العميق للثباتية سيجعلك مبرمج واجهات أمامية أفضل بكثير.
الخلاصة: من الفوضى إلى النظام 🧘
العودة إلى قصتنا، بعد أن اكتشفنا المشكلة، أعدنا كتابة الأجزاء الحساسة من نظامنا باستخدام مبدأ الثباتية. اختفت الأخطاء الغامضة كالسحر. أصبح الكود أكثر وضوحاً، وأصبحنا ننام ليلاً ونحن واثقون أن بياناتنا لن تتغير فجأة. الاستثمار في تعلم وتطبيق الثباتية لم يكن مجرد حل لمشكلة، بل كان استثماراً في صحتنا العقلية كمبرمجين وفي جودة المنتج على المدى الطويل.
نصيحتي الأخيرة لك: لا تنظر إلى الثباتية على أنها مجرد “تقنية برمجية”، بل انظر إليها كفلسفة لبناء برمجيات قوية، متينة، ويمكن التنبؤ بسلوكها. ابدأ اليوم، ولو بخطوة صغيرة، ومستقبلك البرمجي سيشكرك. ويعطيكم العافية يا جماعة.