التركيب مقابل الوراثة
التركيب مقابل الوراثة
تعرّفت الآن على كيفية بناء تسلسلات كلاسات باستخدام extends. لكن الوراثة ليست الأداة الصحيحة دائمًا. في هذا الدرس ستتعلّم التركيب — وهو أسلوب مختلف لإعادة استخدام الكود — وستفهم متى يكون كل أسلوب مناسبًا.
علاقة "هو نوع من" مقابل "يحتوي على"
السؤال الأكثر فائدة عند الاختيار بين الوراثة والتركيب هو:
- هل هي علاقة "هو نوع من"؟ استخدم الوراثة.
Dogهو نوع منAnimal.SavingsAccountهو نوع منBankAccount. - هل هي علاقة "يحتوي على"؟ استخدم التركيب.
Carيحتوي علىEngine.Personيحتوي علىAddress.
حين تكون العلاقة هرمية حقيقية ويصحّ الاستبدال — أي في أي مكان يُتوقّع BankAccount يصلح SavingsAccount — فالوراثة مناسبة. أما حين تمتلك إحدى الكلاسات كلاسًا آخر أو تستخدمه فحسب، فالتركيب هو الخيار الأفضل.
مثال سريع: الطريقة الخاطئة
لنفترض أنّك نمذجت سيارة بالوراثة من Engine:
يُترجَم هذا الكود، لكنّه مفاهيميًا خاطئ. Car ليست نوعًا من Engine؛ بل تمتلك محرّكًا. والأسوأ أن كل دالة عامة في Engine أصبحت مكشوفة على Car — يمكن للمُستدعين استدعاء car.start() ظنًا أنّهم يُشغّلون السيارة، لكنّهم في الواقع يصلون إلى تفصيلة تنفيذ داخلية.
الطريقة الصحيحة: التركيب
بالتركيب، تحتفظ Car بمرجع لكائن من نوع Engine كحقل:
تُفوِّض Car سلوك الإقلاع إلى Engine عبر استدعاء دالة، لا عبر الوراثة. تبقى تفاصيل Engine الداخلية مخفية تمامًا عن مستدعي Car.
مشكلة الكلاس الأساسية الهشّة
من أهم الأسباب للتفضيل التركيب هي مشكلة الكلاس الأساسية الهشّة: حين تُعدّل كلاسًا أصليًا، قد تكسر جميع الكلاسات المشتقّة بصمت — حتى تلك التي لم تلمسها قط.
إليك مثالًا كلاسيكيًا. تخيّل قائمة مخصّصة تحصي عدد العناصر التي أُضيفت على الإطلاق:
لماذا 6 بدلًا من 3؟ لأن ArrayList.addAll تستدعي add داخليًا لكل عنصر. لذلك تزيد CountingList.addAll العدّاد بـ 3، ثم تستدعي super.addAll بدورها CountingList.add ثلاث مرات إضافية، فيزيد العدّاد مجدّدًا. انكسرت الكلاس المشتقّة لأنها اعتمدت على تفصيلة تنفيذية داخلية للكلاس الأصلية — وهي تفصيلة ليست جزءًا من العقد العام وقد تتغيّر في أي إصدار قادم من JDK.
ArrayList أو HashMap أو Stack يُفضي دائمًا تقريبًا إلى هذا النوع من الأخطاء الخفية.
الإصلاح بالتركيب
لفّ القائمة بدلًا من التوسيع منها:
الآن تمتلك CountingList منطق العدّ بالكامل. لا يمكن للتغييرات في تنفيذ ArrayList أن تؤثّر عليها.
تفضيل التركيب: المبدأ التوجيهي
عبارة "افضل التركيب على الوراثة" مأخوذة من كتاب Design Patterns الكلاسيكي ("عصابة الأربعة"). لا تعني أبدًا استخدام الوراثة — بل تعني استخدامها فقط حين تجتاز علاقة "هو نوع من" الاختبار باقتناع وصُمِّمت الكلاس الأصلية للتمديد. في كل الحالات الأخرى، استخدم التركيب.
- التركيب أكثر مرونة. يمكنك استبدال الكائن المُركَّب في وقت التشغيل (مثل حقن
Engineمختلف)، وهو أساس أنماط تصميم عديدة كـ Strategy وDecorator. - التركيب تغليف أحكم. لا تتسرّب التفاصيل الداخلية للكلاس المساعدة إلى واجهة الكلاس الخارجية.
- الوراثة لا تزال صحيحة أحيانًا. نمذجة تسلسل أنواع حقيقي —
Shape/Circle/Rectangle— هو بالضبط ما وُجدت الوراثة من أجله. المفتاح هو الصدق في تحديد طبيعة العلاقة.
الخلاصة
الوراثة تنمذج علاقات "هو نوع من"، والتركيب ينمذج علاقات "يحتوي على". تُظهر مشكلة الكلاس الأساسية الهشّة أن توسيع الكلاسات الملموسة لإعادة استخدام كودها قد يكسر بصمت عند تغيير الكلاس الأصلية. التركيب — الاحتفاظ بمرجع لكائن مساعد والتفويض إليه — يتجنّب هذا الترابط الضيّق، ويمنحك واجهة برمجية أنظف، ويجعل كودك أسهل في الاستبدال والاختبار. في الدرس الختامي من هذه السلسلة ستطبّق كل ما تعلّمته ببناء تسلسل كامل لأشكال هندسية.