مفهوم الـ Abstraction في إخفاء التفاصيل

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

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

المقدمة

اليوم سنشرح ثالث مفهوم من مفاهيم الـ OOP وهو الـ Abstraction

يمكننا إعطاء تعريف بسيط له ونقول

أن الـ Abstraction هو مفهوم يركز على التعامل مع الأشياء دون الاهتمام بالتفاصيل التي تحيط بهذا الشيء

سنتكلم عن كيف تم توظيف هذا المفهوم في عالم الـ OOP

أصل المشكلة

تخيل معي أن لدينا كلاس بسيط يدعى Employee يمثل الموظفين

class Employee {
  protected name: string;
  protected salary: number;
  protected companyName: string;

  constructor(name: string, salary: number, companyName: string) {
    this.name = name;
    this.salary = salary;
    this.companyName = companyName;
  }

  public work() {
    console.log('work instructions ...');
  }
}

به اسم الموظف وراتبته واسم الشركة التي يعمل بها ولدينا constructor بسيط
ودالة تدعى work وتخيل معي أن الدالة هذه تجعل الموظف يقوم بشيء ما في عمله
الآن عندما نقوم بعمل object لهذا الكلاس للموظف يدعى أحمد و object آخر لموظف يدعى محمد وآخر لمحمود
وكل واحد يعمل في شركة مختلفة

let e1 = new Employee('Ahmed', 3000, 'Careem');
let e2 = new Employee('Mohamed', 6000, 'Sutra');
let e3 = new Employee('Mohamed', 7000, 'Vodafone');

لنسأل بعض الأسئلة هنا:

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

استخدام الـ Hierarchical inheritance

قد تقول لي الحل سهل، نستطيع أن ننشئ كلاس لكل شركة ثم نقوم بعمل overriding لدالة الـ work في كل كلاس
ونكتب فيها التفاصيل التي تناسب كل شركة وطريقة عملها

تبدو فكرة سليمة ومنطقية، لنجربها ونرى ماذا سيحدث ..
أولا ننشئ كلاس لكل شركة وكل كلاس سيرث الكلاس الأساسي وهو الـ Employee بالطبع
وفقط سنقوم بعمل overriding ونعدل على دالة work في كل كلاس

class CareemEmployee extends Employee {
  public work() {
    console.log(`${this.name} helps people get around!`);
  }
}

class SutraEmployee extends Employee {
  public work() {
    console.log(`${this.name} helps people wear fashionable clothes!`);
  }
}

class VodafoneEmployee extends Employee {
  public work() {
    console.log(`${this.name} helps people stay connected!`);
  }
}
let e1 = new CareemEmployee('Ahmed', 3000, 'Careem');
let e2 = new SutraEmployee('Mohamed', 6000, 'Sutra');
let e3 = new VodafoneEmployee('Mahmoud', 7000, 'Vodafone');

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

لكن هل لاحظت شيئًا ما ؟

هل إنشاء الدوال في كلاس Employee له فائدة ؟

سؤال، الآن الدالة work التي تتواجد في الكلاس Employee الأساسي، هل لها فائدة ؟
اقصد هل كتابة أي تفاصيل فيها او عمل implementation لها له فائدة ؟

فكر في الأمر، نحن لا نستعمل دالة work المتواجدة في كلاس الـ Employee بعينها
نحن فقط نقوم بعمل overriding لها لنغيرها ولتناسب كل شركة
لذا الدالة الأصلية المتواجدة في الـ Employee نحن لا نستعملها
لذا كتابة كتابة implementation لها ليس له أي فائدة في حالتنا تلك، لأننا نقوم بعمل overriding لها على أي حال

إذا ماذا نفعل ؟ هل نمسح الدالة من كلاس الـ Employee ؟ حسنا يبدو حلًا منطقيًا طالما نحن لا نستفيد فعليا من وجودها في كلاس الـ Employee
لأننا نعرف الدالة بالفعل يدويًا في الكلاسات التي ترث الـ Employee وكتب الـ implementation الذي يحلو لنا

لكن هنا تظهر مشكلة أخرى إذا مسحنا الدالة، ماذا اذا نسينا تعريف الدالة في الكلاسات التي سترث الـ Employee ؟
بحكم انه لا شئ ينبهنا أو يذكرنا بأن نكتبها

إذا ماذا نفعل ؟ هل نكتب الدالة بهذا الشكل ؟

public work() : void { }

بحيث تكون متواجدة في كلاس الـ Employee وأيضا خالية من أي implementation يبدو أننا حللنا المشكلة ! لكن تظل هناك بعض المشاكل الصغيرة إذا كانت الدالة تقوم بإرجاع شيء ما ؟

public getSalary() : number { } // Error!!

