يا مية أهلاً وسهلا فيكم يا جماعة الخير، معكم أخوكم أبو عمر.
اسمحوا لي آخذكم معي في رحلة قصيرة لورا، قبل كم سنة. كنا غرقانين لشوشتنا في مشروع ضخم ومعقد، تطبيق فيه تفاصيل وبيانات بتتداخل ببعضها بشكل مش طبيعي. في يوم من الأيام، واحنا بنشتغل على جزئية “سلة المشتريات” في متجر إلكتروني، بلّشت المشاكل “تفقع” بوجهنا من كل حدب وصوب.
المستخدم يضيف منتج للسلة، يروح على صفحة الدفع، ولّا هو المنتج اختفى! مرة ثانية، يطبق كود خصم، النظام يحسب الخصم صح، لكن لما يرجع للسلة يلاقي السعر القديم. قضينا أيام وليالي واحنا بنطارد أشباح في الكود. كل ما نصلح شغلة، تخرب عشرة غيرها. كان الإحساس العام في الفريق إنه البيانات “بتتغير من تحت رجلينا”، زي اللي بيمشي على رمل متحرك. كل خطوة ممكن تغوص فيها وما تطلع.
في واحد من اجتماعاتنا اللي وصلت لطريق مسدود، وقفت وقلت للفريق: “يا جماعة، المشكلة مش في الكود اللي بنكتبه، المشكلة في الطريقة اللي بنتعامل فيها مع البيانات نفسها. احنا بنسمح لأي حدا يغيّر أي إشي في أي وقت، وهذا هو سبب البهدلة كلها”. ومن هداك اليوم، بلّشت رحلتنا مع مفهوم غيّر طريقة تفكيرنا تماماً: اللامُتَغَيِّرية (Immutability).
ما هي “المشكلة” بالضبط؟ القابلية للتغيير (Mutability)
قبل ما نحكي عن الحل، خلينا نفهم أصل المشكلة. في معظم لغات البرمجة، لما نعرّف متغير (خصوصاً الكائنات – Objects والمصفوفات – Arrays)، بنقدر نغيّر محتواه في أي وقت وفي أي مكان. هذا هو ما نسميه “القابلية للتغيير” أو الـ Mutability.
خلونا نشوف مثال بسيط في لغة JavaScript:
// نعرّف سلة مشتريات ككائن (Object)
let cart = {
items: ["خبز", "جبنة"],
total: 5.00
};
// نمرر السلة لدالة تطبق خصم
function applyDiscount(shoppingCart) {
// ⛔️ خطأ شائع: تغيير الكائن الأصلي مباشرة!
shoppingCart.total = shoppingCart.total * 0.90;
return shoppingCart;
}
// نطبق الخصم
applyDiscount(cart);
// الآن، قيمة الـ 'cart' الأصلية تغيرت بشكل دائم
console.log(cart); // { items: ["خبز", "جبنة"], total: 4.50 }
على الورق، هذا الكود يبدو بريئاً، أليس كذلك؟ لكن هنا يكمن الشيطان في التفاصيل. لقد قمنا بتغيير الكائن cart الأصلي من داخل دالة. هذا ما يسمى بـ “الأثر الجانبي” (Side Effect).
جحيم الآثار الجانبية (Side Effects)
تخيل أن هناك عشرة أجزاء أخرى من البرنامج تستخدم نفس المتغير cart. أحدها يعرض السعر الإجمالي، والآخر يحسب تكلفة الشحن، والثالث يحفظ السلة في قاعدة البيانات. عندما تقوم دالة applyDiscount بتغيير الكائن الأصلي “بصمت”، فإنها تسبب سلسلة من المشاكل غير المتوقعة في كل مكان آخر:
- صعوبة التتبع: عندما تجد خطأ في السعر، من أين تبدأ البحث؟ أي جزء من الكود هو الذي قام بهذا التغيير “الخاطئ”؟ تصبح عملية تصحيح الأخطاء (Debugging) مثل البحث عن إبرة في كومة قش.
- حالة غير متوقعة: يصبح من المستحيل التنبؤ بحالة التطبيق في أي لحظة. البيانات تتغير باستمرار من مصادر متعددة، مما يؤدي إلى سلوك فوضوي وغير متناسق.
- مشاكل التزامن (Concurrency): إذا كان تطبيقك يتعامل مع عمليات متعددة في نفس الوقت (مثل تطبيقات الويب الحديثة)، فإن السماح بتغيير البيانات المشتركة مباشرة هو وصفة لكارثة مضمونة.
باختصار، القابلية للتغيير تجعل كودك هشاً، صعب الفهم، ومليئاً بالمفاجآت غير السارة.
دخول “اللامتغيرية” (Immutability) إلى المشهد
اللامتغيرية هي مبدأ بسيط وقوي: بمجرد إنشاء قطعة من البيانات (كائن أو مصفوفة)، لا يمكن تغييرها أبداً.
“لحظة يا أبو عمر!”، قد تقول. “كيف بدنا نشتغل إذا ما بنقدر نغيّر إشي؟ كيف أضيف منتج للسلة أو أطبق خصم؟”
الجواب بسيط: بدلاً من تغيير البيانات القديمة، نقوم بإنشاء نسخة جديدة من البيانات مع التعديلات التي نريدها. الأصل يبقى كما هو، نقيّاً لم يمسه أحد.
اللامتغيرية في الممارسة
دعونا نعيد كتابة مثال سلة المشتريات باستخدام مبدأ اللامتغيرية. سنستخدم تقنيات حديثة في JavaScript مثل عامل الانتشار (Spread Operator `…`) لجعل العملية سهلة وقابلة للقراءة.
const originalCart = {
items: ["خبز", "جبنة"],
total: 5.00
};
// ✅ طريقة صحيحة: الدالة لا تغير الكائن الأصلي
function applyDiscountImmutable(shoppingCart) {
// ننشئ كائناً جديداً باستخدام محتويات القديم
// ثم نحدد القيمة الجديدة للـ total
const newCart = {
...shoppingCart, // انسخ كل خصائص shoppingCart القديم
total: shoppingCart.total * 0.90 // قم بتجاوز خاصية الـ total فقط
};
return newCart;
}
// نطبق الخصم ونستقبل السلة الجديدة في متغير جديد
const discountedCart = applyDiscountImmutable(originalCart);
// لنرَ النتائج
console.log("السلة الأصلية (لم تتغير):", originalCart);
// الناتج: { items: ["خبز", "جبنة"], total: 5.00 }
console.log("السلة الجديدة بعد الخصم:", discountedCart);
// الناتج: { items: ["خبز", "جبنة"], total: 4.50 }
لاحظ الفرق الجوهري هنا. الدالة applyDiscountImmutable أصبحت “دالة نقية” (Pure Function). لا تسبب أي آثار جانبية. يمكنك استدعاؤها مليون مرة بنفس المدخلات، وستحصل دائماً على نفس المخرجات، دون أن تؤثر على أي شيء خارجها. الكائن originalCart بقي سليماً كما ولدته أمه!
وماذا عن إضافة عنصر لمصفوفة؟
نفس المبدأ ينطبق على المصفوفات. بدلاً من استخدام .push() الذي يغير المصفوفة الأصلية، نستخدم طرقاً أخرى.
const originalItems = ["بندورة", "خيار"];
// لإضافة عنصر جديد
const newItems = [...originalItems, "فلفل"]; // نستخدم عامل الانتشار
console.log(originalItems); // ["بندورة", "خيار"] (الأصل لم يتغير)
console.log(newItems); // ["بندورة", "خيار", "فلفل"] (النسخة الجديدة)
فوائد تبني اللامتغيرية
عندما بدأنا في تطبيق هذا المبدأ في مشروعنا، كانت النتائج أشبه بالسحر:
- شيفرة قابلة للتنبؤ (Predictable Code): اختفت الأخطاء الغامضة. أصبحنا نعرف بالضبط أين وكيف تتغير البيانات، لأن كل تغيير ينتج عنه نسخة جديدة وواضحة.
- تصحيح أخطاء أسهل بـ 10 أضعاف: أصبح بإمكاننا مقارنة الحالة “قبل” و “بعد” التغيير بسهولة. أدوات المطورين مثل Redux DevTools، التي تعتمد بشكل كبير على هذا المبدأ، تسمح لنا بـ “السفر عبر الزمن” ورؤية كل تغيير حدث في حالة التطبيق خطوة بخطوة.
- تحسين الأداء (نعم، حقاً!): قد تظن أن إنشاء نسخ جديدة باستمرار أمر مكلف. لكن في الواقع، العديد من أطر العمل الحديثة مثل React تستغل اللامتغيرية لتحسين الأداء. يمكنها مقارنة مرجع الكائن القديم بالجديد بسرعة البرق (Shallow Comparison). إذا كان المرجع مختلفاً، فهذا يعني أن البيانات تغيرت ويجب تحديث واجهة المستخدم. إذا كان المرجع هو نفسه، فلا داعي لفعل أي شيء.
- إدارة حالة أبسط (Simpler State Management): أصبحت إدارة الحالة العامة للتطبيق (Global State) أكثر تنظيماً وأماناً. لم نعد نخاف من أن يقوم جزء من التطبيق بإفساد البيانات على جزء آخر.
نصائح أبو عمر العملية
الانتقال إلى هذا النمط من التفكير يحتاج بعض الممارسة. إليك بعض النصائح من خبرتي الشخصية:
- ابدأ بالتدريج: لست مضطراً لإعادة كتابة كل مشروعك. ابدأ بتطبيق اللامتغيرية في الأجزاء الأكثر حساسية، مثل إدارة الحالة المركزية (State Management Store).
- استخدم الأدوات المساعدة: لا تخترع العجلة من جديد. إذا كان التعامل مع الكائنات المتشعبة صعباً، استخدم مكتبات مثل Immer.js. هذه المكتبة تسمح لك بكتابة كود يبدو وكأنه يغير البيانات مباشرة، لكنها في الكواليس تقوم بإنشاء نسخ جديدة بشكل آمن وفعال.
- اجعلها عادة: كلما كتبت دالة تأخذ كائناً أو مصفوفة، اسأل نفسك: “هل يجب أن أغير هذا المدخل، أم يجب أن أعيد نسخة جديدة؟”. في 99% من الحالات، الخيار الثاني هو الأصح.
- فكر بطريقة “وظيفية”: اللامتغيرية هي حجر الزاوية في البرمجة الوظيفية (Functional Programming). تعلم المزيد عن الدوال النقية (Pure Functions) وتجنب الآثار الجانبية سيجعلك مبرمجاً أفضل بشكل عام.
الخلاصة: اجعل كودك صخرة، لا رملاً متحركاً! 🧗♂️
العودة إلى قصتنا، بعد تبني اللامتغيرية، تحول مشروعنا من كابوس فوضوي إلى نظام منظم يمكن التنبؤ به. استغرق الأمر بعض الوقت لتغيير عاداتنا، لكن المردود كان هائلاً من حيث استقرار النظام، سرعة التطوير، وراحة البال التي لا تقدر بثمن.
اللامتغيرية ليست مجرد تقنية أو مصطلحاً فاخراً، إنها فلسفة في كتابة الكود. إنها التزام ببناء أنظمة قوية وواضحة يمكن صيانتها وتطويرها بثقة. إنها الفرق بين بناء بيتك على أساس صخري متين، أو على رمال متحركة تتغير من تحت قدميك.
أتمنى أن تكون هذه المشاركة قد أنارت لكم الطريق. والله ولي التوفيق.