من أكثر أسئلة مقابلات البرمجة كائنية التوجّه تكرارًا: متى تستخدم الوراثة (Inheritance)، ومتى التكوين (Composition)؟ والقاعدة الشائعة "فضّل التكوين على الوراثة" (Favor Composition Over Inheritance) تسمعها كثيرًا بدون شرح حقيقي لسببها. هذا المقال يشرحها بمثال واحد فقط.
الفكرة في سطرين
- الوراثة (is-a): فئة فرعية هي نوع من الفئة الأب، وترث كل سلوكها تلقائيًا. علاقة ثابتة تُحدَّد وقت كتابة الكود.
- التكوين (has-a): الكائن يملك أو يستخدم كائنات سلوك أخرى بدل أن يرثها، ويقدر يبدّلها وقت التشغيل.
مشكلة الوراثة العميقة
تخيل نظام مركبات: سيارة عادية، سيارة كهربائية، وسيارة برمائية تقدر تسبح كمان.
// البداية بسيطة
class Vehicle {
drive() { console.log("أقود على الطريق"); }
}
class ElectricVehicle extends Vehicle {
charge() { console.log("أشحن البطارية"); }
}
// ثم تحتاج سيارة تقدر تطفو... أين تضعها بالشجرة؟
class AmphibiousVehicle extends ElectricVehicle {
swim() { console.log("أسبح في الماء"); }
}
يبدو منطقيًا، لكن ماذا لو احتجت سيارة برمائية غير كهربائية (بمحرك
بنزين)؟ لا يوجد مكان مناسب لها بالشجرة الحالية—إما تكرر كود swim()
في فئة منفصلة، أو تعيد هيكلة الشجرة كاملة. المشكلة الجذرية: الوراثة
تفترض أن كل قدرة (charge, swim) مرتبطة بمستوى واحد ثابت في تسلسل
هرمي، بينما القدرات الحقيقية تُخلط وتُطابق بحرية أكبر من ذلك.
الحل — تكوين قدرات مستقلة
بدل أن ترث القدرات، اجعل كل قدرة كائنًا مستقلًا يُحقَن في المركبة:
// كل قدرة كائن مستقل، لا علاقة وراثة بينها
const electricEngine = {
charge: () => console.log("أشحن البطارية"),
};
const swimCapability = {
swim: () => console.log("أسبح في الماء"),
};
class Vehicle {
constructor(...capabilities) {
Object.assign(this, ...capabilities); // تركيب القدرات بالكائن
}
drive() {
console.log("أقود على الطريق");
}
}
// سيارة كهربائية عادية
const tesla = new Vehicle(electricEngine);
tesla.drive();
tesla.charge();
// سيارة برمائية كهربائية — تركيب حر لأي قدرات تحتاجها
const amphibiousTesla = new Vehicle(electricEngine, swimCapability);
amphibiousTesla.swim();
// سيارة برمائية بمحرك بنزين — بدون charge() نهائيًا، بلا أي تكرار كود
const amphibiousGas = new Vehicle(swimCapability);
amphibiousGas.swim();
كل قدرة قطعة مستقلة تُضاف فقط لمن يحتاجها فعلاً—بدون شجرة وراثة جامدة تفرض عليك تركيبات غير موجودة أو تكرار كود بين فروع متباعدة.
متى تستخدم كل واحد؟
| السؤال | الوراثة (Inheritance) | التكوين (Composition) |
|---|---|---|
| العلاقة | "هو نوع من" (is-a) ثابتة | "يملك" أو "يقدر يفعل" (has-a) |
| المرونة | تسلسل هرمي محدَّد وقت الكتابة | تركيب/تبديل وقت التشغيل |
| تعدد القدرات المستقلة | يحتاج تعدد وراثة (غير مدعوم بمعظم اللغات) أو شجرة معقدة | يجمع أي عدد من القدرات بلا قيود |
| مثال مناسب | Square extends Shape (مربع دائمًا شكل) | مركبة تملك قدرة سباحة أو شحن حسب الحاجة |
القاعدة العملية: ابدأ بالتكوين افتراضيًا، ولا تلجأ للوراثة إلا لعلاقة "هو نوع من" واضحة لن تحتاج كسرها لاحقًا.
علاقتها بأنماط التصميم
هذا المبدأ ليس نظريًا فقط—هو الأساس وراء عدة أنماط تصميم شهيرة: نمط Strategy يحقن خوارزمية كسلوك منفصل بدل فئة فرعية لكل خوارزمية، ونمط Decorator يضيف سلوكًا بتغليف كائن بدل وراثته. لو تبغى تتعمق في هذه الأنماط بأمثلة كاملة، مسار أنماط التصميم بالعربي يغطّيها جميعًا خطوة بخطوة.
الخلاصة
الوراثة قوية لكنها جامدة: علاقة واحدة، تُحدَّد مبكرًا، يصعب تعديلها. التكوين أكثر مرونة: تجمع أي عدد من السلوكيات المستقلة وتبدّلها براحتك. اسأل نفسك دائمًا: هل هذه فعلاً علاقة "هو نوع من"، أم مجرد "يقدر يفعل"؟ الجواب يحدد الأداة الصحيحة.