لا يمكننا ترك الدالة فارغة هكذا يجب أن ترجع شيء والحلول التي نقترحها كترك الدالة فارغة هي مجرد حلول تحايلية وليست حلول حقيقية ومنطقية اذا فكرت فيها

لذا نحن بحاجة لشيء ما نستطيع من خلالها تعريف الدالة دون أن نقوم بعمل implementation له
ففي نفس الوقت عندما يقوم اي كلاس بوراثة كلاس الـ Employee يجب أن ننبه ونذكره بأن يكتب implementation لدالة التي ورثها للتو

تعليق ChatGPT

لقد جعلت ChatGPT يقيم الفقرة السابقة ويعطي رأيه عن هذا الأمر

نعم، إنشاء الدوال في كلاس Employee له فائدة في حالات عديدة. على الرغم من أننا قد نقوم بـ overriding هذه الدوال في الكلاسات التي ترث من Employee، إلا أنه من المهم وجود دوال أساسية لتعريف واجهة الـ Employee وعلى الأقل إعطاء الفكرة العامة عن سلوك الموظف العام.
كما ذكرت، يمكن أن تساعدنا إنشاء الدوال في الـ Employee على تذكيرنا بضرورة تعريف هذه الدوال في الكلاسات التي ترث منه. ويمكن استخدام مفهوم الـ Abstraction لتعريف دوال في Employee بدون تحديد implementation، وهذا يساعد في توحيد الواجهة للكلاسات التي ترث منه وتسهيل الصيانة في المستقبل.
- ChatGPT

Abstraction

هنا يأتي مفهوم الـ Abstraction بشكل عملي في الـ OOP

الـ Abstraction كما قلنا هو مفهوم يركز على إلغاء أي تفاصيل أو implementation للأشياء

في عالم الـ OOP لدينا عدة تطبيقات عملية له

فهنا ستظهر لنا keyword جديدة في اللغة تسمى abstract
تساعدنا في تطبيق هذا المفهوم، وسنشرح ما تفعل بالتحديد

لكن أريدك أن تعرف هذه المسميات البسيطة ثم سنشرحها فيما بعد

ملحوظة: هناك قاعدة هامة عليك أن تعرفها وهو أي شيء يتحول إلى abstract لا يتم وراثته كما هو
بل يتم اعادة بناءه وعمل overriding له بشكل اجباري وعمل implementation بشكل اجباري
تذكر هذا جيدًا، وسنفهم السبب لاحقًا وسأذكرك به لا تقلق

Abstract Member

الـ Abstract Member هو مجرد مسمى يكون عائد على Abstract Property أو Abstract Method
وكلمة abstract هي مجرد keyword جديدة تساعدنا على تطبيق مفهوم الـ Abstraction

ملحوظة: أي Abstract Member يجب أن يتواجد داخل Abstract Class و سنشرح السبب وسنعرف ما هو الـ Abstract Class فيما بعد

سنشرح هنا الـ Abstract Method قبل الـ Abstract Property

Abstract Method

قلنا أننا عندما نضع كلمة abstract خلف أي دالة يصبح اسمها Abstract Method
ومعنى هذا هو جعل الدالة مجردة من التفاصيل اي خالية من أي implementation وهذا ما نريده

abstract class Employee {
  public abstract work(): void;
  public abstract getSalary(): number;
}

هكذا دالة work أصبحت abstract بمعنى أننا عرفناها لكن لن نعطيها اي implementation بعد
وكتبنا void لكي نوضح أنها لا ترجع شيء
كذلك نفس الأمر مع getSalary وضحنا أنها abstract وترجع number

أعرف أنك تتساءل عن الـ abstract class لكن اصبر علي سنشرحها عندما ننتهي من الـ abstract member

الآن دعونا نرى المميزات بشكل عملي لنعرف ما الذي حصل
لنجعل كلاس CareemEmployee يرث الـ Employee بهذا الشكل

class CareemEmployee extends Employee {}

عندما ترث الـ Employee بدون أن تكتب أو تعرف اي شيء في كلاس CareemEmployee
ستجد هذه الرسالة الجميلة تقابلك

Non-abstract class 'CareemEmployee' does not implement inherited abstract member 'work' from class 'Employee'
Non-abstract class 'CareemEmployee' does not implement inherited abstract member 'getSalary' from class 'Employee'.

ستجد أنه يتم تنبيهك بأنك نسيت عمل implementation للـ abstract method بسبب أن من ميزات الـ abstract أنه ينبهك بأن تقوم بعمل implementation لها في الكلاسات التي سترثها

class CareemEmployee extends Employee {
  // abstract method implementation (اجباري)
  public work() {
    console.log(`${this.name} helps people get around!`);
  }
  // abstract method implementation (اجباري)
  public getSalary() {
    return this.salary * 2 - 2000;
  }
}

