مفهوم الـ Abstraction في إخفاء التفاصيل
السلام عليكم ورحمة الله وبركاته
المقدمة
اليوم سنشرح ثالث مفهوم من مفاهيم الـ 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');
لنسأل بعض الأسئلة هنا:
- هل أحمد ومحمد ومحمود سيعملون بنفس الطريقة في الشركة ؟
- هل طريقة العمل المكتوبة في دالة
workايًا ما كانت يمكن ان تنطبق على أحمد ومحمد ومحمود ؟
هنا يتضح لنا شيء ما، وهو انه لا يمكننا وضع طريقة ثابتة وموحدة لكيفية عمل كل شخص، لأن كل شركة لها طريقتها للعمل
فهنا تكمن المشكلة ! فكيف نحلها ؟
استخدام الـ 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 لدينا عدة تطبيقات عملية له
Abstract MemberAbstract PropertyAbstract Method(Abstract Function)
Abstract ClassInterface
فهنا ستظهر لنا keyword جديدة في اللغة تسمى abstract
تساعدنا في تطبيق هذا المفهوم، وسنشرح ما تفعل بالتحديد
لكن أريدك أن تعرف هذه المسميات البسيطة ثم سنشرحها فيما بعد
- عندما نضع كلمة
abstractخلف أي دالة يصبح اسمهاAbstract Method - عندما نضع كلمة
abstractخلف أي متغير يصبح اسمهAbstract Property - عندما نضع كلمة
abstractخلف أي كلاس يصبح اسمهAbstract Class - تبقى الـ
Interfaceوهو مفهوم مميز قليلًا سنتحدث عنه عندما نصل اليه
ملحوظة: هناك قاعدة هامة عليك أن تعرفها وهو أي شيء يتحول إلى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، مثل ...
- لا وجود لـ
Contractorفي الـInterfaceبالتالي لا يمكن أن يكون لهobject - جميع دواله تكون
abstract methodبشكل اجباري بالتالي لا داعي لكتابة كلمةabstractقبل الدالة- لا يمتلك أي دالة عادية، فقط
abstract method - بالتالي لا يمكنك عمل
implementationلاي دالة
- لا يمتلك أي دالة عادية، فقط
- لا يمتلك
Access Modifiersوكل شيء يكونPublicبشكل اجباري، لذا لا داعي لكتابتها أيضًا - الـ
interfaceمجرد نموذج وصفي بحت مفرغ من أيimplementationبشكل كامل - لا يستخدم مفهوم الـ
inheritance- الكلاسات لا يمكنها أن ترثه
- الكلاسات تستخدم
implementsلاستخدامه وليسextends - يمكن للكلاس الواحد أن يبني أكثر من
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