مقدمة: الفخ الذي وقع فيه الكثيرون
\nتخيل موقفًا: أنت تكتب تطبيقًا لمكتبة إلكترونية. تحتاج إلى صنف خاص Library يخزن الكتب، ويستطيع فرزها والبحث فيها وعرض الإحصائيات. أول ما يخطر ببالك هو الوراثة من List<Book>. فهو يمتلك كل شيء بالفعل: Add و Remove والفهرسة والبحث. منطقي؟ يبدو ذلك.
لكن مطوري C# ذوي الخبرة سيقولون لك: «لا ترث أبدًا من List<T>». هذا السؤال على Stack Overflow حصد آلاف المشاهدات، وليس بدون سبب. وراء البساطة الظاهرية تكمن مشاكل خطيرة في البنية والأداء، والأهم من ذلك، انتهاك مبادئ التصميم الشيئي.
\nفي هذه المقالة، سننظر تحت غطاء List<T>، ونفهم لماذا الوراثة منه هي نمط معاكس، وماذا نستخدم بدلاً منه. هل أنت مستعد؟ هيا بنا!
ما المشكلة في List<T> من وجهة نظر التصميم؟
\nلنبدأ بالأساسي: List<T> لم يُصمم كقاعدة للتسلسل الهرمي، بل كـ تنفيذ محدد وعالي الأداء لمصفوفة ديناميكية. واجهته العامة هي مجموعة من الطرق للتعامل مع المجموعة، وليست عقدًا للتوسيع.
ألقِ نظرة على تعريفه في المصادر الأصلية لـ .NET (مبسّط):
\npublic class List<T> : IList<T>, ICollection<T>, IEnumerable<T>, IList, ICollection, IEnumerable\n{\n private T[] _items;\n private int _size;\n \n public void Add(T item) { /* ... */ }\n public bool Remove(T item) { /* ... */ }\n // ... عشرات الطرق الأخرى\n}\nلاحظ: List<T> غير مُعلّم كـ abstract. لا يوفر طرقًا افتراضية لإعادة التعريف (باستثناء القليل منها، سنذكرها لاحقًا). هذا يعني أنه لا يمكنك تغيير سلوكه بأمان. إذا أعدت تعريف Add، فأنت تخاطر بتعطيل المنطق الداخلي الذي يعتمد على هذه الطريقة.
المشكلة رقم 1: انتهاك مبدأ استبدال ليسكوف (LSP)
\nينص مبدأ استبدال ليسكوف على: «يجب على الصنف الابن أن يُكمّل سلوك الأب، لا أن يستبدله». عندما ترث من List<T>، فأنت تعد بأن صنفك يمكن استخدامه في أي مكان يُتوقع فيه List<T>. لكن هذا نادرًا ما يتحقق.
مثال: تنشئ صنفًا ReadOnlyLibrary : List<Book> وتُعيد تعريف Add ليرمي استثناءً:
public class ReadOnlyLibrary : List<Book>\n{\n public new void Add(Book book)\n {\n throw new InvalidOperationException("المجموعة للقراءة فقط!");\n }\n}\n\n// في مكان ما في الكود:\nList<Book> list = new ReadOnlyLibrary();\nlist.Add(new Book()); // أوه! لن يتم رمي أي استثناء!\n// لأن new لا يُعيد التعريف، بل يُخفي الطريقة.\n// سيتم استدعاء List.Add، وليس طريقتنا.\nحتى لو استخدمت override (و List.Add ليس افتراضيًا)، لا يمكنك منع الإضافة — الواجهة IList لا تسمح بذلك. أنت ببساطة تُخلّ بالعقد.
المشكلة رقم 2: انتهاك التغليف
\nبوراثتك من List<T>، فإنك تُظهر واجهته العامة بالكامل للخارج. سيحصل صنفك Library على طرق Sort و BinarySearch و Reverse و Insert إلخ. ولكن ماذا لو أردت أن تكون الكتب مرتبة دائمًا حسب المؤلف؟ سيتمكن مستخدم صنفك من استدعاء library.Reverse() وتدمير الثبات.
لن تتمكن من منع ذلك دون إعادة كتابة جميع الطرق — وهناك العشرات منها. هذا انتهاك للتغليف: تُظهر للخارج تنفيذًا داخليًا لا يمكنك التحكم به.
\n\nننظر تحت الغطاء: كيف يعمل List<T> داخليًا
\nلفهم