الغرض الأساسي من الـ abstract هو أنه هناك كيانات مثل الإنسان أو الحيوانات أو الموظفين أو أي كيان بشكل عام لا يمكننا توحيد أو تعريف دالة على سبيل المثال ونعممها على الجميع

فنحن نستخدم الـ abstract method لتعريف الدوال الاساسية التي تتواجد عند الجميع
لكن تفاصيل هذه الدالة وطريقة تنفيذها تختلف من شخص للتاني

Abstract Property

قلنا أننا عندما نضع كلمة abstract خلف أي متغير يصبح اسمه Abstract Property
ومعنى هذا هو جعل المتغير يكون مجرد وصف لشيء ما ولا يمكن أن يحتوي هذ المتغير على قيمة افتراضية
الأمر مشابه للـ abstract method حيث أنه لا تحتوي على implementation

abstract class Employee {
  public abstract name: string;
}

هنا الـ name اصبح abstract
بالتالي هو لن يتم وراثته بل يجب على الكلاسات مثل CareemEmployee عندما يرث الـ Employee
أن يقوم بتعريفه عنده بشكل اجباري

class CareemEmployee extends Employee {
  // define abstract property (اجباري)
  public name: string = 'unknown';

  // abstract method implementation (اجباري)
  public work() {
    console.log(`${this.name} helps people get around!`);
  }

  // abstract method implementation (اجباري)
  public getSalary() {
    return this.salary * 2 - 2000;
  }
}

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

ملحوظة: في بعض اللغات يجب أعطاء قيمة افتراضية للـ Abstract Property

فيمكننا فهم وتعريف الـ Abstract Member على أنه مجرد نموذج لشيء ما، خالي من أي تفاصيل
عندما يأخذ أي كلاس هذا النموذج فهو يكون مجبور على اتباع هذا النموذج وبناءه عنده

تعليق ChatGPT

لقد جعلت ChatGPT يعطي رأيه مرة أخرى عن الـ Abstract Member

الـ Abstract Member هو نوع من المتغيرات أو الدوال التي يتم تعريفها بـ abstract والتي لا تحتوي على تفاصيل محددة للـ implementation في الكلاس الذي يحتوي عليها، وتعتبر فقط نموذجاً لعنصر ما يجب توفيره في الكلاسات المشتقة منه. يجب على الكلاسات المشتقة من الكلاس الأصلي توفير implementation لهذه الـ abstract members، سواء كان ذلك عن طريق تحديد قيمة للـ abstract property، أو بتوفير implementation للـ abstract method. بذلك، يمكن استخدام الـ abstract members لتعريف عناصر جزئية من واجهة المستخدم أو تحديد المتطلبات الواجب توفيرها لأي كلاس مشتق.
- ChatGPT

إنشاء Object من كلاس به Abstract Member

الآن لدي سؤال وأريدك أن تفكر فيه جيدًا
هل عندما يكون لدينا كلاس ما مثل كلاس الـ Employee وبه Abstract Member ولتكن دالة بهذا الشكل

abstract class Employee {
  public abstract work(): void;
}

let e1 = new Employee();
e1.work(); // what will happen ?

وأنشأنا منه object كما ترى واستدعينا دالة work التي جعلناها abstract
هل هذا منطقي بالنسبة لك ؟

لنحلل الأمر قليلًا، دالة work لا تحتوي على implementation لانها abstract method
لانه لا يمكننا وضع طريقة ثابتة وموحدة للدالة كما قلنا
فاستدعاءك لها ليس له هدف أو منطق

إذا لنعود للسؤال الأساسي، هل من المنطقي إنشاء object من كلاس به abstract method ؟
ستجد نفسك بعد التفكير أنه يجب أن لا نسمح بإنشاء object من كلاس إذا كان يملك abstract method واحدة على الاقل

وأيضًا إذا فكرنا بشكل اعمق ستجد أننا من الأساس ننشيء abstract method عندما يكون الكلاس نفسه يمثل شيء عام لا يمكن تخصيصه
بمعنى في مثالنا هنا الـ Employee كلاس يمثل فئة الموظفين، ولا يمثل فئة معينه منهم بل يمثل الموظفين بشكل عام
لذا ستجد أنك إذا فكرت بكلاس الـ Employee بذاته ستجد أننا يجب أن نجرده من التفاصيل ولا نكتب أي implementation لأغلب الدوال التي ستتعرف بداخله
لأن الكلاس نفسه عام جدًا ولا نستطيع أن نخصص شيء ونعممه على جميع الموظفين

نحن نستطيع أن نكتفي بتعريف الدوال والصفات التي يجب أن تكون في كل موظف
مثل أن

بدون كتابة أي implementation للدوال، لانه لا يمكننا تعميمها من الأساس

