البرمجة كائنية التوجه في Dart

مقدمة إلى البرمجة كائنية التوجه والفئات في Dart

45 دقيقة الدرس 1 من 8

ما هي البرمجة كائنية التوجه؟

البرمجة كائنية التوجه (OOP) هي نموذج برمجي ينظم الكود حول الكائنات بدلاً من الدوال والمنطق. الكائن هو حزمة من البيانات المرتبطة (تسمى الخصائص أو الحقول) والسلوك (يسمى الطرق). فكر فيها مثل العالم الحقيقي: السيارة لها خصائص (اللون والسرعة ومستوى الوقود) وسلوكيات (التسارع والفرملة والانعطاف). OOP تتيح لك نمذجة كودك بنفس الطريقة.

Dart هي لغة كائنية التوجه بالكامل -- في الواقع كل شيء في Dart هو كائن. حتى الأرقام والنصوص والقيم المنطقية هي كائنات لها طرق. عندما تكتب 42.toString() أو 'hello'.toUpperCase() فأنت تستدعي طرقاً على كائنات.

لماذا OOP مهمة لـ Flutter

Flutter مبنية بالكامل على مفاهيم OOP. كل ودجة تنشئها هي فئة. كل شاشة هي كائن. فهم OOP ليس اختيارياً لـ Flutter -- إنه الأساس الذي بُني عليه كل شيء. إليك معاينة سريعة:

Flutter هي OOP

// كل ودجة Flutter هي فئة
class MyApp extends StatelessWidget {   // الوراثة
  const MyApp({super.key});             // المُنشئ

  @override                             // تعدد الأشكال
  Widget build(BuildContext context) {  // الطريقة
    return MaterialApp(home: HomeScreen());
  }
}
ملاحظة: لا تحتاج لفهم كود Flutter هذا الآن. الفكرة هي أن الفئات والمُنشئات والوراثة والطرق موجودة في كل مكان في Flutter. هذا الدليل التعليمي سيعلمك كل هذه المفاهيم خطوة بخطوة.

الركائز الأربع لـ OOP

OOP مبنية على أربعة مبادئ أساسية:

  • التغليف -- تجميع البيانات والطرق معاً والتحكم في الوصول للتفاصيل الداخلية.
  • التجريد -- إخفاء تفاصيل التنفيذ المعقدة وإظهار ما هو ضروري فقط.
  • الوراثة -- إنشاء فئات جديدة بناءً على فئات موجودة وإعادة استخدام الكود.
  • تعدد الأشكال -- كائنات من فئات مختلفة تستجيب لنفس الطريقة بطرق مختلفة.

سنستكشف كل واحدة من هذه خلال هذا الدليل التعليمي. الآن لنبدأ بأهم لبنة بناء: الفئات.

إنشاء أول فئة لك

الفئة هي مخطط لإنشاء الكائنات. تحدد ما هي الخصائص التي يمتلكها الكائن وماذا يمكنه أن يفعل. الكائن (يسمى أيضاً النسخة) هو شيء ملموس يُنشأ من ذلك المخطط.

تعريف فئة بسيطة

// الفئة هي المخطط
class Person {
  // الخصائص (البيانات)
  String name = '';
  int age = 0;

  // الطريقة (السلوك)
  void introduce() {
    print('مرحباً أنا $name وعمري $age سنة.');
  }
}

void main() {
  // إنشاء كائن (نسخة) من المخطط
  Person person1 = Person();
  person1.name = 'أحمد';
  person1.age = 25;
  person1.introduce();  // مرحباً أنا أحمد وعمري 25 سنة.

  // إنشاء كائن آخر من نفس المخطط
  Person person2 = Person();
  person2.name = 'سارة';
  person2.age = 30;
  person2.introduce();  // مرحباً أنا سارة وعمري 30 سنة.

  // كل كائن مستقل
  print(person1.name);  // أحمد (لم يتغير)
}
تشبيه: الفئة مثل قالب الكعك والكائنات هي قطع الكعك. قالب واحد (فئة) يمكنه صنع العديد من قطع الكعك (كائنات) وكل قطعة يمكن تزيينها بشكل مختلف (قيم خصائص مختلفة).

