الوراثة والكلمة المفتاحية extends
ما هي الوراثة؟
الوراثة هي إحدى الركائز الأربع لـ OOP. تتيح لك إنشاء فئة جديدة (تسمى فئة فرعية أو فئة ابن) ترث جميع الخصائص والطرق من فئة موجودة (تسمى فئة أساسية أو فئة أب). يمكن للفئة الفرعية بعدها إضافة ميزات جديدة أو تعديل السلوك الموروث. فكر فيها كقول: “الكلب هو حيوان” -- فئة الكلب ترث كل شيء من الحيوان وتضيف ميزات خاصة بالكلب.
الوراثة تعزز إعادة استخدام الكود. بدلاً من نسخ نفس الخصائص والطرق في فئات متعددة تضع الكود المشترك في فئة أب وتدع الأبناء يرثونه.
الوراثة الأساسية مع extends
استخدم الكلمة المفتاحية extends لإنشاء فئة فرعية. الابن يرث جميع الأعضاء غير الخاصة من الأب.
أول وراثة لك
// الفئة الأب (الأساسية)
class Animal {
String name;
int age;
Animal(this.name, this.age);
void eat() {
print('$name يأكل.');
}
void sleep() {
print('$name ينام.');
}
@override
String toString() => 'حيوان($name، العمر: $age)';
}
// الفئة الابن (الفرعية) -- ترث من Animal
class Dog extends Animal {
String breed;
Dog(String name, int age, this.breed) : super(name, age);
// طريقة جديدة خاصة بالكلب
void bark() {
print('$name يقول: هاو! هاو!');
}
}
// فئة ابن أخرى
class Cat extends Animal {
bool isIndoor;
Cat(String name, int age, {this.isIndoor = true}) : super(name, age);
void meow() {
print('$name يقول: مياو!');
}
}
void main() {
Dog dog = Dog('ريكس', 5, 'الراعي الألماني');
Cat cat = Cat('لونا', 3, isIndoor: true);
// الطرق الموروثة تعمل
dog.eat(); // ريكس يأكل.
dog.sleep(); // ريكس ينام.
dog.bark(); // ريكس يقول: هاو! هاو!
cat.eat(); // لونا يأكل.
cat.meow(); // لونا يقول: مياو!
// الخصائص الموروثة تعمل
print(dog.name); // ريكس
print(dog.age); // 5
print(dog.breed); // الراعي الألماني
}
class Dog extends Animal, Pet. لسلوك مشابه للوراثة المتعددة تستخدم Dart المزيجات (mixins) (تُغطى في درس لاحق).الكلمة المفتاحية super
الكلمة المفتاحية super تشير إلى الفئة الأب. تستخدمها لـ:
- استدعاء مُنشئ الأب:
super(args) - استدعاء طرق الأب:
super.methodName() - الوصول لخصائص الأب:
super.propertyName
استخدام super في المُنشئات
class Vehicle {
String make;
String model;
int year;
Vehicle({required this.make, required this.model, required this.year});
String get info => '$year $make $model';
}
class Car extends Vehicle {
int doors;
String fuelType;
// استدعاء مُنشئ الأب مع super
Car({
required super.make,
required super.model,
required super.year,
this.doors = 4,
this.fuelType = 'بنزين',
});
@override
String get info => '${super.info} ($doors أبواب، $fuelType)';
}
class ElectricCar extends Car {
double batteryCapacity; // كيلوواط ساعة
ElectricCar({
required super.make,
required super.model,
required super.year,
super.doors,
required this.batteryCapacity,
}) : super(fuelType: 'كهربائي');
double get rangeKm => batteryCapacity * 6;
@override
String get info => '${super.info} [${batteryCapacity}kWh، ~${rangeKm}km مدى]';
}
void main() {
var civic = Car(make: 'هوندا', model: 'سيفيك', year: 2024);
print(civic.info); // 2024 هوندا سيفيك (4 أبواب، بنزين)
var tesla = ElectricCar(
make: 'تسلا', model: 'موديل 3', year: 2024,
batteryCapacity: 75,
);
print(tesla.info);
// 2024 تسلا موديل 3 (4 أبواب، كهربائي) [75.0kWh، ~450.0km مدى]
}
super.paramName في المُنشئات (مثل required super.make) هي اختصار Dart 3. تمرر المعامل تلقائياً لمُنشئ الأب. قبل Dart 3 كان عليك كتابة: Car({required String make, ...}) : super(make: make, ...).تجاوز الطرق
تجاوز الطرق يتيح للفئة الفرعية تقديم تنفيذها الخاص لطريقة مُعرّفة في الفئة الأب. استخدم التعليق التوضيحي @override للإشارة لنيتك.
تجاوز الطرق
class Shape {
String color;
Shape(this.color);
double get area => 0;
void describe() {
print('شكل $color بمساحة ${area.toStringAsFixed(2)}');
}
}
class Circle extends Shape {
double radius;
Circle(String color, this.radius) : super(color);
@override
double get area => 3.14159 * radius * radius;
@override
void describe() {
print('دائرة $color بنصف قطر $radius ومساحة ${area.toStringAsFixed(2)}');
}
}
class Rectangle extends Shape {
double width;
double height;
Rectangle(String color, this.width, this.height) : super(color);
@override
double get area => width * height;
@override
void describe() {
// استدعاء طريقة الأب أولاً ثم إضافة المزيد
super.describe();
print(' الأبعاد: ${width} x $height');
}
}
void main() {
Circle circle = Circle('أزرق', 5);
Rectangle rect = Rectangle('أخضر', 10, 4);
circle.describe();
// دائرة أزرق بنصف قطر 5.0 ومساحة 78.54
rect.describe();
// شكل أخضر بمساحة 40.00
// الأبعاد: 10.0 x 4.0
}
@override اختياري تقنياً استخدمه دائماً. يخبر Dart (والمطورين الآخرين) أنك تتجاوز طريقة الأب عمداً. إذا أخطأت في كتابة اسم الطريقة سيحذرك المحلل أنك لا تتجاوز شيئاً -- مما يلتقط خطأ مبكراً.استدعاء طرق super
عندما تتجاوز طريقة لا يزال بإمكانك استدعاء نسخة الأب باستخدام super.methodName(). هذا مفيد عندما تريد توسيع (وليس استبدال) سلوك الأب.
توسيع سلوك الأب
class Logger {
void log(String message) {
print('[LOG] $message');
}
}
class TimestampLogger extends Logger {
@override
void log(String message) {
// إضافة طابع زمني ثم استدعاء الأب
String timestamp = DateTime.now().toIso8601String().substring(11, 19);
super.log('[$timestamp] $message');
}
}
class FileLogger extends TimestampLogger {
final List<String> _logHistory = [];
@override
void log(String message) {
_logHistory.add(message); // حفظ في السجل
super.log(message); // استدعاء الأب (الذي يضيف الطابع الزمني)
}
List<String> get history => List.unmodifiable(_logHistory);
}
void main() {
var logger = FileLogger();
logger.log('بدأ التطبيق');
logger.log('تم تسجيل الدخول');
print(logger.history); // [بدأ التطبيق، تم تسجيل الدخول]
}
سلاسل الوراثة
الوراثة يمكن أن تمتد لعدة مستويات. كل فئة ترث من أبيها الذي يرث من أبيه وهكذا حتى Object (جذر كل فئة في Dart).
وراثة متعددة المستويات
class LivingThing {
bool isAlive = true;
void breathe() => print('يتنفس...');
}
class Animal extends LivingThing {
String name;
Animal(this.name);
void move() => print('$name يتحرك.');
}
class Pet extends Animal {
String ownerName;
Pet(String name, this.ownerName) : super(name);
void greetOwner() => print('$name يحيي $ownerName!');
}
class Dog extends Pet {
String breed;
Dog(String name, String ownerName, this.breed) : super(name, ownerName);
void fetch() => print('$name يجلب الكرة!');
}
void main() {
var dog = Dog('ريكس', 'أحمد', 'لابرادور');
dog.breathe(); // يتنفس... (من LivingThing)
dog.move(); // ريكس يتحرك. (من Animal)
dog.greetOwner(); // ريكس يحيي أحمد! (من Pet)
dog.fetch(); // ريكس يجلب الكرة! (من Dog)
// Dog هو-Pet هو-Animal هو-LivingThing هو-Object
print(dog is Dog); // true
print(dog is Pet); // true
print(dog is Animal); // true
print(dog is LivingThing); // true
}
عوامل is و as
استخدم is للتحقق مما إذا كان كائن نسخة من فئة (بما في ذلك الفئات الأب). استخدم as لتحويل كائن لنوع محدد.
فحص النوع والتحويل
class Employee {
String name;
double salary;
Employee(this.name, this.salary);
void work() => print('$name يعمل.');
}
class Manager extends Employee {
List<Employee> team;
Manager(String name, double salary, {List<Employee>? team})
: team = team ?? [],
super(name, salary);
void conductMeeting() => print('$name يعقد اجتماعاً مع ${team.length} أشخاص.');
}
class Developer extends Employee {
String language;
Developer(String name, double salary, this.language) : super(name, salary);
void code() => print('$name يبرمج بـ $language.');
}
void processEmployee(Employee emp) {
emp.work();
// فحص النوع مع is
if (emp is Manager) {
emp.conductMeeting(); // Dart يحول تلقائياً بعد فحص is (تحويل ذكي)
} else if (emp is Developer) {
emp.code(); // تحويل ذكي -- لا حاجة لـ (emp as Developer).code()
}
}
void main() {
List<Employee> staff = [
Manager('سارة', 120000),
Developer('أحمد', 95000, 'Dart'),
Developer('خالد', 90000, 'Python'),
];
for (var emp in staff) {
processEmployee(emp);
print('---');
}
}
is يرقّي Dart المتغير تلقائياً للنوع المفحوص داخل تلك الكتلة. لذا بعد if (emp is Developer) يمكنك استدعاء emp.code() بدون تحويل صريح. هذا يسمى ترقية النوع ويزيل الحاجة لمعظم تحويلات as.تجاوز toString والمساواة
تجاوز toString و == و hashCode
class Coordinate {
final double x;
final double y;
const Coordinate(this.x, this.y);
@override
String toString() => 'إحداثية($x, $y)';
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is Coordinate && other.x == x && other.y == y;
}
@override
int get hashCode => x.hashCode ^ y.hashCode;
}
void main() {
var a = Coordinate(1, 2);
var b = Coordinate(1, 2);
var c = Coordinate(3, 4);
print(a); // إحداثية(1.0, 2.0)
print(a == b); // true (مساواة القيمة!)
print(a == c); // false
var set = {a, b, c};
print(set.length); // 2 (a و b "متساويان")
}
متى تستخدم الوراثة
الوراثة يجب أن تنمذج علاقة “هو-نوع من”. اسأل نفسك: “هل [الابن] نوع من [الأب]؟” إذا نعم الوراثة منطقية. إذا لا استخدم التركيب (وجود خاصية من نوع آخر) بدلاً من ذلك.
هو-نوع مقابل لديه
// جيد: علاقة هو-نوع -- وراثة
class Animal { void eat() {} }
class Dog extends Animal { void bark() {} }
// الكلب هو-نوع من الحيوان -- صحيح!
// سيء: ليست علاقة هو-نوع
class Engine { void start() {} }
// class Car extends Engine {} // السيارة هي-نوع من المحرك؟ لا!
// جيد: علاقة لديه -- تركيب
class Car {
Engine engine; // السيارة لديها محرك -- صحيح!
Car(this.engine);
void start() => engine.start();
}
void main() {
var engine = Engine();
var car = Car(engine);
car.start();
}
مثال عملي: نظام الإشعارات
مثال وراثة واقعي
class Notification {
final String title;
final String message;
final DateTime createdAt;
bool _isRead = false;
Notification({required this.title, required this.message})
: createdAt = DateTime.now();
bool get isRead => _isRead;
void markAsRead() => _isRead = true;
String get preview => message.length > 50
? '${message.substring(0, 50)}...'
: message;
@override
String toString() => '[${isRead ? "مقروء" : "جديد"}] $title: $preview';
}
class EmailNotification extends Notification {
final String senderEmail;
final bool hasAttachment;
EmailNotification({
required super.title,
required super.message,
required this.senderEmail,
this.hasAttachment = false,
});
@override
String toString() {
String attach = hasAttachment ? ' [مرفق]' : '';
return 'بريد من $senderEmail$attach - ${super.toString()}';
}
}
class PushNotification extends Notification {
final String appName;
final String? actionUrl;
PushNotification({
required super.title,
required super.message,
required this.appName,
this.actionUrl,
});
bool get hasAction => actionUrl != null;
@override
String toString() => 'إشعار [$appName] - ${super.toString()}';
}
void main() {
List<Notification> inbox = [
EmailNotification(
title: 'اجتماع غداً',
message: 'لا تنسَ الاجتماع الصباحي الساعة 9',
senderEmail: 'manager@company.com',
),
PushNotification(
title: 'تحديث جديد',
message: 'الإصدار 2.0 متاح الآن مع ميزات وتحسينات جديدة ومثيرة',
appName: 'تطبيقي',
actionUrl: '/update',
),
];
print('--- البريد (${inbox.length} إشعارات) ---');
for (var notif in inbox) {
print(notif);
}
inbox[0].markAsRead();
int unread = inbox.where((n) => !n.isRead).length;
print('\nغير مقروءة: $unread');
}
تمرين عملي
افتح DartPad وابنِ نظام مكتبة وسائط: (1) أنشئ فئة أساسية MediaItem بخصائص: title و creator و year و _rating (double خاصة 0-5). أضف setter للتقييم مع التحقق و getter ratingStars يرجع نجوماً مثل “★★★☆☆” وطريقة describe(). (2) أنشئ Movie extends MediaItem بـ durationMinutes و genre. تجاوز describe() لتشمل المدة. (3) أنشئ Song extends MediaItem بـ album و durationSeconds. أضف getter formattedDuration يرجع تنسيق “3:45”. (4) أنشئ Podcast extends MediaItem بـ episodeNumber و series. (5) أنشئ List<MediaItem> بأنواع مختلطة وكرر عليها واستدعِ describe() واستخدم فحوصات is لمخرجات خاصة بالنوع.