الأمر مشابه لأي شيء نستطيع أن يكون وصف لكيان ما مثل إنسان، حيوان، موظف، طباخ، عامل، سيارة و ... وغيره
كل هؤلاء مجرد وصف لكيان عام، لذا نستطيع أن نجعلهم Abstraction ونضع الدوال والمتغير والأمور التي تصف هذا الكيان

حسنًا الآن نحتاج إلى أن نمنع إنشاء object من كلاس إذا كان يملك abstract member واحدة على الاقل

هنا يظهر الـ Abstract Class

Abstract Class

تتذكر عندما قلنا أننا يجب إنشاء أي Abstract Member داخل الـ Abstract Class
الآن سنعرف السبب وأحد مميزات الـ Abstract Class

عندما تنشيء abstract member داخل كلاس عادي هكذا

class Employee {
  public abstract work(): void; // Error!!
}

ستجد أن اللغة تمنعك من هذا
وستجد رسالة جميلة أخرى تقول لك

Abstract methods can only appear within an abstract class.

الأمر مماثل مع الـ abstract property

class Employee {
  public abstract name: string; // Error!!
}

وستجد نفس الرسالة جميلة تقول لك

Abstract properties can only appear within an abstract class.

بمعنى أنك لا تستطيع تعريف أي abstract member داخل كلاس عادي
أي انك تستطيع إنشاء Abstract Member داخل Abstract Class فقط

بهذا الشكل

abstract class Employee {
  public abstract work(): void; // No Error :D
}

وعندما تحاول إنشاء object منه، سينبهك بأنك لا يمكنك القيام بهذا

Abstract methods can only appear within an abstract class.

كما ترى فهو يقول لك أنك ببساطة لا تستطيع إنشاء object من كلاس نوعه abstract

الـ Abstract Class هو كلاس عادي لكن يختلف عنه في بعض الأمور، سنستعرضها في هذا الجدول البسيط

Abstract Class Normal Class
لا يمكن إنشاء منه object لاحتواءه على abstract member يمكن إنشاء منه object
نستخدمه كقالب أساسي لتمثيل شيء ما بتعريف خواصه ودواله
دون كتابة أي تفاصيل أو implementation لها
نستخدمه ليقوم بعمل implementation للدوال
ويكون تمثيل حي لطبيعة عمل شيء ما
يمكن أن يحتوي على دوال ومتغيرات عادية و abstract member يمكن أن يحتوي على دوال ومتغيرات عادية فقط
يمكننا إجبار الكلاسات التي سترثه
بعمل implementation للـ abstract method
الكلاسات التي سترثه ليست مجبرة على
عمل overriding و implementation لأي دالة

إذًا الـ Abstract Class يمكنك أن نفهمه بأنه كلاس عادي جدًا لكنه يضم abstract member
وتكون بعض الدوال والمتغيرات التي لا يمكن تعميمها على الجميع

ثم تأتي الكلاسات التي سترث منه وتبدأ هي بأخذ هذه الـ abstract member وتقوم بعمل implementation لها

وكما قلنا أنه لا يمكن إنشاء منه object لأن مفهوم الـ Abstraction هو تجريد من التفاصيل
فمن المنطقي انه لا يمكن إنشاء منه اي object لان Abstraction لا يقدم لك أي وظائف تفاعلية من الأساس
والـ object يحتاج لتفاصيل كاملة ليكون له هوية ما يستطيع أن يتفاعل ويؤدي وظيفته من خلالها

لنرى كيف سيصبح شكل المثال الاساسي خاصتنا الآن

abstract class Employee {
  protected name: string;
  protected salary: number;
  protected companyName: string;

  constructor(name: string, salary: number, companyName: string) {
    this.name = name;
    this.salary = salary;
    this.companyName = companyName;
  }

  // abstract methods
  public abstract work(): void;
  public abstract getSalary(): number;

  // non-abstract method (normal)
  public getCompanyName() {
    return this.companyName;
  }
}

class CareemEmployee extends Employee {
  public work() {
    console.log(`${this.name} helps people get around!`);
  }

  public getSalary() {
    return this.salary * 2 - 2000;
  }
}

class SutraEmployee extends Employee {
  public work() {
    console.log(`${this.name} helps people wear fashionable clothes!`);
  }

  public getSalary() {
    return this.salary * 3 - 4000;
  }
}

class VodafoneEmployee extends Employee {
  public work() {
    console.log(`${this.name} helps people stay connected!`);
  }

  public getSalary() {
    return this.salary / 2 + 3000;
  }
}
let e1 = new CareemEmployee('Ahmed', 3000, 'Careem');
let e2 = new SutraEmployee('Mohamed', 6000, 'Sutra');
let e3 = new VodafoneEmployee('Mahmoud', 7000, 'Vodafone');