المُنشئات

تعيين الخصائص واحدة تلو الأخرى بعد إنشاء الكائن أمر ممل وعرضة للخطأ. المُنشئ هو طريقة خاصة تعمل تلقائياً عند إنشاء كائن مما يتيح لك تهيئة الخصائص فوراً.

المُنشئ الافتراضي

class Person {
  String name;
  int age;

  // المُنشئ -- نفس اسم الفئة
  Person(this.name, this.age);
  // 'this.name' هو اختصار Dart لـ: this.name = name

  void introduce() {
    print('مرحباً أنا $name وعمري $age سنة.');
  }
}

void main() {
  // الآن نمرر القيم مباشرة عند إنشاء الكائن
  Person person = Person('أحمد', 25);
  person.introduce();  // مرحباً أنا أحمد وعمري 25 سنة.

  // أنظف بكثير من تعيين كل خاصية يدوياً!
  Person person2 = Person('سارة', 30);
  person2.introduce();  // مرحباً أنا سارة وعمري 30 سنة.
}

المعاملات المسماة في المُنشئات

للفئات ذات الخصائص الكثيرة المعاملات المسماة تجعل الكود أكثر قابلية للقراءة. ضعها بين أقواس معقوفة {}:

المعاملات المسماة

class User {
  String username;
  String email;
  int age;
  bool isActive;

  // معاملات مسماة مع required وقيم افتراضية
  User({
    required this.username,
    required this.email,
    required this.age,
    this.isActive = true,  // قيمة افتراضية
  });

  void displayInfo() {
    print('المستخدم: $username ($email)، العمر: $age، نشط: $isActive');
  }
}

void main() {
  User user1 = User(
    username: 'ahmed_dev',
    email: 'ahmed@example.com',
    age: 25,
  );
  user1.displayInfo();
  // المستخدم: ahmed_dev (ahmed@example.com)، العمر: 25، نشط: true

  User user2 = User(
    username: 'sara_code',
    email: 'sara@example.com',
    age: 30,
    isActive: false,  // تجاوز القيمة الافتراضية
  );
  user2.displayInfo();
  // المستخدم: sara_code (sara@example.com)، العمر: 30، نشط: false
}
ملاحظة: في Flutter تقريباً كل ودجة تستخدم معاملات مسماة مع required. هذا النمط سيصبح طبيعياً عندما تبني تطبيقات Flutter. مثال: Text('مرحبا', style: TextStyle(fontSize: 20)).

الخصائص والطرق

الخصائص تحمل بيانات الكائن. الطرق تحدد سلوك الكائن. معاً تشكل كائناً كاملاً.

الخصائص والطرق في العمل

class BankAccount {
  String owner;
  double _balance;  // الشرطة السفلية تجعلها "خاصة"

  BankAccount({required this.owner, double initialBalance = 0})
      : _balance = initialBalance;

  // Getter -- وصول للقراءة فقط للبيانات الخاصة
  double get balance => _balance;

  // طريقة: إيداع المال
  void deposit(double amount) {
    if (amount <= 0) {
      print('خطأ: مبلغ الإيداع يجب أن يكون موجباً.');
      return;
    }
    _balance += amount;
    print('تم إيداع \$${amount.toStringAsFixed(2)}. الرصيد الجديد: \$${_balance.toStringAsFixed(2)}');
  }

  // طريقة: سحب المال
  bool withdraw(double amount) {
    if (amount <= 0) {
      print('خطأ: مبلغ السحب يجب أن يكون موجباً.');
      return false;
    }
    if (amount > _balance) {
      print('خطأ: رصيد غير كافٍ.');
      return false;
    }
    _balance -= amount;
    print('تم سحب \$${amount.toStringAsFixed(2)}. الرصيد الجديد: \$${_balance.toStringAsFixed(2)}');
    return true;
  }

