تخطَّ إلى المحتوى

الكلاسات المجردة والواجهات في Dart: abstract و implements

الدرس 9 من 14 · Dart

في درس الكلاسات تعلمت الوراثة بـ extends. لكن أحياناً لا تريد كلاساً جاهزاً للاستخدام، بل تريد قالباً يفرض قواعد: «كل شكل هندسي يجب أن تكون له مساحة»، دون أن تحدد كيف تُحسب. هذا دور الكلاس المجرد (Abstract Class) والواجهة (Interface). وستقابلهما في Flutter كل يوم.

تخيّل عقد عمل فيه بنود فارغة: «على الموظف أن يقدّم تقريراً أسبوعياً». العقد لا يكتب التقرير، لكنه يُلزم كل من يوقّعه بكتابته. الكلاس المجرد هو هذا العقد: يعلن عن توابع بدون جسم، ويترك تنفيذها للكلاسات التي ترث منه.

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'. هذه أكبر فائدة: الخطأ يُكتشف قبل التشغيل.

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 فقط ولا يُورَّث منه.

في 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
المعنى «أنا نوع من» «ألتزم بعقد»
هل يُورَّث الكود؟ نعم لا، تكتب كل شيء
العدد المسموح أب واحد فقط أي عدد
استخدمه عندما يوجد كود مشترك حقيقي يوجد سلوك مشترك بين كلاسات مختلفة

اكتب كلاساً مجرداً Employee له اسم، وتابع مجرد double salary()، وتابع عادي printSalary يطبع الاسم والراتب. ثم اكتب ابنين: FullTime راتبه ثابت، و Hourly راتبه عدد الساعات مضروباً في أجر الساعة. جرّب موظفاً بدوام كامل راتبه 8000، وآخر عمل 60 ساعة بأجر 50:

الناتج
هدى: 8000.0
كريم: 3000.0
  • محاولة إنشاء كائن من كلاس مجرد: Shape() مرفوضة. أنشئ كائناً من أحد الأبناء.
  • نسيان تابع مطلوب: مع implements يجب كتابة كل التوابع، حتى التي لها جسم في الكلاس الأصلي.
  • استخدام implements وأنت تريد الكود: إذا أردت أن ترث التنفيذ الجاهز فاستخدم extends.
  • حذف @override: الكود يعمل بدونها، لكنها تحميك: لو أخطأت في اسم التابع فسينبهك المحرر فوراً.

سؤال

ما الذي يميّز التابع المجرد؟

سؤال

كم واجهة يستطيع الكلاس أن يطبّق بـ implements؟

  • الكلاس المجرد abstract قالب لا يُنشأ منه كائن، وقد يحتوي توابع بدون جسم.
  • الأبناء ملزمون بتنفيذ التوابع المجردة، والخطأ يظهر قبل التشغيل.
  • extends ترث الكود من أب واحد، و implements تلتزم بعقد دون وراثة كود.
  • أي كلاس في Dart يصلح واجهة، و abstract interface class عقد خالص.
  • القائمة من نوع الواجهة تجمع كائنات من كلاسات مختلفة.