e1.work(); // OUTPUT: Ahmed helps people get around!
e2.work(); // OUTPUT: Mohamed helps people wear fashionable clothes!
e3.work(); // OUTPUT: Mahmoud helps people stay connected!

كما ترى الأمور اصبحت ابسط ومنظمة أكثر
لدينا كلاس Employee نوعه abstract به الخواص الاساسية التي نريدها
ولدينا abstract methods وهي دالة work و getSalary نقوم بعمل implementation لهما في باقي الكلاسات
ولدينا ايضًا دالة getCompanyName وهي دالة عادية ليست abstract method لانها تقوم بغرض واحد يمكن أن يعمم بدون مشاكل
أنت لك الحرية بجعل الدالة تكون abstract method أم لا، بحسب الغرض ووظيفة الدالة

لاحظ أنه برغم من أن الـ Abstract Class يمتلك Constructor إلا أنه لا يمكن إنشاء منه Object
الـ Constructor فائدته أنه يتم وراثته من باقي الكلاسات فقط

عدم استطاعتنا وراثة أكثر من Abstract Class واحد

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

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

فهكذا سنمتلك Abstract Class لكل هذا، واحد لـ Person وآخر لـ Employee و Father و Trader و Content Creator و Influencer .. و ... و ...

فلنفترض أنه لدينا عم أيمن وهو شخص يقوم بكل ما سبق فهو موظف وأب وصانع محتوى و ... إلخ
كيف لعم أيمن أن يرث من كلاس Person ومن كلاس Employee و ومن Father ومن Trader ومن Content Creator ومن Influencer و من ... و ... و ...

وأنت تعرف أن اللغة تدعم الوراثة من كلاس واحد فقط
طبعًا تحدثنا عن السبب بالتفصيل في المقالة السابقة الخاصة بالوراثة
وتحدثنا عن الـ Multiply Inheritance وكل مشاكله في هذه المقالة inheritance

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

لهذا تم التعديل على مفهوم الـ Multiple قليلًا واستبداله بالـ Interface

Interface

الـ Interface هو أيضًا يتبع نفس مفهوم الـ Abstraction
فهو تجريد تام من التفاصيل وعدم اعطاء أي implementation

لكن الـ Interface ليس مثل الكلاسات ولا يندرج تحت مسمى كلاس، بل هو مفهوم جديد ومختلف عنه
ولا يتبع مفهوم الوراثة الخاصة بالكلاسات ولا يملك constructor
أريدك أن لا تتخيل الـ Interface على انه كلاس، بل تخيله كشيء جديد سنتعلمه الآن

الفرق بين الـ Interface والـ Abstract Class

أنظر إلى هذا الـ Abstract Class

abstract class Employee {
  protected name: string;
  protected salary: number;
  protected companyName: string;

  constructor(name: string, salary: number, companyName: string) {
    this.name = name;
    this.salary = salary;
    this.companyName = companyName;
  }

  // abstract methods
  public abstract work(): void;
  public abstract getSalary(): number;

  // non-abstract method (normal)
  public getCompanyName() {
    return this.companyName;
  }
}

الآن أنظر لكيف سيبدو إذا حولناه لـ Interface

interface Employee {
  name: string;
  salary: number;
  companyName: string;

  work(): void;
  getSalary(): number;
  getCompanyName(): string;
}

سأعرض لكم جدول يقارن أهم الاختلافات بين الـ Interface والـ Abstract Class
ثم سنوضح كل اختلاف ونفصصه

Interface Abstract Class
لا يخضع لمفهوم الـ Inheritance يخضع لمفهوم الـ Inheritance
الكلاسات الأخرى تستخدم implements
لكي تقوم ببناء كل ما يقدمه الـ Interface
الكلاسات الأخرى تستخدم extends
لكي تقوم بوراثته وتبني فقط الـ abstract method
لا يحتوي على Constructor يحتوي على Constructor
جميع دواله ومتغيراته تكون abstract member بشكل اجباري قد يملك abstract member واحدة على الأقل
وقد يملك دوال عادية لها implementation
كل دواله ومتغيراته تكون public بشكل اجباري يمكننا تخصيص أي Access Modifiers نريده
يمكن للكلاس الواحد أن يبني أكثر من Interface كل كلاس يستطيع أن يرث Abstract Class واحد فقط

أظنك تمتلك ألف سؤال الآن، لا تقلق سأحاول الإجابة على ما أستطيع منهم
الـ Interface ليس كلاس، عليك أن تضع هذا في الحسبان
بالتالي ما الذي سيترتب على هذا ؟ كل شيء تعرفه عن الكلاسات لن يتواجد في الـ Interface، مثل ...