  // طريقة: عرض معلومات الحساب
  void displayInfo() {
    print('صاحب الحساب: $owner، الرصيد: \$${_balance.toStringAsFixed(2)}');
  }
}

void main() {
  BankAccount account = BankAccount(owner: 'أحمد', initialBalance: 1000);
  account.displayInfo();     // صاحب الحساب: أحمد، الرصيد: $1000.00
  account.deposit(500);      // تم إيداع $500.00. الرصيد الجديد: $1500.00
  account.withdraw(200);     // تم سحب $200.00. الرصيد الجديد: $1300.00
  account.withdraw(2000);    // خطأ: رصيد غير كافٍ.
  print(account.balance);    // 1300.0 (باستخدام getter)
}
مهم: في Dart بادئة الشرطة السفلية (_) تجعل الخاصية أو الطريقة خاصة على مستوى المكتبة وليس خاصة على مستوى الفئة. هذا يعني أنها قابلة للوصول داخل نفس الملف لكنها مخفية من الملفات الأخرى. هذا نهج Dart في التغليف. سنغطي هذا بمزيد من التفصيل في درس لاحق.

الكلمة المفتاحية this

الكلمة المفتاحية this تشير إلى النسخة الحالية من الفئة. هي مفيدة عندما تتطابق أسماء المعاملات مع أسماء الخصائص أو عندما تحتاج إرجاع الكائن الحالي:

استخدام this

class Rectangle {
  double width;
  double height;

  // اختصار 'this' في المُنشئ
  Rectangle(this.width, this.height);

  double get area => width * height;
  double get perimeter => 2 * (width + height);

  // تسلسل الطرق باستخدام 'this'
  Rectangle scale(double factor) {
    width *= factor;
    height *= factor;
    return this;  // إرجاع نفس الكائن للتسلسل
  }

  void display() {
    print('المستطيل: ${width}x$height، المساحة: $area، المحيط: $perimeter');
  }
}

void main() {
  Rectangle rect = Rectangle(10, 5);
  rect.display();  // المستطيل: 10.0x5.0، المساحة: 50.0، المحيط: 30.0

  // تسلسل الطرق
  rect.scale(2).display();  // المستطيل: 20.0x10.0، المساحة: 200.0، المحيط: 60.0
}

كائنات متعددة من فئة واحدة

كل كائن يُنشأ من فئة مستقل تماماً. تغيير كائن لا يؤثر على آخر:

كائنات مستقلة

class Counter {
  String label;
  int _count = 0;

  Counter(this.label);

  int get count => _count;

  void increment() => _count++;
  void decrement() => _count--;
  void reset() => _count = 0;

  @override
  String toString() => '$label: $_count';
}

void main() {
  Counter likes = Counter('الإعجابات');
  Counter views = Counter('المشاهدات');

  likes.increment();
  likes.increment();
  likes.increment();

  views.increment();
  views.increment();
  views.increment();
  views.increment();
  views.increment();

  print(likes);  // الإعجابات: 3
  print(views);  // المشاهدات: 5

  // هما مستقلان تماماً
  likes.reset();
  print(likes);  // الإعجابات: 0
  print(views);  // المشاهدات: 5 (لم يتغير)
}

طريقة toString

كل فئة في Dart ترث من Object التي لها طريقة toString(). افتراضياً تُرجع شيئاً مثل Instance of 'ClassName'. تجاوزها لتقديم تمثيل نصي ذي معنى:

تجاوز toString

class Product {
  String name;
  double price;
  int quantity;

  Product({required this.name, required this.price, this.quantity = 0});

  double get totalValue => price * quantity;

  @override
  String toString() {
    return 'منتج($name, \$${price.toStringAsFixed(2)}, الكمية: $quantity)';
  }
}

