الكلاسات المجردة والواجهات في Dart: abstract و implements
في درس الكلاسات تعلمت الوراثة بـ extends. لكن أحياناً لا تريد كلاساً جاهزاً للاستخدام، بل تريد قالباً يفرض قواعد: «كل شكل هندسي يجب أن تكون له مساحة»، دون أن تحدد كيف تُحسب. هذا دور الكلاس المجرد (Abstract Class) والواجهة (Interface). وستقابلهما في Flutter كل يوم.
الكلاس المجرد: abstract
Section titled “الكلاس المجرد: abstract”تخيّل عقد عمل فيه بنود فارغة: «على الموظف أن يقدّم تقريراً أسبوعياً». العقد لا يكتب التقرير، لكنه يُلزم كل من يوقّعه بكتابته. الكلاس المجرد هو هذا العقد: يعلن عن توابع بدون جسم، ويترك تنفيذها للكلاسات التي ترث منه.
abstract class Shape { String get name; double area();
void describe() { print('$name مساحته ${area()}'); }}
class Rectangle extends Shape { final double width; final double height; Rectangle(this.width, this.height);
@override String get name => 'المستطيل';
@override double area() => width * height;}
class Circle extends Shape { final double radius; Circle(this.radius);
@override String get name => 'الدائرة';
@override double area() => 3.14 * radius * radius;}
void main() { List<Shape> shapes = [Rectangle(4, 5), Circle(10)]; for (var shape in shapes) { shape.describe(); }}المستطيل مساحته 20.0الدائرة مساحته 314.0لاحظ ثلاثة أشياء في Shape:
double area();تنتهي بفاصلة منقوطة بدون جسم. هذا تابع مجرد: كل ابن ملزم بكتابته.describeتابع عادي له جسم، وكل الأبناء يرثونه جاهزاً. والجميل أنه يستدعيarea()مع أنها لم تُكتب بعد فيShape!- لا يمكن إنشاء كائن من
Shapeنفسه. لو كتبتShape()فسيرفض المحرر برسالةAbstract classes can't be instantiated. وهذا منطقي: ما مساحة «شكل» لا نعرف نوعه؟
وإذا نسيت كتابة area في أحد الأبناء، فلن يعمل البرنامج أصلاً، وسيخبرك المحرر: Missing concrete implementation of 'Shape.area'. هذه أكبر فائدة: الخطأ يُكتشف قبل التشغيل.
الواجهة: implements
Section titled “الواجهة: implements”extends تعني: «أنا نوع من هذا الكلاس، وأرث كوده». أما implements فتعني: «أنا ألتزم بكل ما في هذا الكلاس من توابع، وسأكتبها كلها بنفسي». لا يُورَّث أي كود، بل الالتزام فقط.
الفائدة الكبرى أن الكلاس يرث من أب واحد فقط، لكنه يستطيع تطبيق عدة واجهات:
abstract interface class Playable { void play();}
abstract interface class Downloadable { void download();}
class Song implements Playable, Downloadable { final String title; Song(this.title);
@override void play() => print('تشغيل الأغنية: $title');
@override void download() => print('تنزيل: $title');}
class Game implements Playable { @override void play() => print('بدأت اللعبة!');}
void main() { List<Playable> items = [Song('نشيد الصباح'), Game()]; for (var item in items) { item.play(); } Song('نشيد الصباح').download();}تشغيل الأغنية: نشيد الصباحبدأت اللعبة!تنزيل: نشيد الصباحالأغنية واللعبة لا علاقة قرابة بينهما، لكن كلتيهما «يمكن تشغيلها». لذلك وضعناهما في قائمة واحدة من نوع Playable، واستدعينا play دون أن نسأل عن النوع الحقيقي.
الكلمتان abstract interface class معاً تعنيان: هذا عقد خالص، يُطبَّق بـ implements فقط ولا يُورَّث منه.
كل كلاس هو واجهة
Section titled “كل كلاس هو واجهة”في Dart لا تحتاج كلمة خاصة لتعريف واجهة: أي كلاس يصلح أن تكتب بعده implements. عندها تأخذ أسماء توابعه فقط، وتكتب أنت كل الأجسام:
class Duck { void swim() => print('البطة تسبح'); void fly() => print('البطة تطير');}
class RobotDuck implements Duck { @override void swim() => print('البطة الآلية تسبح بمحرك');
@override void fly() => print('البطة الآلية لا تطير!');}
void main() { Duck duck = RobotDuck(); duck.swim(); duck.fly();}البطة الآلية تسبح بمحركالبطة الآلية لا تطير!RobotDuck لم ترث سطراً واحداً من Duck، لكنها تُقبل في أي مكان يطلب Duck. هذه الحيلة مشهورة في الاختبارات: تستبدل كلاساً حقيقياً يتصل بالإنترنت بكلاس مزيف يعيد بيانات ثابتة.
extends أم implements؟
Section titled “extends أم implements؟”extends |
implements |
|
|---|---|---|
| المعنى | «أنا نوع من» | «ألتزم بعقد» |
| هل يُورَّث الكود؟ | نعم | لا، تكتب كل شيء |
| العدد المسموح | أب واحد فقط | أي عدد |
| استخدمه عندما | يوجد كود مشترك حقيقي | يوجد سلوك مشترك بين كلاسات مختلفة |
جرّب بنفسك
Section titled “جرّب بنفسك”اكتب كلاساً مجرداً Employee له اسم، وتابع مجرد double salary()، وتابع عادي printSalary يطبع الاسم والراتب. ثم اكتب ابنين: FullTime راتبه ثابت، و Hourly راتبه عدد الساعات مضروباً في أجر الساعة. جرّب موظفاً بدوام كامل راتبه 8000، وآخر عمل 60 ساعة بأجر 50:
هدى: 8000.0كريم: 3000.0أخطاء شائعة
Section titled “أخطاء شائعة”- محاولة إنشاء كائن من كلاس مجرد:
Shape()مرفوضة. أنشئ كائناً من أحد الأبناء. - نسيان تابع مطلوب: مع
implementsيجب كتابة كل التوابع، حتى التي لها جسم في الكلاس الأصلي. - استخدام
implementsوأنت تريد الكود: إذا أردت أن ترث التنفيذ الجاهز فاستخدمextends. - حذف
@override: الكود يعمل بدونها، لكنها تحميك: لو أخطأت في اسم التابع فسينبهك المحرر فوراً.
اختبر نفسك
Section titled “اختبر نفسك”سؤال
ما الذي يميّز التابع المجرد؟
التابع المجرد إعلان فقط ينتهي بفاصلة منقوطة، والأبناء غير المجردين ملزمون بتنفيذه.
سؤال
كم واجهة يستطيع الكلاس أن يطبّق بـ implements؟
الوراثة بـ extends من أب واحد، أما implements فتقبل أي عدد من الواجهات مفصولة بفواصل.
الخلاصة
Section titled “الخلاصة”- الكلاس المجرد
abstractقالب لا يُنشأ منه كائن، وقد يحتوي توابع بدون جسم. - الأبناء ملزمون بتنفيذ التوابع المجردة، والخطأ يظهر قبل التشغيل.
extendsترث الكود من أب واحد، وimplementsتلتزم بعقد دون وراثة كود.- أي كلاس في Dart يصلح واجهة، و
abstract interface classعقد خالص. - القائمة من نوع الواجهة تجمع كائنات من كلاسات مختلفة.