الـ Interface يمكنك أن نفهمه بأنه مثل نموذج يمثل شكل عام أو هيكل لشيء ما خالي من التفاصيل
يحتوي على العناصر والخواص والدوال والأمور الاساسية التي يجب أن تتواجد في هذا الكيان
كأنك تكتب تفاصيل وخواص شيء ما على ورقة فمثلًا عندما تحاول وضع نموذج للسيارة فتبدأ بسؤال أنفسنا عن مما تتكون السيارة بشكل عام وما خواصها ؟

أريدك أن تعيد قراءة هذه النقاط وتركز معها
لكن سأحاول أن استفيض واشرح بعض هذه النقاط بشكل عملي

استخدام الـ Interface بشكل عملي

حسنًا لدينا هذا الـ interface الذي سيكون اسمه IEmployee
لاحظ أننه وضعنا I قبل الإسم لنعرف أن هذا interface يمكنك أن تعطيه أي اسم أو أن تسميه Employee ولن يحدث أي مشكله
لكن وضع I مجرد اسلوب تسمية متعارف عليه للـ interface لذا سنحاول اتباعه

interface IEmployee {
  // forced to be abstract properties
  name: string;
  salary: number;
  companyName: string;

  // forced to be abstract methods
  work(): void;
  getSalary(): number;
  getCompanyName(): string;
}

هكذا يكون شكل الـ interface لا constructor ولا access modifiers ولا حتى أي implementation لأي دالة

لنجعل كلاس ما يقوم بعمل implements له

class CareemEmployee implements IEmployee {}

وترى رسالة جميلة تقول لك الآتي

Class 'CareemEmployee' incorrectly implements interface 'IEmployee'. Type 'CareemEmployee' is missing the following properties from type 'IEmployee': name, salary, companyName, work, getSalary and getCompanyName.

تقول لك أنك نسيت أن تنشيء أو تبني الدوال والمتغيرات التالية name, salary, companyName, work, getSalary and getCompanyName

لاحظ أنه ينبهك أن تعرف كل شيء
السبب أن الـ Interface يقوم بجعل كل شيء يكون abstract
بالتالي الكلاس CareemEmployee يجب أن ينشيء كل المتغيرات والدوال عنده ويقوم بعمل implementation لها
فكما قلنا أن الـ Interface تخيله نموذج تام لوصف كيان ما
ويجب على كل الكلاسات عندما تقوم بعمل implements للـ interface فهي كهذا تكون مجبورة على ملئ وبناء هذا النموذج

class CareemEmployee implements IEmployee {
  public name: string;
  public salary: number;
  public companyName: string;

  constructor(name: string, salary: number, companyName: string) {
    this.name = name;
    this.salary = salary;
    this.companyName = companyName;
  }

  public work() {
    console.log(`${this.name} helps people get around!`);
  }
  public getSalary(): number {
    return this.salary * 2 - 2000;
  }
  public getCompanyName(): string {
    return this.companyName;
  }
}

هل لاحظت أنه في كلاس الـ CareemEmployee عرفنا المتغيرات name, salary, companyName وأيضًا أنشأنا Constructor
فالـ CareemEmployee لم يرثها من الـ Interface لانه كما قلنا لا يخضع لمفهوم الـ Inheritance
لانه كما ذكرنا مجرد نموذج توصيفي بحت كأنه كلام على ورقة
على عكس الـ Abstract Class حيث كان يتمتع بصفات الكلاس ويخضع لمفهوم الـ Inheritance

استخدام الـ Abstract Class مع الـ Interface

هل يمكننا الاستفادة من المفهومين واستخدامهما معًا
نعم! ، الكل وما الأمر أننا سنجعل الـ Abstract Class هو الذي سيقوم بعمل implements للـ Interface
ثم نجعل الكلاسات تتعامل مع الـ Abstract Class وترث منه

interface IEmployee {
  work(): void;
  getSalary(): number;
  getCompanyName(): string;
}

abstract class Employee implements IEmployee {
  protected name: string;
  protected salary: number;
  protected companyName: string;

  constructor(name: string, salary: number, companyName: string) {
    this.name = name;
    this.salary = salary;
    this.companyName = companyName;
  }

  // abstract methods
  public abstract work(): void;
  public abstract getSalary(): number;

  // non-abstract method
  public getCompanyName() {
    return this.companyName;
  }
}

هكذا ببساطة جعلت الـ Abstract Class هو الذي سيقوم بعمل implements للـ Interface
لاحظ أنني ازلت الـ Abstract Properties من الـ Interface ووضعتها في الـ Abstract Class

بسبب أن الـ Interface يجعل جميع عناصره تكون Public بشكل اجباري ولا يمكن تغيرها
لذا حذفت الـ name, salary, companyName من الـ Interface ونقلتها للـ Abstract Class وجعلتها protected
لأن هذا ما احتاجه، اذا كنت تريد عمل متغيرات وتكون public فلا بأس أن تبقيها في الـ interface
في غالب المشاريع التي ستتعامل معها ستجد أنك تستعمل الـ Interface لتعريف الدوال فقط ونادرًا ما سوف تعرف
على أي حال الأمر عائد لك ولنوعية المشروع الذي تعمل عليه والكلاسات التي تنشؤها

