الـ Inheritance، وراثة الكلاسات

السلام عليكم ورحمة الله وبركاته

وقت القراءة: ≈ 25 دقيقة (بمعدل فنجان واحد من الشاي 😊)

المقدمة

نبدأ المقالة الثالثة في سلسلتنا عن الـ 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;
}
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 = 'Ahmed'; // child object CAN access `name`

هنا أي شيء public في الـ Parent يستطيع كلاس الـ Child الوصول له
وأيضًا يستطيع الـ object من كلاس الـ Child الوصول له

private

class Parent {
  private name!: string;
}
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 = 'Ahmed'; // ERROR!!, Property 'name' is private and only accessible within class 'Parent'.

هنا أي شيء private في الـ Parent لا يستطيع كلاس الـ Child الوصول له
وأيضًا لا يستطيع الـ object من كلاس الـ Child الوصول له

protected

class Parent {
  protected name!: string;
}
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 = 'Ahmed'; // 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;

  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;

  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;

  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;

  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

يمكننا أن نبسط كل ما سبق بالتالي:

اختبار بسيط على ما سبق

حسنًا بعد ما فهمت الـ overriding وما هو الـ super و كيف يتم التعامل مع الـ constructor

أريدك أن تتأمل في الكود التالي وترى هل يمكنك أن تحسنه قليلًا ؟
خذ وقتك ثم أكمل القراءة

class Student extends Person {
  private level!: number;

  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

هنا يمكنك أن تتخيل الأمر بأن

مثال توضيحي

سنقوم بعمل مثال بسيط وهو أننا ننشيء كلاس يدعى 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 بكود تخيلي

class A {}
class B {}
class C extends A, B {} // Error!!, Classes can only extend a single class.

قلنا أن الـ typescript لا تدعم الـ Multiple لذا حصلنا على رسالة تخبرنا بهذا
لكن لنتخيل ماذا لو كان مدعومًا، لنستطيع أن نتخيل بشكل أفضل ونفهم المشاكل التي ستترتب عليها

هل يمكنك أن تفكر بالمشاكل التي قد تحدث ؟

تشابه أسماء الدوال أو المتغيرات في الكلاسات

تأمل في الكود التالي

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 هو أن على الكلاس

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 عند هذا الحد ؟
للآسف، لا هناك شكل فرعي يعتبر دمج بين الـ Hierarchical مع الـ Multiple

أولًا لننشيء شكل 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 مرتين!

فكر في هذه النقاط

أريدك فقط هنا أن تستوعب كمية التعقيد والصعوبات التي تدور حول الـ Multiple Inheritance

كيف تعاملت اللغات القديمة مع مشاكل الـ Multiple

برغم من كل هذه الصعوبات مع الــ Multiple Inheritance فهناك لغات مثل لغة الـ C++ تدعمها بشكل كامل
وبالطبع ستجد اختلافات وأمور جديدة تقدمها اللغة لكي تتعامل مع هذا النوع
وأيضًا قامت لغة C++ بابتكار مفهوم يدعى virtual لتسهيل التعامل مع أغلب مشاكل الـ Multiple Inheritance
لكننا هنا لن نتطرق لشرح تفاصيلها لان هذا ليس موضوعنا، ولأن الأمر يحتاج لمقالة مستقلة لنشرحها باستفاضة وبالتفصيل
لكني أحببت فقط أن اننوه على مفهوم الـ virtual الخاص بلغة C++ لتكن على علم لا أكثر

هل الـ Multiple له فائدة من الأساس ؟

بعد كل هذه الصعوبات، هل الـ Multiple نوع سيء فقط ولا يوجد له فائدة ؟
الاجابة بالطبع لا، فلما ابتكروه من البداية!

لا يوجد شيء ينشيء أو يتواجد دون هدف
في الحقيقة الـ Multiple يعتبر من أهم الأنواع هنا

إذًا هنا نطرح سؤال، هل في عالمنا هذا يوجد شيء يأخذ صفات من أكثر شيء ؟
بمعنى هل فكرة الـ Multiple يمكن تطبيقها في عالمنا أو في افكار مشاريع حقيقية ؟
الإجابة، نعم لا يمكنك أن تحصر شيء ما بأنه يرث أو يأخذ صفاته من شيء واحد فقط لاغير
دائما ستحتاج إلى أن تأخذ صفات من أكثر شيء

فعلى سبيل المثال الإنسان يمكنه أن يكون موظف في شركة ما وفي نفس الوقت يكون أب لأسرة
و في نفس الوقت يعمل في دوام جزئي في محل تجاري وفي نفس الوقت صانع محتوي أو مؤثر على مواقع التواصل الاجتماعي و .. و ...
**نحن بني ادمين!**، بالطبع سنملك صفات وأشياء كثيرة من أشياء وأمور متعددة

ففي المثال السابق سيكون لدينا كلاس اسمه على سبيل المثال عم أيمن
عم أيمن وارث من كلاس Person ومن كلاس Employee و ومن Father ومن Trader ومن Content Creator ومن Influencer و من ... و ... و ...

هل استوعبت الفكرة ؟ والأمر لا ينحسر على البني ادمين فقط يمكنك أن تطبق الأمر على كل شيء تعرفه
واسأل نفسك هل فعلا يمكن لشيء أن يرث أو يأخذ صفات من شيء واحد فقط ؟ ام يأخذ صفات من أكثر من شيء مختلف ؟

الـ Multiple مهم جدًا ويجب أن نستخدمه دائمًا لهذا اللغات القديمة كانت تدعمها وتبتكر طريقة لتجنب أو التعامل مع صعوباته

محاولة اللغات الحديثة لاستبدال مفهوم الـ Multiple بالـ Interface

اللغات الحديثة لم تتبنى فكرة الـ Multiple بسبب الصعوبات التي تكلمنا عنها
ولكنها في نفس اللحظة لا تريد ان تتخلي عن فكرة أننا نستطيع أن نرث من أكثر من كلاس
لانه كما قلنا إنها مطلوبة بشكل كبير وكل شيء نعرفه يأخذ صفات أكثر من شيء واحد ولا ينحصر على كلاس واحد

لهذا تم التعديل على مفهوم الـ Multiple قليلا واستبداله بالـ Interface وهو كما قلنا سنتحدث عنه بالتفصيل في المقالة القادمة بإذن الله

Hybrid Inheritance

الـ Hybrid Inheritance ليس نوعًا جديدًا ولا يختلف عن ما نعرفه
أي مزيج بين الأربع أنواع الذي تكلمنا عنها يعتبر Hybrid Inheritance
بمعنى أنه خليط من نوعين أو أكثر

فعلى سبيل المثال

مشكلة شكل الألماسة التي تحدثنا عنها Diamond Problem تعتبر Hybrid Inheritance

فالـ Hybrid Inheritance ليس نوعًا جديدًا كما قلنا هو فقط خليط ودمج بين باقي الأنواع
يمكنك أن تفكر بالأمور التي ستترتب على هذا
نحن لن نتطرق إلى كل شكل ونوع لأننا لن نقول شيء جديد