void main() {
  Product laptop = Product(name: 'لابتوب', price: 999.99, quantity: 5);

  // بدون تجاوز toString: Instance of 'Product'
  // مع تجاوز toString:
  print(laptop);  // منتج(لابتوب, $999.99, الكمية: 5)

  // يعمل أيضاً في استيفاء النصوص
  print('العنصر: $laptop، الإجمالي: \$${laptop.totalValue}');
  // العنصر: منتج(لابتوب, $999.99, الكمية: 5)، الإجمالي: $4999.95
}

الفئة مقابل الكائن: التمييز الأساسي

تذكر:
الفئة هي مخطط/قالب -- تحدد الهيكل.
الكائن (أو النسخة) هو شيء ملموس يُنشأ من الفئة -- يحمل بيانات فعلية.
• يمكنك إنشاء العديد من الكائنات من فئة واحدة.
• كل كائن لديه نسخته الخاصة من الخصائص.
• جميع الكائنات تشترك في نفس الطرق (الكود) لكن كل منها يشغل الطرق على بياناته الخاصة.

مثال عملي: بناء مدير المهام

مدير المهام بالفئات

class Task {
  String title;
  String description;
  bool isCompleted;
  DateTime createdAt;

  Task({
    required this.title,
    this.description = '',
    this.isCompleted = false,
  }) : createdAt = DateTime.now();

  void complete() {
    isCompleted = true;
    print('المهمة "$title" تم تعليمها كمكتملة.');
  }

  void reopen() {
    isCompleted = false;
    print('المهمة "$title" أُعيد فتحها.');
  }

  @override
  String toString() {
    String status = isCompleted ? '[تم]' : '[مطلوب]';
    return '$status $title';
  }
}

class TaskManager {
  String projectName;
  List<Task> _tasks = [];

  TaskManager(this.projectName);

  // إضافة مهمة جديدة
  void addTask(String title, {String description = ''}) {
    _tasks.add(Task(title: title, description: description));
    print('تمت الإضافة: "$title"');
  }

  // الحصول على المهام المعلقة
  List<Task> get pendingTasks =>
      _tasks.where((t) => !t.isCompleted).toList();

  // الحصول على المهام المكتملة
  List<Task> get completedTasks =>
      _tasks.where((t) => t.isCompleted).toList();

  // إكمال مهمة بالفهرس
  void completeTask(int index) {
    if (index >= 0 && index < _tasks.length) {
      _tasks[index].complete();
    }
  }

  // عرض الملخص
  void showSummary() {
    print('\n--- $projectName ---');
    print('الإجمالي: ${_tasks.length} | معلقة: ${pendingTasks.length} | مكتملة: ${completedTasks.length}');
    for (var task in _tasks) {
      print('  $task');
    }
  }
}

void main() {
  TaskManager manager = TaskManager('تطبيق Flutter');

  manager.addTask('إعداد هيكل المشروع');
  manager.addTask('إنشاء شاشة تسجيل الدخول');
  manager.addTask('إضافة تكامل API');
  manager.addTask('كتابة اختبارات الوحدة');

  manager.completeTask(0);
  manager.completeTask(1);

  manager.showSummary();
  // --- تطبيق Flutter ---
  // الإجمالي: 4 | معلقة: 2 | مكتملة: 2
  //   [تم] إعداد هيكل المشروع
  //   [تم] إنشاء شاشة تسجيل الدخول
  //   [مطلوب] إضافة تكامل API
  //   [مطلوب] كتابة اختبارات الوحدة
}

تمرين عملي

افتح DartPad وابنِ التالي: (1) أنشئ فئة Student بخصائص: name و studentId وقائمة خاصة _grades (List<double>). (2) أضف مُنشئاً بمعاملات مسماة مطلوبة للاسم ورقم الطالب. (3) أضف طريقة addGrade(double grade) تقبل فقط الدرجات بين 0 و 100. (4) أضف getter averageGrade يحسب ويرجع المتوسط. (5) أضف getter letterGrade يرجع A/B/C/D/F بناءً على المتوسط. (6) تجاوز toString() لعرض معلومات الطالب. (7) أنشئ 3 كائنات Student وأضف درجات لكل منها واطبع معلوماتها.