الآن كلاس الـ CareemEmployee سيقوم بوراثة الـ Employee بشكل اعتيادي

class CareemEmployee extends Employee {
  public getSalary(): number {
    return this.salary * 2 - 2000;
  }
  public work() {
    console.log(`${this.name} helps people get around!`);
  }
}

let e1 = new CareemEmployee('Ahmed', 3000, 'Careem');
let salary = e1.getSalary();

e1.work(); // OUTPUT: Ahmed helps people get around!
console.log(`Net salary is: ${salary}`); // OUTPUT: Net salary is 4000

تعليق ChatGPT

لاحظنا أن الكود السابق يحتوي على الـ Interface IEmployee والـ Abstract Class Employee الذي ينفذ هذه الـ Interface.
هذا الأمر يعطينا إمكانية توفير الكثير من الجهد في كتابة الكود، حيث أن الكثير من المتغيرات والدوال والصفات متشابهة بين العديد من الكائنات المختلفة.
علاوة على ذلك، يسهل عملية الصيانة والتحكم في الكود، حيث أنه بمجرد تغيير أي متغير أو دالة أساسية في الـ Interface سيتم تحديث كل الكلاسات التي تنفذ هذه الـ Interface.
علاوة على ذلك، تمكنا من إنشاء الـ Abstract Class Employee لتوفير بعض الميزات المشتركة بين العديد من الكلاسات مثل الـ Constructor والمتغيرات والدوال، بحيث يمكننا توفيرها مرة واحدة في الـ Abstract Class وترثها في الكلاسات الأخرى التي تستخدم هذه الصفات والدوال.
- ChatGPT

استخدام أكثر من Interface

تتذكرون عندما تحدثنا عن الـ Multiple Inheritance
وقلنا أن البني آدم مننا مثل عم أيمن
يكون متعدد الأدوار ولديه العديد من الصفات والمهام المختلفة
فيمكنه أن يكون على سبيل المثال، Employee و Father و Trader و Content Creator و Influencer إلخ

كان الأمر صعبًا مع الـ Multiple Inheritance لكن الآن مع الـ Interface الأمر سيكون ممكن مع اختلافات قليلًا
الـ Interface لا يملك Constructor لذا هكذا تخلصنا من اغلب المشاكل التي كانت تقابلنا في الـ Multiple inheritance

لنرى كيف سيكون الأمور مع الـ Interface
أولًا لننشيء Abstract Class بسيط لتعريف الأمور الاساسية والـ Constructor

abstract class Employee {
  protected name: string;
  protected salary: number;
  protected companyName: string;
  protected workArea: string;

  constructor(
    name: string,
    salary: number,
    companyName: string,
    workArea: string
  ) {
    this.name = name;
    this.salary = salary;
    this.companyName = companyName;
    this.workArea = workArea;
  }

  public getCompanyName() {
    return this.companyName;
  }
}

الآن لنجعل كلاس CareemEmployee يرث الـ Employee

class CareemEmployee extends Employee {}
let e1 = new CareemEmployee('Ahmed', 3000, 'Careem', 'Cairo');

console.log(e1.getCompanyName()); // OUTPUT: Cairo

الآن لنعرف الـ Interface التي سنستخدمها،وسيكون لدينا اثنين منهما وليس واحد

interface IEmployee {
  work(): void;
  getSalary(): number;
}

interface IDriver {
  getWorkArea(): string;
  hasLicense(): boolean;
}

الآن لكي نجعل CareemEmployee يقوم بعمل implements لكلا الـ Interface فقط نكتبهم ونفصلهم بفاصلة ,
بهذا الشكل

class CareemEmployee extends Employee implements IEmployee, IDriver {}

لاحظ أننا نستخدم extends و implements في آن واحد وأيضا نقوم بعمل implements لاثنين interface وليس واحد

بالطبع سنحصل على رسالة من كل من IEmployee و IDriver تنبهنا أن علينا عمل Implementation للـ Abstract Method الخاصة بهما

لذا لنقوم بهذا ونرى الشكل النهائي للكلاس

class CareemEmployee extends Employee implements IEmployee, IDriver {
  public getWorkArea(): string {
    return this.workArea;
  }
  public hasLicense(): boolean {
    // some checking logic
    return true;
  }
  public getSalary(): number {
    return this.salary * 2 - 2000;
  }
  public work() {
    console.log(`${this.name} helps people get around!`);
  }
}
let e1 = new CareemEmployee('Ahmed', 3000, 'Careem', 'Cairo');

