تعدد الأشكال
ما هو تعدد الأشكال؟
تعدد الأشكال يعني “أشكال كثيرة”. هو قدرة كائنات مختلفة على الاستجابة لنفس استدعاء الطريقة بطرق مختلفة. تستدعي .area على دائرة وتحصل على مساحة الدائرة؛ تستدعي .area على مستطيل وتحصل على مساحة المستطيل -- نفس اسم الطريقة وسلوك مختلف. المُستدعي لا يحتاج لمعرفة أي نوع شكل هو.
تعدد الأشكال أحد الركائز الأربع لـ OOP ويمكن القول أنه الأقوى. هو أساس بنيات الإضافات وحقن التبعيات ونظام ودجات Flutter بالكامل.
تعدد الأشكال عبر الوراثة
أكثر أشكال تعدد الأشكال شيوعاً هو عندما يحمل متغير من نوع الأب كائن فئة ابن. طريقة الابن المتجاوَزة تعمل وليس الأب:
تعدد الأشكال الأساسي
class Animal {
String name;
Animal(this.name);
String speak() => '$name يصدر صوتاً';
}
class Dog extends Animal {
Dog(String name) : super(name);
@override
String speak() => '$name يقول: هاو!';
}
class Cat extends Animal {
Cat(String name) : super(name);
@override
String speak() => '$name يقول: مياو!';
}
class Duck extends Animal {
Duck(String name) : super(name);
@override
String speak() => '$name يقول: كواك!';
}
void main() {
// جميعها مخزنة كنوع الأب Animal
List<Animal> animals = [
Dog('ريكس'),
Cat('لونا'),
Duck('دونالد'),
Dog('بادي'),
Cat('ويسكرز'),
];
// نفس استدعاء الطريقة سلوك مختلف
for (var animal in animals) {
print(animal.speak());
}
// ريكس يقول: هاو!
// لونا يقول: مياو!
// دونالد يقول: كواك!
// بادي يقول: هاو!
// ويسكرز يقول: مياو!
}
for لا تعرف ولا تهتم ما إذا كان كل حيوان كلباً أو قطة أو بطة. فقط تستدعي speak() والنسخة الصحيحة تعمل تلقائياً. هذا تعدد الأشكال -- اكتب حلقة واحدة تعمل مع أي نوع حيوان حتى الأنواع التي لم توجد بعد.لماذا تعدد الأشكال قوي
بدون تعدد الأشكال ستحتاج كود فحص أنواع قبيح في كل مكان:
بدون مقابل مع تعدد الأشكال
// بدون تعدد الأشكال -- هش ويجب تغييره لكل نوع جديد
void makeAnimalSpeak(dynamic animal) {
if (animal is Dog) {
print(animal.bark());
} else if (animal is Cat) {
print(animal.meow());
} else if (animal is Duck) {
print(animal.quack());
}
// أضف ببغاء؟ يجب تعديل هذه الدالة!
}
// مع تعدد الأشكال -- نظيف ويعمل مع أي حيوان للأبد
void makeAnimalSpeak(Animal animal) {
print(animal.speak()); // يعمل فقط. دائماً.
// أضف ببغاء يرث من Animal؟ يعمل تلقائياً!
}
تعدد الأشكال مع الفئات المجردة
الفئات المجردة هي أشهر طريقة لإعداد تعدد الأشكال. الفئة المجردة تحدد العقد والفئات الفرعية توفر التنفيذات:
تعدد أشكال الفئة المجردة
abstract class PaymentMethod {
String get name;
double get processingFee;
bool validate();
Future<bool> processPayment(double amount);
// طريقة ملموسة تستخدم طرقاً مجردة -- تعدد الأشكال في العمل
Future<String> pay(double amount) async {
if (!validate()) return 'فشل التحقق لـ $name';
double total = amount + processingFee;
bool success = await processPayment(total);
return success
? 'تم دفع \$${total.toStringAsFixed(2)} عبر $name'
: 'فشل الدفع عبر $name';
}
}
class CreditCard extends PaymentMethod {
final String cardNumber;
final String expiry;
CreditCard(this.cardNumber, this.expiry);
@override
String get name => 'بطاقة ائتمان';
@override
double get processingFee => 2.50;
@override
bool validate() => cardNumber.length == 16 && expiry.isNotEmpty;
@override
Future<bool> processPayment(double amount) async {
print('خصم \$${amount.toStringAsFixed(2)} من البطاقة المنتهية بـ ${cardNumber.substring(12)}...');
return true;
}
}
class PayPal extends PaymentMethod {
final String email;
PayPal(this.email);
@override
String get name => 'PayPal';
@override
double get processingFee => 1.50;
@override
bool validate() => email.contains('@');
@override
Future<bool> processPayment(double amount) async {
print('إرسال \$${amount.toStringAsFixed(2)} عبر PayPal إلى $email...');
return true;
}
}
// هذه الدالة تعمل مع أي طريقة دفع -- الآن وفي المستقبل
Future<void> checkout(double amount, PaymentMethod method) async {
String result = await method.pay(amount);
print(result);
}
void main() async {
var methods = <PaymentMethod>[
CreditCard('4111111111111111', '12/26'),
PayPal('ahmed@example.com'),
];
for (var method in methods) {
await checkout(99.99, method);
print('---');
}
}
نوع وقت التشغيل مقابل نوع وقت الترجمة
عندما تخزن كائن ابن في متغير من نوع الأب المتغير له نوعان:
نوعان في العمل
class Shape {
double get area => 0;
void describe() => print('أنا شكل');
}
class Circle extends Shape {
double radius;
Circle(this.radius);
@override
double get area => 3.14159 * radius * radius;
@override
void describe() => print('أنا دائرة بنصف قطر $radius');
// طريقة خاصة بالدائرة
double get circumference => 2 * 3.14159 * radius;
}
void main() {
Shape s = Circle(5); // نوع وقت الترجمة: Shape، نوع وقت التشغيل: Circle
// هذه تعمل -- مُعرّفة في Shape (نوع وقت الترجمة)
print(s.area); // 78.54 (تجاوز Circle يعمل!)
s.describe(); // أنا دائرة (تجاوز Circle يعمل!)
// هذه لا تعمل -- circumference ليست في Shape
// print(s.circumference); // خطأ ترجمة!
// للوصول لأعضاء Circle الخاصة استخدم فحص النوع
if (s is Circle) {
print(s.circumference); // 31.42 (تحويل ذكي بعد فحص is)
}
print(s.runtimeType); // Circle
print(s is Shape); // true
print(s is Circle); // true
}
s.area يرجع مساحة الدائرة رغم أن s مُعلن كـ Shape.تعدد الأشكال مع المجموعات
تعدد الأشكال يتألق عند معالجة مجموعات من أنواع مختلطة:
معالجة المجموعات المختلطة
abstract class Notification {
String get title;
String get body;
String get channel;
void send() {
print('[$channel] $title: $body');
}
}
class EmailNotification extends Notification {
final String to;
final String subject;
final String message;
EmailNotification({required this.to, required this.subject, required this.message});
@override
String get title => subject;
@override
String get body => message;
@override
String get channel => 'بريد';
@override
void send() {
print('إرسال بريد إلى $to...');
super.send();
}
}
class SmsNotification extends Notification {
final String phone;
final String text;
SmsNotification({required this.phone, required this.text});
@override
String get title => 'رسالة';
@override
String get body => text;
@override
String get channel => 'SMS';
@override
void send() {
print('إرسال رسالة إلى $phone...');
super.send();
}
}
void main() {
List<Notification> queue = [
EmailNotification(to: 'ahmed@test.com', subject: 'مرحبا!', message: 'شكراً لانضمامك.'),
SmsNotification(phone: '+971501234567', text: 'رمزك هو 1234'),
];
// حلقة واحدة تتعامل مع جميع أنواع الإشعارات
for (var notification in queue) {
notification.send();
print('');
}
// التصفية حسب النوع
var emails = queue.whereType<EmailNotification>().toList();
print('عدد الرسائل البريدية: ${emails.length}'); // 1
}
تعدد الأشكال في Flutter
Flutter مبنية بالكامل على تعدد الأشكال. كل ودجة هي Widget وكل تخطيط هو Widget وكل زر هو Widget:
كيف يستخدم Flutter تعدد الأشكال
// مثال مبسط شبيه بـ Flutter
abstract class Widget {
void render();
}
class Text extends Widget {
final String data;
Text(this.data);
@override
void render() => print('رسم نص: "$data"');
}
class Button extends Widget {
final String label;
Button(this.label);
@override
void render() => print('رسم زر: [$label]');
}
class Column extends Widget {
final List<Widget> children;
Column(this.children);
@override
void render() {
print('رسم عمود بـ ${children.length} أبناء:');
for (var child in children) {
child.render(); // تعدد الأشكال! كل ابن يُرسم بشكل مختلف
}
}
}
void main() {
var screen = Column([
Text('مرحباً بالعالم!'),
Button('اضغط هنا'),
Text('مرحباً في Flutter'),
]);
screen.render();
}
Column(children: [Text('مرحبا'), ElevatedButton(...), Image(...)]) يعمل لأنهم جميعاً يرثون من Widget. العمود لا يعرف ما الودجات المحددة التي يحتويها -- فقط يستدعي build() على كل واحدة والودجة الصحيحة تُرسم. هذا تعدد الأشكال يجعل Flutter ممكنة.مثال عملي: مولد التقارير
نظام تقارير متعدد الأشكال
abstract class ReportSection {
String get title;
String generate();
@override
String toString() => '=== $title ===\n${generate()}';
}
class TextSection extends ReportSection {
@override
final String title;
final String content;
TextSection(this.title, this.content);
@override
String generate() => content;
}
class TableSection extends ReportSection {
@override
final String title;
final List<String> headers;
final List<List<String>> rows;
TableSection(this.title, this.headers, this.rows);
@override
String generate() {
String header = headers.join(' | ');
String body = rows.map((r) => r.join(' | ')).join('\n');
return '$header\n$body';
}
}
class SummarySection extends ReportSection {
@override
final String title;
final Map<String, dynamic> stats;
SummarySection(this.title, this.stats);
@override
String generate() =>
stats.entries.map((e) => '${e.key}: ${e.value}').join('\n');
}
class Report {
final String name;
final List<ReportSection> sections;
Report(this.name, this.sections);
void printReport() {
print('\n*** $name ***\n');
for (var section in sections) {
print(section); // تعدد الأشكال! كل قسم يُولّد بشكل مختلف
print('');
}
}
}
void main() {
var report = Report('تقرير المبيعات الشهري', [
TextSection('نظرة عامة', 'أداء المبيعات لمارس 2024 تجاوز الأهداف بنسبة 15٪.'),
TableSection('أفضل المنتجات', ['المنتج', 'الوحدات', 'الإيرادات'], [
['منتج أ', '150', '4500\$'],
['منتج ب', '89', '2670\$'],
]),
SummarySection('مقاييس رئيسية', {
'إجمالي الإيرادات': '7170\$',
'إجمالي الوحدات': 239,
'النمو': '+15٪',
}),
]);
report.printReport();
}
تمرين عملي
افتح DartPad وابنِ حاسبة أشكال: (1) أنشئ فئة مجردة Shape مع getters مجردة area و perimeter و getter لـ name وطريقة ملموسة describe() تطبع كل المعلومات. (2) أنشئ Circle و Rectangle و Triangle (باستخدام صيغة هيرون) و Square extends Rectangle. (3) أنشئ دالة printShapeReport(List<Shape> shapes) تطبع وصف كل شكل وتجد الأكبر بالمساحة والأصغر بالمحيط وتحسب المساحة الإجمالية. (4) أنشئ List<Shape> بـ 6 أشكال مختلطة على الأقل ومررها للدالة. (5) استخدم whereType<Circle>() لتصفية الدوائر فقط وطباعة عددها.