مفهوم الـ Abstraction في إخفاء التفاصيل
السلام عليكم ورحمة الله وبركاته
الفهرس
- المقدمة
- أصل المشكلة
- استخدام الـ Hierarchical inheritance
- هل إنشاء الدوال في كلاس Employee له فائدة ؟
- مفهوم الـ Abstraction
- إنشاء Object من كلاس به Abstract Member
- Abstract Class
- Interface
- الفرق بين الـ Interface والـ Abstract Class
- استخدام الـ Interface بشكل عملي
- استخدام الـ Abstract Class مع الـ Interface
- استخدام أكثر من Interface في كلاس واحد
- خاتمة
المقدمة
اليوم سنشرح ثالث مفهوم من مفاهيم الـ 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 Member
Abstract Property
Abstract Method
(Abstract Function
)
Abstract Class
Interface
فهنا ستظهر لنا 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