e1.work(); // OUTPUT: Ahmed helps people get around!
console.log(e1.getCompanyName()); // OUTPUT: Careem
console.log(e1.getWorkArea()); // OUTPUT: Cairo
console.log(e1.hasLicense()); // OUTPUT: true
console.log(e1.getSalary()); // OUTPUT: 4000

الـ Interface كما ترى تساعدنا في وضع واجهه أو شكل عام لشكل يجب أن يهتم بها أي كلاس يقرر بناءه
ويمكن للكلاس الواحد أن يقوم ببناء وتنفيذ أكثر من Interface
الأمر مفيد جدًا على النطاق البعيد وعندما يكبر منك المشروع
فيمكنك تقسيم كل شيء لديك إلى Interface مسؤول عن وصف شيء ما
وأي كلاس فيما بعد يستطيع أن يقوم بتنفيذ مجموعة الـ Interface التي تناسبه

class SutraEmployee extends Employee implements IEmployee, ISeller {
  // Implement everything ...
}
class VodafoneEmployee extends Employee implements IEmployee, IDeveloper {
  // Implement everything ...
}
//...

الآن عم أيمن يستطيع أن يكون كل ما يريده

class Uncle
  extends Human
  implements Employee, Father, Trader, ContentCreator, Influencer {
  // implement everything ...
}

ملحوظة: عندما يكون لدينا اثنين interface لديهما نفس الدوال والمتغيرات ويقوم كلاس ما بعمل implements لهما فلن يحدث تكرا او تعارض بل سيتم تجاهل هذه التكرارات والتعامل معها كأنها ليست موجودة

interface IEmployee {
  name: string;
  work(): void;
}

interface IDriver {
  name: string;
  work(): void;
}

class CareemEmployee implements IEmployee, IDriver {
  name: string = 'Unknown';
  public work(): void {
    console.log(`${this.name} helps people get around!`);
  }
}

لاحظ أن IEmployee و IDriver لهما نفس الأشياء
وعندما قام CareemEmployee بعمل implements لها فلم يتم الاهتمام بالتكرار
إن فهمت فكرة الـ Interface فستفهم لما التكرار ليست مشكلة لانه ليس كلاس بل شكل توصيفي للأمور، فعندما نملك وصفيين متشابهين فلن يختلف تنفيذنا لأي منهما لانهم نفس الوصف

خاتمة

حسنًا وصلنا إلى نهاية هذه المقالة
ما أريدك أن تستوعبه هو فهم هذه المفاهيم مثل Abstraction و الـ Abstract Member و الفرق بين الـ Abstract Class و الـ Interface
حاولت إيضاح لك المفهوم نفسه لأن هذا هو المهم أن تستوعب المفهوم وليس أن تحفظه
عندما تفهمه وتستوعبه فيمكنك حينها أن توظفه كما تشاء وتجد استخدامات له في مشروعك

في لغات مثل Java و C# بدأت إنها تضيف إمكانية عمل implementation للدوال داخل الـ interface
لكي يكون هناك سلاسة أكبر في كتابة الكود وتساعد في تسريع الانتاجية لأن الهدف هو التسهيل على المبرمج ليخرج بمنتج أو خدمة

في معظم اللغات لا توجد الامكانية لعمل implementation داخل الـ interface لأنها قد تكون لغات تحب أن تلتزم بالقواعد والمبادئ
لكن هذا لم يمنع لغات مثل الـ Java أوالـ C# من الالتزام بتلك القواعد والمبادئ المتعارف عليها في الـ interface

لأن الـ Abstraction مجرد مفهوم والـ Abstract Class و Interface مجرد وسائل تحقق هذا المفهوم بطريقة ما
لهذا السبب علينا أن نفهم المفهوم بذاته ولا نتمسك بالوسائل او نقدسها ونفهم أن هذه مجرد طرق ووسائل تحقق المفهوم وممكن أن تختلف وتتغير بحسب اللغة

قد تجد لغات تضيف وسائل جديدة لتطبق مفاهيم مختلفة بطرق مختلفة
فمثلا في لغة PHP ستجدها أنها أضافت شيء مميز يدعى trait وهي وسيطة جديدة تحقق مفهوم الـ inheritance بشكل مختلف
ولغة مثل Dart أضافت شيء يدعى mixin نفس فكرة الـ trait

فهذه مجرد طرق مبتكرة ومختلفة تطبق مبدأ
وكل لغة تبدع كما تشاء لكي تستمر عجلة الانتاجية في الدوران
لو كل اللغات أصبحت تتطبق كل شيء بنفس الطريقة ستجد عُقم في كل شيء في الإنتاجية والصناعة والإبداع وتسهيل الكود وسلاسته

أرجوا أن أكون اوصلت هذا المفهوم بشكل جيد لك
أراكم في آخر مقالة لدينا وهو شرح مفهوم الـ Polymorphism