الـ Inheritance، وراثة الكلاسات
السلام عليكم ورحمة الله وبركاته
الفهرس
- المقدمة
- أين المشكلة ؟
- مناقشة لحل المشكلة
- الآن كيف نقوم بعملية الوراثة تلك ؟
- Access Modifiers
- إضافة خواص جديدة للكلاس غير التي حصل عليها من الوراثة
- مفهوم الـ Overriding
- ما هو الـ super ؟
- ماذا يحدث للـ Constructor في الوراثة
- اختبار بسيط على ما سبق
- الأشكال المختلفة للـ Inheritance
- Single Inheritance
- Multilevel Inheritance
- Hierarchical Inheritance
- Multiple Inheritance
- توضيح عملي
- مناقشة مشاكل الـ Multiple Inheritance بكود تخيلي
- التعامل مع أكثر من Constructor في آن واحد
- مشكلة شكل الألماسة | Diamond Problem
- كيف تعاملت اللغات القديمة مع مشاكل الـ Multiple Inheritance
- هل الـ Multiple Inheritance له فائدة من الأساس ؟
- محاولة اللغات الحديثة لاستبدال مفهوم الـ Multiple Inheritance بالـ Interface
- Hybrid Inheritance
المقدمة
نبدأ المقالة الثالثة في سلسلتنا عن الـ OOP
سنتحدث عن أحد اهم مفاهيم الـ OOP
وهو الـ Inheritance
الوراثة
class Student {
private name: string;
private age: number;
constructor(name: string, age: number) {
this.setName(name);
this.setAge(age);
}
private setAge(age: number) {
// imagine there is a nice validation here
this.age = age;
}
private setName(name: string) {
// imagine there is a nice validation here
this.name = name;
}
public printInfo() {
console.log(`Name is: ${this.name}, his age: ${this.age}.`);
}
}
let s1 = new Student('Ahmed', 22);
s1.printInfo(); // OUTPUT: Name is: Ahmed, his age: 22.
حسنًا لدينا هنا كلاس بسيط يمثل بيانات الطلاب لدينا متغيرات مثل الاسم والعمر
ودوال setName
و setAge
لإسناد قيمهم، وهذه الدوال كما تعلمنا في مقالة الـ Encapsulation
تفيدنا لأنها تعمل كوسيط يمكننا وضع شروط وقيود قبل اسناد القيم
على أي حال أنشأنا object
اسمه s1
واستعملنا الـ constructor
لاعطاء قيم للاسم والعمر، ثم استخدما دالة printInfo
لطباعة بيانات الطالب
أين المشكلة ؟
كل شيء يبدو جيدًا، لكن أين المشكلة ؟
حسنًا هنا لدينا كلاس واحد فقط، دعونا نُنشيء كلاس آخر يمثل المعلمين
class Teacher {
private name: string;
private age: number;
constructor(name: string, age: number) {
this.setName(name);
this.setAge(age);
}
private setAge(age: number) {
// imagine there is a nice validation here
this.age = age;
}
private setName(name: string) {
// imagine there is a nice validation here
this.name = name;
}
public printInfo() {
console.log(`Name is: ${this.name}, his age: ${this.age}.`);
}
}
let t1 = new Teacher('Ali', 43);
t1.printInfo(); // OUTPUT: Name is: Ali, his age: 43.
ماذا لاحظت ؟
لاحظت التكرار في كل شيء، كلا الطالب والمعلم لديهما خواص متشابه
كليهما لهما نفس المتغيرات name
و age
ولهم نفس الدوال setName
و setAge
وتلك الدوال تقوم بفعل نفس الشيء
هل المشكلة في التكرار فقط ؟
لا، دالة setName
على سبيل المثال قد نضع فيها شروط وقيود قبل اسناد قيمة الاسم
إن أردنا تعديل شرط أو نزود شرط هل نعدلها في كلا المكانيين
وايضًا لنفترض أننا نملك كلاس للمدراء وكلاس للموظفين وكلاس لـ .. وكلاس لـ ..
وجميعهم سيملكون خواص ودوال مشتركة مثل setName
هل نعدل الدالة في جميع هذه الكلاسات إن اردنا تغير شرط ما ؟
هذه مجرد جانب من جوانب المشكلة، إن نظرت للمشكلة من على المدى البعيد
فستجد أن في المشاريع الكبيرة ستكون المشكلة أكبر من مجرد تكرار
- فهم الكود وتعديله سيكون صعبًا ومرهقًا
- ظهور أخطاء كثيرة بسبب تعديل نفس الشيء في أكثر من مكان
- صعوبة تتبع الكود ومحاولة إجاد الخطأ
- كبر حجم الكلاسات وكثرتها بسبب التكرار
مناقشة لحل المشكلة
بعد ما استوعبت بعض جوانب المشكلة
كيف سيكون الحل برأيك ؟ الفكرة ببساطة أننا نريد أن نضع الأشياء المشتركة في مكان واحد
وعندما ننشيء شيء جديد نجعله يأخذ هذه الأشياء والخواص المشتركة
لنرى الموضوع بشكل عملي قليلًا، ما الأشياء المشتركة بين الطالب والمعلم ؟
كليهما بني أدمين ! ، لذا ما رايكم بعمل كلاس يمثل الإنسان بشكل عام
فيمكننا أن ننشيء كلاس يدعى Person
على سبيل المثال
ونضع فيه الأشياء المشتركة التي ستكون في الطالب والمدرس والمدير والموظف وأي إنسان بشكل عام
class Person {
private name: string;
private age: number;
constructor(name: string, age: number) {
this.setName(name);
this.setAge(age);
}
private setAge(age: number) {
// imagine there is a nice validation here
this.age = age;
}
private setName(name: string) {
// imagine there is a nice validation here
this.name = name;
}
public printInfo() {
console.log(`Name is: ${this.name}, his age: ${this.age}.`);
}
}
الآن عندما نريد إنشاء كلاس يمثل الطالب نريده أن يأخذ نسخة من المتغيرات والدوال التي عرفناها في كلاس الـ Person
عملية الأخذ هنا هي مفهوم الـ Inheritance
الوراثة
بمعنى أننا سننشيء كلاس يدعى Student
ونجعله يرث كل الخواص والدوال التي بداخل كلاس الـ Person
لكي نمنع التكرار والمشكلات التي ستترتب عليها كما ذكرنا سابقًا
يمكننا أن نعبر عن العلاقة بهذا الشكل
+---------------+
| Person |
+---------------+
٨
|
|
+---------------+
| Student |
+---------------+
Student inherit from Person
الآن كيف نقوم بعملية الوراثة تلك ؟
الأمر بسيط جدًا، فقط نستخدم extends
ملحوظة
: بعض اللغات مثلC++
تستخدم علامة:
بدلًا منextends
class Student extends Person {
// Student now contains methods & attributes from Person
}
let s1 = new Student('Ahmed', 22);
s1.printInfo(); // OUTPUT: Name is: Ahmed, his age: 22.
هنا يمكننا أن نقول ان كلاس Student
أخد جميع الخواص من الـ Person
, كأنه أصبح نسخة منه
حتى الـ constructor
أخذ نفس شكله بالمتغيرات التي يستقبلها
واستعمل دالة printInfo
التي في الأصل كانت في كلاس الـ Person
ملحوظة
: يوجد بعض المصطلحات في الأسماء تصف العلاقة بين كلاسيين في حالة الوراثة
الكلاسPerson
يسمى بالكلاس الأبParent Class
أوBase Class
أوSuper Class
الكلاسStudent
يسمى بالكلاس الابنChild Class
أوDerived Class
أوSub Class
Access Modifiers
سنشرح ماذا يحدث للـ Access Modifiers
في عندما يحدث لها Inheritance
هذا جدول يلخص العلاقة بين الـ Access Modifiers
في الـ Parent
بالنسبة للـ Child
Parent Access Modifiers | بالنسبة لـ Child Class |
بالنسبة لـ Child Object |
---|---|---|
Public | ✔️ يستطيع الوصول | ✔️ يستطيع الوصول |
Protected | ✔️ يستطيع الوصول | ❌ لا يستطيع الوصول |
Private | ❌ لا يستطيع الوصول | ❌ لا يستطيع الوصول |
ثم سنتحدث تاليا على كل حالة على حدى يمكنك أن تعود للجدول للمراجعة
public
class Parent {
public name: string | undefined;
}
class Child extends Parent {
// child class CAN access `name`
public getName() {
return this.name;
}
}
// Parent
let parent = new Parent();
parent.name = 'Ahmed';
// Child
let child = new Child();
child.name = 'Ali'; // child object CAN access `name`
هنا أي شيء public
في الـ Parent
يستطيع كلاس الـ Child
الوصول له
وأيضًا يستطيع الـ object
من كلاس الـ Child
الوصول له
private
class Parent {
private name: string | undefined;
}
class Child extends Parent {
// child class CAN NOT access `name`
public getName() {
return this.name; // ERROR!!, Property 'name' is private and only accessible within class 'Parent'.
}
}
// Parent
let parent = new Parent();
parent.name = 'Ahmed'; // ERROR!!, Property 'name' is private and only accessible within class 'Parent'.
// Child
let child = new Child();
// child object CAN NOT access `name`
child.name = 'Ali'; // ERROR!!, Property 'name' is private and only accessible within class 'Parent'.
هنا أي شيء private
في الـ Parent
لا يستطيع كلاس الـ Child
الوصول له
وأيضًا لا يستطيع الـ object
من كلاس الـ Child
الوصول له
protected
class Parent {
protected name: string | undefined;
}
class Child extends Parent {
// ONLY child class CAN access `name`
public getName() {
return this.name;
}
}
// Parent
let parent = new Parent();
parent.name = 'Ahmed'; // ERROR!!, Property 'name' is protected and only accessible within class 'Parent' and its subclasses.
// Child
let child = new Child();
// child object CAN NOT access `name`
child.name = 'Ali'; // ERROR!!, Property 'name' is protected and only accessible within class 'Parent' and its subclasses.
هنا أي شيء protected
في الـ Parent
فقط يستطيع كلاس الـ Child
الوصول له
لكن لا يستطيع الـ object
من كلاس الـ Child
الوصول له
حتى الـ object
من الـ Parent
لا يستطيع الوصول له مثله مثل الـ private
الفرق الوحيد الكلاس التي سترث من الـ Parent
هى المسموح لها بالوصول للـ protected
إضافة خواص جديدة للكلاس غير التي حصل عليها من الوراثة
حسنًا لنعود لموضوعنا الأساسي
أولًا أريدك ألا تنسى كلاس الـ Person
class Person {
// NOTE: We changed the name and age from private to protected
// To allow the Student class to access them
protected name: string;
protected age: number;
constructor(name: string, age: number) {
this.setName(name);
this.setAge(age);
}
private setAge(age: number) {
// imagine there is a nice validation here
this.age = age;
}
private setName(name: string) {
// imagine there is a nice validation here
this.name = name;
}
public printInfo() {
console.log(`Name is: ${this.name}, his age: ${this.age}.`);
}
}
ملحوظة
: لقد غيرنا الـname
و الـage
منprivate
إلىprotected
لكي نسمح لكلاس الـStudent
الوصول لهم بسهولة
هنا نستطيع أن نضع دوال ومتغيرات في كلاس الـ Student
زائدة عن كلاس الـ Person
بمعنى أن الـ Student
يستطيع أن يملك خواص جديدة غير التي حصل عليها من الـ Person
وهذا بديهي
class Student extends Person {
private level: number | undefined;
public setLevel(level: number) {
// some validation
if (level < 0 && level > 5) throw new Error('Invalid level');
// set the level
this.level = level;
}
public getLevel() {
return this.level;
}
}
let s1 = new Student('Ahmed', 22);
s1.setLevel(2);
let level = s1.getLevel();
console.log(level); // OUTPUT : 2
زودنا في كلاس الـ Student
متغير level
يمثل مستوى أو الفرقة التي ينتمي لها الطالب
وأنشأنا setLevel
و getLevel
وستلاحظ أننا عملنا شرط أن المستوى يجيب ان يكون بين الـ 1
و 5
للتذكيركم ببعض فوائد أننا نقوم بعمل دالة setter
لاحظ أننا قمنا بإضافة متغيرات ودوال لم تكن في كلاس الـ Person
الأساسي
وهذا ما يميز الـ Inheritance
أنك تضع الأشياء المشتركة في كلاس ما وتجعل باقي الكلاسات يرثوه ويضيفوا المتغيرات والدوال التي يريدونها
كلاس الـ
Teacher
يستطيع أن يرث كلاس الـPerson
ويضيف الأشياء التي يريدها
ونفس الأمر مع الـManager
يستطيع أن يرث كلاس ونفس الأمر مع الـEmployee
يستطيع أن يرث كلاس و ... إلخ
مفهوم الـ Overriding
الـ overriding
هو عندما يكون هناك دالة موجودة في الـ Parent
أي الكلاس الأساسي وتريد أن تعدلها في الكلاس الذي سيرثه
بمعنى أننا لدينا دالة ما في كلاس الـ Person
ونريد أن نعدلها في كلاس الـ Student
لنرى مثالًا للمشكلة التي جعلتنا نلجأ للـ overriding
class Student extends Person {
private level: number | undefined;
public setLevel(level: number) {
// imagine there is a nice validation here
this.level = level;
}
}
let s1 = new Student('Ahmed', 22);
s1.setLevel(2);
s1.printInfo(); // OUTPUT: Name is: Ahmed, his age: 22.
تذكر أننا اضفنا متغير جديد في الـ Student
وهو الـ level
ونعرف أن هناك دالة تدعى printInfo
ورثناها من كلاس الـ Person
وكانت تطبع المعلومات التي يحتويها الكلاس
s1.printInfo(); // OUTPUT: Name is: Ahmed, his age: 22.
لكن ستلاحظ أنها تطبع لنا الإسم والعمر فقط ولا تطبع لنا أي شيء عن المتغير التي اضفناه
وهذا طبيعي لان الدالة في كلاس الـ Person
كانت تطبع البيانات الاساسية التي بنيت عليها
لكن الآن نحتاج لتعديلها هنا ظهر مفهوم الـ overriding
وهو امكانية التعديل على الدوال التي ورثتها الكلاسات
تطبيق عملي على الـ Overriding
حسنًا، يبقى السؤال كيف نعدل دالة موجودة في كلاس الـ Person
من داخل كلاس الـ Student
؟
الأمر في غاية البساطة
كل ما في الأمر أنك تعيد كتابة الدالة داخل كلاس الـ Student
بنفس الإسم
class Student extends Person {
private level: number | undefined;
public setLevel(level: number) {
// imagine there is a nice validation here
this.level = level;
}
public printInfo() {
console.log(
`Name is: ${this.name}, his age: ${this.age}, his level: ${this.level}.`
);
}
}
let s1 = new Student('Ahmed', 22);
s1.setLevel(2);
s1.printInfo(); // OUTPUT: Name is: Ahmed, his age: 22, his level: 2.
ما فعلناه مع الـ printInfo
في كلاس الـ Student
يسمى Overriding
وهو أننا اعدنا تعريف الدالة في كلاس الـ Student
عرفنا دالة printInfo
داخل كلاس الـ Student
بنفس اسمها في كلاس الـ Person
فهنا لدينا دالتان بنفس الاسم
عندما يستدعي object
من الـ Student
دالة الـ printInfo
فسيتم استدعاء أقرب دالة بالنسبة للـ object
وأقرب دالة له هي الموجودة في كلاس الـ Student
بالطبع
أريدك أن تتذكر هذه الجملة جيدًا (أقرب دالة بالنسبة للـ object
) لأننا سنحتاجُها فيما بعد
ملحوظة: قد يتم تغير الـ
access modifier
في عملية الـoverriding
يمكن لكلاس الـStudent
جعل دالةprintInfo
تكونprivate
بدلًا منpublic
بالتالي الـobject
لن يستطيع استدعائها
class Student extends Person {
private level: number | undefined;
public setLevel(level: number) {
// imagine there is a nice validation here
this.level = level;
}
// override the method and change its access modifier to private
private printInfo() {
console.log(
`Name is: ${this.name}, his age: ${this.age}, his level: ${this.level}.`
);
}
}
let s1 = new Student('Ahmed', 22);
s1.setLevel(2);
s1.printInfo(); // Error!!, Property 'printInfo' is private and only accessible within class 'Student'
لاحظ أننا جعلنا دالة printInfo
تكون private
بدلًا من public
عدما قمنا بعمل overriding
لها
بالتالي الـ object
لم يستطع استدعائها
ما هو الـ super ؟
لدينا شيء مهم جدًا يظهر عندما تحدث عملية الوراثة وهو ظهور شيء يدعى super
وهو ببساطة يكون reference
للكلاس الـ Parent
الذي ورثنا منه
تتذكر الـ this
؟ الـ this
كانت reference
تشير للكلاس الحالي
أما الـ super
هو نفس الفكرة لكنه reference
يشير للكلاس الذي ورثنا منه وهو الـ Parent
تذكر أن من أسماء الكلاس الـ
Parent
كانSuper class
فالاسم جاء من هنا
تأمل في الكود التالي
class Parent {
public hello() {
console.log('Hello from Parent class!!');
}
}
class Child extends Parent {
public hello() {
console.log('Hello from Child class!!');
}
}
// Child
let child = new Child();
child.hello(); // OUTPUT: Hello from Child class!!
كود بسيط يطبق مفهوم الـ overriding
بأننا عدلنا دالة الـ hello
في الـ Child
وأنشأنا object
واستدعينا الدالة وطبعت لنا Hello from Child class
الآن ركز معي جيدًا
class Child extends Parent {
public hello() {
super.hello(); // call hello from from super class
console.log('Hello from Child class!!');
}
}
// Child
let child = new Child();
child.hello();
ماذا سيكون الناتج ؟، ستلاحظ أننا في دالة hello
في كلاس الـ Child
استدعينا دالة hello
من الـ super
وقلنا أن الـ super
يكون reference
يتواجد في كلاس الـ Child
يشير لكلاس الـ Parent
فهكذا نحن هنا super.hello();
استدعينا دالة hello
الخاصة بكلاس الـ Parent
فسيكون الناتج هكذا
Hello from Parent class!!
Hello from Child class!!
الجملة الأولى من الـ super.hello();
والثانية كانت من console.log('Hello from Child class!!');
تأمل المثال التالي
class Parent {
public f1() {
console.log('Call f1 from Parent Class!!');
}
public f2() {
console.log('Call f2 from Parent Class!!');
}
public f3() {
console.log('Call f3 from Parent Class!!');
}
}
class Child extends Parent {
public getAllFromParent() {
super.f1();
super.f2();
super.f3();
}
}
// Child
let child = new Child();
child.getAllFromParent();
لدينا دالة تدعى getAllFromParent
وظيفتها فقط أنها تستدعي الدوال f1
, f2
, f3
من الـ Parent
بالتالي نواتج الطباعة ستكون هكذا
Call f1 from Parent Class!!
Call f2 from Parent Class!!
Call f3 from Parent Class!!
ملحوظة
: من أهم فوائد الـsuper
أنك بعد ما تقوم بعملoverriding
لدالة معينة فأنت لا تفقد الدالة الخاصة بالـParent
بشكل كامل، بل يمكنك استدعائها باستخدام الـsuper
بسهولة
ماذا يحدث للـ Constructor في الوراثة
عندما نقوم بعمل new Child()
برأيك ما هو الـ Constructor
الذي تم استدعاؤه ؟
ستقول لي بكل بساطة الـ Constructor
الخاص بالـ Child
لكن مع علمك أن كلاس الـ Child
وارث من كلاس الـ Parent
ففي هذه الحالة new Child()
سيتم استدعاء اي Constructor
؟
أو يمكننا أن نطرح السؤال بشكل أشمل ماذا يحدث للـ Constructor
أثناء الوراثة ؟
سنقوم بتبسيط الأمور بشكل كامل
تأمل في المثال التالي
class Parent {}
class Child extends Parent {}
// Parent
let parent = new Parent();
هنا الـ Child
وارث من الـ Parent
عندما نقوم بعمل new Parent()
فإننا هنا نستدعي الـ default constructor
وهو كما ذكرنا في مقالة سابقة أنه الـ constructor
الافتراضي الذي يتم إنشاؤه عندما لا تقوم انت يعمل اي constructor
داخل في الكلاس
وقلنا الـ default constructor
يبدو هكذا
class Parent {
constructor() {}
}
الآن قلنا أن الـ Child
وارث من الـ Parent
class Parent {
constructor() {
console.log('Parent default constructor');
}
}
class Child extends Parent {}
// Child
let child = new Child();
هنا يظهر سؤال الـ default constructor
الخاص بالـ Child
كيف سيبدو ؟
إن كنت تظن أنه مثله مثل الـ Parent
فهذا خطأ
برائك عندما نقوم بعمل new Child();
هل يجب أن نستدعي كلاس الـ Parent أيضًا ؟
الإجابة بالطبع نعم يجب، لأنه من البديهي أنه لا يمكن أن يتواجد الـ Child
دون الـ Parent
لأنه ببساطة الـ Child
وارث منه، فكر في الأمر
لهذا في الكود السابق إن شغلته فستجد أنه طبع لك Parent default constructor
برغم من أننا استدعينا الـ constructor
الخاص بالـ Child
فقط new Child();
لهذا في الـ default constructor
الخاص بالـ Child
يجب أن نستدعي أي constructor
من الـ Parent
هل تتخيل كيف سيبدو الـ default constructor
الخاص بالـ Child
الآن ؟
class Parent {
constructor() {
console.log('Parent default constructor');
}
}
class Child extends Parent {
constructor() {
super(); // call the default constructor of Parent
console.log('Child default constructor');
}
}
// Child
let child = new Child();
لاحظ وجود super()
، لما ؟
كما قلنا سابقًا فإن الـ super
هي reference
للـ Parent
و يمكنك أن تتصرف كـ constructor
للـ Parent
، لهذا فعندما تقوم بعمل super()
فكأننا استدعينا الـ default constructor
الخاص بالـ Parent
يمكننا أن نبسط كل ما سبق بالتالي:
- أنه لا يمكن أن يتواجد الـ
Child
دون الـParent
- يجب أن نستدعي أي
constructor
من الـParent
- نستطيع أن نستدعي الـ
constructor
الخاص بالـParent
باستخدامsuper
اختبار بسيط على ما سبق
حسنًا بعد ما فهمت الـ overriding
وما هو الـ super
و كيف يتم التعامل مع الـ constructor
أريدك أن تتأمل في الكود التالي وترى هل يمكنك أن تحسنه قليلًا ؟
خذ وقتك ثم أكمل القراءة
class Student extends Person {
private level: number | undefined;
public setLevel(level: number) {
// imagine there is a nice validation here
this.level = level;
}
public printInfo() {
console.log(
`Name is: ${this.name}, his age: ${this.age}, his level: ${this.level}.`
);
}
}
let s1 = new Student('Ahmed', 22);
s1.setLevel(2);
s1.printInfo();
سأساعدك قليلًا في التفكير new Student('Ahmed', 22);
هنا نحن نرسل قيمتين فقط لأن الـ constructor
الخاص بكلاس الـ Person
مبني هكذا
لكن لدينا متغير level
وضعناه في الـ Student
، لما لا نستخدم الـ constructor
لإرسال قيمته بدل ما نستدعي دالة setLevel
هل نستطيع القيام بهذا ؟، خذ بعض الوقت للتفكير
class Student extends Person {
private level: number;
// overriding the constructor
constructor(name: string, age: number, level: number) {
super(name, age); // call constructor of Person constructor
this.setLevel(level);
}
private setLevel(level: number) {
// imagine there is a nice validation here
this.level = level;
}
public printInfo() {
console.log(
`Name is: ${this.name}, his age: ${this.age}, his level: ${this.level}.`
);
}
}
الحل أننا سنقوم بعمل overriding
للـ constructor
ثم نستدعي super
لنقوم بإرسال القيم التي يحتاجها الـ constructor
الخاص بالـ Person
ثم نقوم بعمل ما نريده داخل constructor
الـ Student
// overriding the constructor
constructor(name: string, age: number, level: number) {
super(name, age); // call constructor of Person constructor
this.setLevel(level);
}
هكذا نستطيع عمل الـ object
هكذا
let s1 = new Student('Ahmed', 22, 5);
s1.printInfo(); // Name is: Ahmed, his age: 22, his level: 5.
أريدك دائمًا أن توسع حدود تفكيرك، وتفهم جيدًا كيف تعمل الأجزاء الصغيرة
لتستطيع أن تجمع هذه الأجزاء الصغيرة مع بعضها لتكون الجزء الذي تريده
الأشكال المختلفة للـ Inheritance
في الـ Inheritance
هناك 5
أنواع وهم Single Inheritance
، Multilevel Inheritance
، Hierarchical Inheritance
، Multiple Inheritance
، Hybrid Inheritance
لا تقلق هذه الأنواع ليست أنواع مختلفة عن ما تعلمناه بل ستجد أنها بسيطة ومتشابهه
بل هي من ضمن ما نعرف، بالطبع هناك أشياء يجب أن تأخذ حذرك منها ومشاكل سنتحدث عنها
لكن جميعها بسيطة بإذن الله
Single Inheritance
الـ Single Inheritance
وهو أبسطهم وهو الوراثة المباشرة بين كلاسيين إثنين فقط
وهو النوع الذي كنا نشرحه بالفعل طوال الوقت
والذي نعبر عنه بهذا الشكل
Single inheritance
+---------------+
| Parent |
+---------------+
٨
|
|
+---------------+
| Child |
+---------------+
Multilevel Inheritance
الـ Multilevel Inheritance
هو امتداد للنوع السابق
وهو بدلًا من أن تكون علاقة بين كلاسيين إثنين فقط، تكون علاقة متسلسلة من الكلاسات
بمعنى أنه لو لدينا كلاس A, B, C, D
تكون شكل الوراثة هكذا D -> C -> B -> A
حيث D
وارث من C
الذي يكون وارث من B
الذي يكون وارث من A
وهكذا
ونعبر عنه بهذا الشكل
Multilevel Inheritance
+---------------+
| A |
+---------------+
٨
|
|
+---------------+
| B |
+---------------+
٨
|
|
+---------------+
| C |
+---------------+
٨
|
|
...ect
هذا النوع كما قنا هو امتداد للنوع السابق
بمعنى أنك يمكنك أن تتخيل العلاقة بين A
و B
كعلاقة مباشرة مثل النوع الأول
ونفس الأمر مع B
و C
تفكر فيهما كعلاقة بين كلاسيين فقط
الفرق أن B
هنا تعمل عمل Child
للـ A
وتكون Parent
للـ C
يمكنك أن تتخيل الأمر هكذا
Multilevel Inheritance
Single Inheritance 1 Single Inheritance 2
+---------------+ +---------------+
| A | | B |
+---------------+ +---------------+
٨ ٨
| |
| |
+---------------+ +---------------+
| B | | C |
+---------------+ +---------------+
مع العلم أن
B
ورثت منA
ثم بعد ذلكC
ورثت منB
فكأنC
وارث منA
بطريقة غير مباشرة وB
يعمل كوسيط هنا
مثال توضيحي
سيكون لدينا كلاس يدعى Person
وكلاس سيمثل فئة الموظفين يدعى Employee
وكلاس يمثل المدراء يدعى Manager
الفكرة أن كل مدير ما هو إلا موظف وكل الموظفين بني أدمين
فطبق هذا المفهوم كعلاقة بين الكلاسات
فستجد أن Employee
سيرث من Person
لأن كل الموظفين بني أدمين كما قلنا
ثم سنجعل الـ Manager
يرث من الـ Employee
لأن كل مدير ما هو إلا موظف
لنرى الأمور بشكل عملي
كلاس الـ Person
سيكون أبسط هذه المرة
class Person {
protected name: string;
protected age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
public printInfo() {
console.log(`Name is: ${this.name}, his age: ${this.age}.`);
}
}
ثم نجعل الـ Employee
يرث من الـ Person
class Employee extends Person {
private salary: number;
constructor(name: string, age: number, salary: number) {
super(name, age);
this.salary = salary;
}
public printInfo() {
console.log(
`Name is: ${this.name}, his age: ${this.age}, his salary: ${this.salary}.`
);
}
}
لاحظ أننا اضفنا salary
وعملنا overriding
للـ constructor
ولدالة printInfo
وأخيرًا لدينا Manager
سيرث من الـ Employee
class Manager extends Employee {
public hire(employee: Employee) {
console.log('hire a new employee');
// ...
}
public fire(employee: Employee) {
console.log('fire an employee');
// ...
}
}
وبالطبع المدير سيملك صلاحيات أعلى من الموظف العادي مثل دالة hire
ودالة fire
متع عينيك في الكود التالي
let employee = new Employee('Ahmed', 25, 3000);
let manager = new Manager('Ali', 40, 5000);
manager.hire(employee); // hire a new employee
manager.printInfo(); // Name is: Ali, his age: 40, his salary: 5000.
الآن سؤال، هنا manager.printInfo()
نحن استعملنا دالة printInfo
برغم من أنها ليست موجودة في كلاس الـ Manager
لكنها موجودة في كلاس الـ Person
والـ Employee
الآن manager.printInfo()
سيستدعي الدالة الموجودة في الـ Person
ام التي في الـ Employee
؟
تذكرون حين قلت لكم أنه يستدعي أقرب دالة بالنسبة للـ object
أقرب printInfo
بالنسبة للـ manager
بالطبع هي الموجودة في الـ Employee
الـ Employee
ورث الدالة من الـ Person
وعمل لها overriding
ثم جاء كلاس الـ Manager
وورث من الـ Employee
بالتالي سيرث دالة printInfo
الخاصة بالـ Employee
، وهذا منطقي وبديهي
لنفترض أن الـ Employee
لم يقم بعمل overriding
للدالة بالتالي الـ Manager
سيأخذ الدالة الموجدة في الـ Person
لاحظ أيضًا أن الـ constructor
الذي يستخدمه الـ Manager
هو الذي قام الـ Employee
بعمل overriding
له
الـ
Multilevel Inheritance
بسيط وسهل، وليس جديًدا عليك الفكرة أنك يجب أن تركز وتعرف تسلسل الكلاسات التي ترث من بعضها، وإن كان كلاس اضاف دوال جديدة أو عدل في دوال وعمل لهاoverriding
فيجب أن تراعي أن الكلاسات التي سترث من هذا الكلاس ستأخذه بنفس التعديلات الذي قام بها
فعلى سبيل المثال الـEmployee
قام بتعديل الـconstructor
ودالة الـprintInfo
فعندما جاء كلاس الـManager
وورث من الـEmployee
، فهو بالتالي ورث تلك التعديلات التي قام بها كلاس الـEmployee
Hierarchical Inheritance
الـ Hierarchical Inheritance
هو لا يختلف كثيرًا عن النوع الأول كذلك
وهو أن هناك أكثر من كلاس يرثون نفس الكلاس
بمعنى أنه لو لدينا كلاس A, B, C, D
تكون شكل الوراثة هكذا B -> A
و C -> A
و D -> A
حيث B
و C
و D
يرثون من A
ونعبر عنه بهذا الشكل
Hierarchical Inheritance
Parent
+---------------+
+--------------> | A | <------------+
| +---------------+ |
| ٨ |
| | |
| | |
+---------------+ +---------------+ +---------------+
| B | | C | | D |
+---------------+ +---------------+ +---------------+
Child 1 Child 2 Child 3
هنا يمكنك أن تتخيل الأمر بأن
B
يرث منA
علاقةSingle Inheritance
C
يرث منA
علاقةSingle Inheritance
D
يرث منA
علاقةSingle Inheritance
- مع العلم أن
B
وC
وD
كلاسات مستقلة بذاتها لا تربطهم أي علاقة
مثال توضيحي
سنقوم بعمل مثال بسيط وهو أننا ننشيء كلاس يدعى Employee
نجعله يرث من الـ Person
وكلاس يدعى Teacher
سيرث من الـ Person
وكلاس يدعى Doctor
سيرث أيضًا من الـ Person
أولًا ننشيء كلاس الـ Person
الجميل الخاص بنا
سنضيف متغير جديد يدعى role
class Person {
protected name: string;
protected age: number;
protected role: string;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
this.role = 'unknown';
}
public printInfo() {
console.log(
`Name is: ${this.name}, his age: ${this.age}, his role: ${this.role}.`
);
}
}
ننشيء كلاس Employee
ونجعله يرث من الـ Person
class Employee extends Person {
constructor(name: string, age: number) {
super(name, age);
this.role = 'Employee';
}
}
نقوم فقط بعمل overriding
على الـ constructor
ولاحظ أننا نسند قيمة للـ role
بـ Employee
ننشيء كلاس Teacher
ونجعله أيضًا يرث من الـ Person
class Teacher extends Person {
constructor(name: string, age: number) {
super(name, age);
this.role = 'Teacher';
}
}
وأخيرًا ننشيء كلاس Doctor
ونجعله أيضًا يرث من الـ Person
class Doctor extends Person {
constructor(name: string, age: number) {
super(name, age);
this.role = 'Doctor';
}
}
الآن أصبح لدينا Hierarchical Inheritance
Hierarchical Inheritance
كلاس مشترك
يرث من +---------------+ يرث من
+--------------> | Person | <------------+
| +---------------+ |
| ٨ |
| | يرث من |
| | |
+---------------+ +---------------+ +---------------+
| Employee | | Teacher | | Doctor |
+---------------+ +---------------+ +---------------+
أكثر من كلاس يرثون كلاس مشترك واحد
حيث أن الـ Employee
والـ Teacher
والـ Doctor
يريثون نفس الكلاس وهو الـ Person
والكلاسات مستقلة بذاتها لا تجمعهم أي علاقة غير أنهم يرثون من نفس الكلاس
بحيث أن تعديل ما على أي كلاس من لا يؤثر على باقي الكلاسات، لكن تعديل على الـ Person
سيؤثر، وهذا بديهي
let employee = new Employee('Ahmed', 25);
employee.printInfo(); // Name is: Ahmed, his age: 25, his role: Employee.
let teacher = new Teacher('Ali', 35);
teacher.printInfo(); // Name is: Ali, his age: 35, his role: Teacher.
let doctor = new Doctor('Othman', 40);
doctor.printInfo(); // Name is: Othman, his age: 40, his role: Doctor.
ستلاحظ أن كل كلاس قام بالتعديل على الـ constructor
وتعديله لم يؤثر على الباقي
Multiple Inheritance
الـ Multiple Inheritance
يمكنك أن تتخيله على أنه معكوس الـ Hierarchical
بمعنى أن هناك كلاس واحد يرث من أكثر من كلاس
بمعنى أنه لو لدينا كلاس A, B, C
تكون شكل الوراثة هكذا C -> A
و C -> B
حيث C
يرث من A
و B
ونعبر عنه بهذا الشكل
Multiple Inheritance
Parent 1 Parent 2
+---------------+ +---------------+
| A | | B |
+---------------+ +---------------+
٨ ٨
| |
+-------+ +-------+
| |
| |
+---------------+
| C |
+---------------+
Child
هنا لدينا كلاس C
يرث من A
و B
توضيح عملي
بصراحة ليس هناك توضيح عملي هنا !
هذا النوع يمتلك العديد من المشاكل والأمور الشائكة والصعوبات الدائرة حوله والتي جعلت اللغات الحديثة تتجنب هذا النوع وعدم دعمه من الأساس
لذا سنحاوله شرحه بشكل نظري ولما هناك صعوبة ومشاكل في التعامل معه
لن نستطيع تطبيقهما بشكل عملي لأن لغة typescript
واغلب اللغات الحديثة الآن لا تدعمها
بل وحاولت استبدالهما بشكل ما بمفهوم يدعى Interface
سنتكلم عنه بالتفصيل في المقالة التالية
لذا سنكتفي فقط ببعض الأكواد التخيلية والمشاكل التي تنشأ بسببه
مناقشة مشاكل الـ Multiple Inheritance بكود تخيلي
class A {}
class B {}
class C extends A, B {} // Error!!, Classes can only extend a single class.
قلنا أن الـ typescript
لا تدعم الـ Multiple Inheritance
لذا حصلنا على رسالة تخبرنا بهذا
لكن لنتخيل ماذا لو كان مدعومًا، لنستطيع أن نتخيل بشكل أفضل ونفهم المشاكل التي ستترتب عليها
هل يمكنك أن تفكر بالمشاكل التي قد تحدث ؟
تشابه أسماء الدوال أو المتغيرات في الكلاسات
تأمل في الكود التالي
class A {
public num: number = 0;
public increaseNum() {
this.num += 100;
}
}
class B {
public num: number = 0;
public increaseNum() {
this.num += 50;
}
}
class C extends A, B {}
هنا لدينا دالة تدعى increaseNum
في كلاس A
ودالة بنفس الإسم في كلاس B
ولاحظ أنهما يشتركان في فس الإسم لكن كل دالة تقوم بتنفيذ عملية مختلفة
عندما يرث الـ C
كلا الدالتين كيف سنفرق بينهما ؟
let objC = new C();
objC.increaseNum(); // Error!!, 'increaseNum' is ambiguous
عندما نستدعي increaseNum
فالـ objC
لا يعرف أي من الدالتين سينفذ!
هل ينفذ الدالة التي في كلاس A
أم التي في كلاس B
كيف يحدد أي دالة ؟
هنا يحدث ambiguous
أي غموض أو ارتباك بمعنى أن الـ object
لا يستطيع أن يحدد
بسبب وجود تشابه فهو لا يعرف ماذا يختار
سؤال آخر، عندما يحاول الـ C
التعامل مع المتغير num
أي متغير سيتم التعامل معه ؟ الخاص بـ A
أم بـ B
؟
class C extends A, B {
public getNum() {
return this.num; // Error!!, 'num' is ambiguous
}
}
let objC = new C();
let num = objC.getNum(); // Impossible?!
حدثت الـ ambiguous
هنا أيضًا مع المتغير لانه لا يعرف أي متغير هو الذي يجب أن يختاره
التعامل مع مشكلة تشابه أسماء الدوال بالـ Overriding
قلنا أننا لدينا دالة تدعى increaseNum
في كلاس A
ودالة بنفس الإسم في كلاس B
class A {
public num: number = 0;
public increaseNum() {
this.num += 100;
}
}
class B {
public num: number = 0;
public increaseNum() {
this.num += 50;
}
}
حسنًا، نحن تعلمنا أننا يمكننا تغير أو تعديل الدالة عن طريق عمل Overriding
السؤال هنا، ماذا سيحدث عندما يقوم الكلاس C بعمل overriding للدالة increaseNum ?
class C extends A, B {
// remove the ambiguous by overriding both methods in A and B
public increaseNum() {
this.num += 15;
}
}
let objC = new C();
objC.increaseNum(); // Works!!, 'num' increased by 15
عندما يقوم كلاس الـ C
بعمل overriding
لدالة الـ increaseNum
فهنا objC
لن يهتم بـ increaseNum
التي ورثها من A
و B
بل سيتعامل مع الدالة التي قام بعمل overriding
لها
هل هذا حل جيد ؟
الاجابة بالطبع لا في بعض الحالات
لنفترض أن كلاس C
يريد طريقة ليختار بحرية الدالة التي يريدها بدون تقييده بعمل overriding
للدالة
لنفرض أن C
يريد في بعض الأحيان أن ينفذ الدالة الخاص بـ A
وأحيانًا أخرى سيريد تنفيذ الدالة الخاصة بـ B
كيف نحل هذه المشكلة
التعامل مع أكثر من Constructor في آن واحد
من المشاكل الأخرى التي تحدث مع الـ Multiple Inheritance
هو أن على الكلاس
class A {
public str: string = 'Unknown';
constructor(str: string) {
this.str = str;
}
}
class B {
public num: number = 0;
constructor(num: number) {
this.num = num;
}
}
لدينا هنا constructor
مختلف لكل كلاس، حيث A
يستقبل متغير من نوع string
و B
يستقبل متغير من نوع number
كيف سيقوم الكلاس C باستدعاء الـ constructor الخاص بـ A و B ؟
لانه كما ذكرنا سابقًا، أنه لا يمكن أن يتواجد الـ Child
دون الـ Parent
ويجب أن نستدعي أي constructor
من الـ Parent
داخل constructor
الـ Child
class C extends A, B {
constructor(num: number, string: string) {
// How to call A and B constructors ?!
}
}
في حالة أن C
وارث من A
وB
فهو يجب أن يتعامل مع كل constructor
والـ super
الذي من المفترض أن يكون reference
للـ Parent
كيف له أن يكون reference
لأكثر من Parent
؟
مشكلة شكل الألماسة | Diamond Problem
هل تتوقف مشاكل الـ Multiple Inheritance
عند هذا الحد ؟
للآسف، لا هناك شكل فرعي يعتبر دمج بين الـ Hierarchical
مع الـ Multiple Inheritance
أولًا لننشيء شكل Hierarchical
بسيط جدًا
Hierarchical Inheritance
+---------------+
| A |
+---------------+
٨ ٨
| |
+-------+ +-------+
| |
+---------------+ +---------------+
| B | | C |
+---------------+ +---------------+
ثم لنفترض أن لدينا كلاس جديد يدعى D
يرث من B
و C
هكذا A, B, C
سيشكلون Hierarchical Inheritance
و B, C, D
سيشكلون Multiple Inheritance
وهكذا العلاقة الكاملة A, B, C, D
سيشكلون شكل المعين أول الألماسة لذا سميت بـ Diamond Problem
الشكل سيكون هكذا
Diamond Problem
A, B, C -> Hierarchical Inheritance
B, C, D -> Multiple Inheritance
+---------------+
| A |
+---------------+
٨ ٨
| |
+-------+ +-------+
| |
+---------------+ +---------------+
| B | | C |
+---------------+ +---------------+
٨ ٨
| |
+-------+ +-------+
| |
| |
+---------------+
| D |
+---------------+
حسنًا، ما المشكلة ؟
أريدك أن تفكر بكل النتائج والأمور التي ستترتب مع الكلاس D
ستلاحظ وجود مساران من A لـ D
، المسار الأول هو D -> B -> A
المسار الثاني هو D -> C - > A
معنى هذا أن الكلاس D يرث الكلاس A مرتين!
فكر في هذه النقاط
- كيف سيتعامل كلاس الـ
D
مع كل هذه الـConstructor
التي سيستقبلها ؟ - هل تستطيع تجنب تكرار استدعاء الكلاس
A
مرتين ؟ لأن هناك مسارين منD
لـA
- بفرض أن الكلاسيين
B
وC
قاما بتعديل بعض الدوال في الـA
- هل يملك الكلاس
D
اختيار المسار الذي يريده، بمعنى أنه في بعض الأحيان يريد أن ينفذ الدالة الخاص بالمسار الأول وأحيانًا أخرى يريد تنفيذ الدالة الخاصة بالمسار الثاني
- هل يملك الكلاس
- مشكلة تشابه أسماء الدوال كيف سيتعامل معها الكلاس
D
- الـ
super
الذي في كلاسD
ماذا سيفعل وسط كل هذه الفوضى ؟
أريدك فقط هنا أن تستوعب كمية التعقيد والصعوبات التي تدور حول الـ
Multiple Inheritance
كيف تعاملت اللغات القديمة مع مشاكل الـ Multiple Inheritance
برغم من كل هذه الصعوبات مع الــ Multiple Inheritance
فهناك لغات مثل لغة الـ C++
تدعمها بشكل كامل
وبالطبع ستجد اختلافات وأمور جديدة تقدمها اللغة لكي تتعامل مع هذا النوع
وأيضًا قامت لغة C++
بابتكار مفهوم يدعى virtual
لتسهيل التعامل مع أغلب مشاكل الـ Multiple Inheritance
لكننا هنا لن نتطرق لشرح تفاصيلها لان هذا ليس موضوعنا، ولأن الأمر يحتاج لمقالة مستقلة لنشرحها باستفاضة وبالتفصيل
لكني أحببت فقط أن اننوه على مفهوم الـ virtual
الخاص بلغة C++
لتكن على علم لا أكثر
هل الـ Multiple Inheritance له فائدة من الأساس ؟
بعد كل هذه الصعوبات، هل الـ Multiple Inheritance
نوع سيء فقط ولا يوجد له فائدة ؟
الاجابة بالطبع لا، فلما ابتكروه من البداية!
لا يوجد شيء ينشيء أو يتواجد دون هدف
في الحقيقة الـ Multiple Inheritance
يعتبر من أهم الأنواع هنا
إذًا هنا نطرح سؤال، هل في عالمنا هذا يوجد شيء يأخذ صفات من أكثر شيء ؟
بمعنى هل فكرة الـ Multiple Inheritance
يمكن تطبيقها في عالمنا أو في افكار مشاريع حقيقية ؟
الإجابة، نعم لا يمكنك أن تحصر شيء ما بأنه يرث أو يأخذ صفاته من شيء واحد فقط لاغير
دائما ستحتاج إلى أن تأخذ صفات من أكثر شيء
فعلى سبيل المثال الإنسان يمكنه أن يكون موظف في شركة ما وفي نفس الوقت يكون أب لأسرة
و في نفس الوقت يعمل في دوام جزئي في محل تجاري وفي نفس الوقت صانع محتوي أو مؤثر على مواقع التواصل الاجتماعي و .. و ...
**نحن بني ادمين!**، بالطبع سنملك صفات وأشياء كثيرة من أشياء وأمور متعددة
ففي المثال السابق سيكون لدينا كلاس اسمه على سبيل المثال عم أيمن
عم أيمن وارث من كلاس Person
ومن كلاس Employee
و ومن Father
ومن Trader
ومن Content Creator
ومن Influencer
و من ... و ... و ...
هل استوعبت الفكرة ؟ والأمر لا ينحسر على البني ادمين فقط يمكنك أن تطبق الأمر على كل شيء تعرفه
واسأل نفسك هل فعلا يمكن لشيء أن يرث أو يأخذ صفات من شيء واحد فقط ؟ ام يأخذ صفات من أكثر من شيء مختلف ؟
الـ Multiple Inheritance
مهم جدًا ويجب أن نستخدمه دائمًا لهذا اللغات القديمة كانت تدعمها وتبتكر طريقة لتجنب أو التعامل مع صعوباته
محاولة اللغات الحديثة لاستبدال مفهوم الـ Multiple Inheritance بالـ Interface
اللغات الحديثة لم تتبنى فكرة الـ Multiple Inheritance
بسبب الصعوبات التي تكلمنا عنها
ولكنها في نفس اللحظة لا تريد ان تتخلي عن فكرة أننا نستطيع أن نرث من أكثر من كلاس
لانه كما قلنا إنها مطلوبة بشكل كبير وكل شيء نعرفه يأخذ صفات أكثر من شيء واحد ولا ينحصر على كلاس واحد
لهذا تم التعديل على مفهوم الـ Multiple Inheritance
قليلا واستبداله بالـ Interface
وهو كما قلنا سنتحدث عنه بالتفصيل في المقالة القادمة بإذن الله
Hybrid Inheritance
الـ Hybrid Inheritance
ليس نوعًا جديدًا ولا يختلف عن ما نعرفه
أي مزيج بين الأربع أنواع الذي تكلمنا عنها يعتبر Hybrid Inheritance
بمعنى أنه خليط من نوعين أو أكثر
فعلى سبيل المثال
- عندما تدمج
Multiple
معHierarchical
نسمي هذاHybrid
- أو عندما تدمج
Hierarchical
معMultiple
معMultilevel
- أو أن تدمج
Hierarchical
معMultilevel
- أو .. إلخ
مشكلة شكل الألماسة التي تحدثنا عنها Diamond Problem
تعتبر Hybrid Inheritance
فالـ Hybrid Inheritance
ليس نوعًا جديدًا كما قلنا هو فقط خليط ودمج بين باقي الأنواع
يمكنك أن تفكر بالأمور التي ستترتب على هذا
نحن لن نتطرق إلى كل شكل ونوع لأننا لن نقول شيء جديد