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

المزيجات والكلمة المفتاحية with

40 دقيقة الدرس 6 من 8

المشكلة: الوراثة الأحادية محدودة

في الدروس السابقة تعلمت أن Dart تدعم الوراثة الأحادية فقط -- الفئة يمكنها أن ترث من أب واحد بالضبط. لكن ماذا يحدث عندما تحتاج فئة لمشاركة سلوك من عدة مصادر؟ مثلاً SmartPhone يحتاج ميزات الكاميرا وميزات GPS وميزات الهاتف. لا يمكنك كتابة class SmartPhone extends Camera, GPS, Phone.

هنا يأتي دور المزيجات. المزيج هو طريقة لإعادة استخدام كود فئة في تسلسلات هرمية متعددة بدون وراثة. فكر في المزيجات كـ “قدرات إضافية” يمكنك إرفاقها بأي فئة.

ما هو المزيج؟

المزيج هو بنية شبيهة بالفئة تعرّف طرقاً وخصائص لكنها مخصصة “للمزج” في فئات أخرى باستخدام الكلمة المفتاحية with. على عكس الفئة الأب لا يمكن إنشاء نسخة من المزيج مباشرة ولا ينشئ علاقة هو-نوع.

أول مزيج لك

// تعريف المزيجات بالكلمة المفتاحية 'mixin'
mixin Swimming {
  void swim() => print('يسبح في الماء!');
  int get swimSpeed => 10;
}

mixin Flying {
  void fly() => print('يطير في الهواء!');
  int get flySpeed => 50;
}

mixin Running {
  void run() => print('يركض على الأرض!');
  int get runSpeed => 30;
}

class Animal {
  String name;
  Animal(this.name);

  @override
  String toString() => name;
}

// استخدام المزيجات بالكلمة المفتاحية 'with'
class Duck extends Animal with Swimming, Flying, Running {
  Duck(String name) : super(name);
}

class Fish extends Animal with Swimming {
  Fish(String name) : super(name);
}

class Eagle extends Animal with Flying {
  Eagle(String name) : super(name);
}

void main() {
  var duck = Duck('دونالد');
  duck.swim();  // يسبح في الماء!
  duck.fly();   // يطير في الهواء!
  duck.run();   // يركض على الأرض!

  var fish = Fish('نيمو');
  fish.swim();  // يسبح في الماء!
  // fish.fly();  // خطأ! السمكة ليس لديها مزيج Flying

  var eagle = Eagle('سام');
  eagle.fly();  // يطير في الهواء!
}
الفرق الأساسي: مع الوراثة الكلب هو حيوان. مع المزيجات البطة يمكنها السباحة والطيران والركض. المزيجات تضيف قدرات وليس هوية. الفئة يمكنها استخدام مزيجات غير محدودة لكن ترث من أب واحد فقط.

صيغة المزيج

هناك طريقتان لتعريف مزيج:

تعريف المزيجات

// الطريقة 1: استخدام الكلمة المفتاحية mixin (مُوصى بها)
mixin Loggable {
  void log(String message) {
    print('[${DateTime.now().toIso8601String().substring(11, 19)}] $message');
  }
}

// الطريقة 2: استخدام mixin class (Dart 3+) -- يمكن أن يكون مزيجاً وفئة معاً
mixin class Identifiable {
  String get id => hashCode.toString();

  void printId() {
    print('المعرف: $id');
  }
}

// mixin class يمكن استخدامه كمزيج أو إنشاء نسخة منه
class User with Loggable, Identifiable {
  String name;
  User(this.name);
}

void main() {
  var user = User('أحمد');
  user.log('تم إنشاء المستخدم');  // [14:30:25] تم إنشاء المستخدم
  user.printId();                     // المعرف: 123456789
}

المزيجات مع الخصائص والحالة

المزيجات يمكن أن تحتوي على خصائص و getters و setters وتحافظ على الحالة -- تماماً مثل الفئات العادية:

مزيجات ذات حالة

mixin Cacheable {
  final Map<String, dynamic> _cache = {};

  void cacheValue(String key, dynamic value) {
    _cache[key] = value;
  }

  dynamic getCachedValue(String key) => _cache[key];
  bool isCached(String key) => _cache.containsKey(key);
  void clearCache() => _cache.clear();
  int get cacheSize => _cache.length;
}

mixin Timestamped {
  DateTime? _createdAt;
  DateTime? _updatedAt;

  DateTime get createdAt => _createdAt ?? DateTime.now();
  DateTime? get updatedAt => _updatedAt;

  void markCreated() => _createdAt = DateTime.now();
  void markUpdated() => _updatedAt = DateTime.now();
}

class UserProfile with Cacheable, Timestamped {
  String name;
  String email;

  UserProfile(this.name, this.email) {
    markCreated();
  }

  void updateEmail(String newEmail) {
    email = newEmail;
    markUpdated();
    cacheValue('lastEmail', newEmail);
  }
}

void main() {
  var profile = UserProfile('أحمد', 'ahmed@test.com');
  profile.updateEmail('new@test.com');
  print(profile.getCachedValue('lastEmail'));  // new@test.com
  print(profile.cacheSize);                      // 1
}

تقييد المزيجات بـ on

يمكنك تقييد مزيج بحيث يُستخدم فقط على فئات ترث من نوع محدد. استخدم الكلمة المفتاحية on:

تقييد المزيجات

class Widget {
  void render() => print('رسم الودجة...');
}

// هذا المزيج يمكن استخدامه فقط على فئات ترث من Widget
mixin Draggable on Widget {
  bool _isDragging = false;

  void startDrag() {
    _isDragging = true;
    print('بدأ السحب');
  }

  void endDrag() {
    _isDragging = false;
    print('توقف السحب');
    render();  // يمكن استدعاء طرق Widget بسبب 'on Widget'
  }
}

mixin Resizable on Widget {
  double _scale = 1.0;

  void resize(double factor) {
    _scale *= factor;
    print('تم تغيير الحجم إلى ${(_scale * 100).toInt()}٪');
    render();
  }
}

// يعمل -- Button يرث من Widget
class Button extends Widget with Draggable, Resizable {
  String label;
  Button(this.label);

  @override
  void render() => print('رسم الزر: $label');
}

void main() {
  var btn = Button('إرسال');
  btn.startDrag();    // بدأ السحب
  btn.endDrag();      // توقف السحب → رسم الزر: إرسال
  btn.resize(1.5);    // تم تغيير الحجم إلى 150٪ → رسم الزر: إرسال
}
مثال Flutter: Flutter تستخدم هذا النمط بكثرة. مثلاً SingleTickerProviderStateMixin يمكن استخدامه فقط on State -- يتطلب الوصول لطرق دورة حياة State. هذا يضمن أن المزيج يُطبق فقط حيث يكون منطقياً.

ترتيب المزيجات مهم (التخطيط)

عندما تعرّف عدة مزيجات نفس الطريقة آخر مزيج يفوز. Dart تطبق المزيجات من اليسار لليمين وكل واحد يطبق فوق السابق. هذا يسمى التخطيط الخطي.

ترتيب المزيجات

mixin A {
  String greet() => 'مرحباً من A';
}

mixin B {
  String greet() => 'مرحباً من B';
}

mixin C {
  String greet() => 'مرحباً من C';
}

class Test1 with A, B, C {}  // C يفوز (الأخير)
class Test2 with C, B, A {}  // A يفوز (الأخير)
class Test3 with A, C, B {}  // B يفوز (الأخير)

void main() {
  print(Test1().greet());  // مرحباً من C
  print(Test2().greet());  // مرحباً من A
  print(Test3().greet());  // مرحباً من B
}
كن حذراً: تعارضات ترتيب المزيجات يمكن أن تسبب أخطاء خفية. إذا عرّف مزيجان نفس الطريقة قد تحصل على سلوك غير متوقع حسب الترتيب. لتجنب هذا اجعل المزيجات مركزة على مسؤوليات مميزة وتجنب تداخل أسماء الطرق.

extends مقابل implements مقابل with

جدول ملخص:
extends -- وراثة فئة أب واحدة. تحصل على كل كودها. تجاوز ما تريد. تنشئ علاقة هو-نوع.
implements -- الوعد بتنفيذ جميع الأعضاء من فئة أو أكثر. لا تحصل على شيء مجاناً. تنشئ عقد يمكن-فعل.
with -- مزج القدرات من مزيج أو أكثر. تحصل على كل كودها. لا علاقة هو-نوع. تنشئ علاقة لديه-قدرة.

يمكنك دمج الثلاثة:
class MyWidget extends StatefulWidget with TickerProviderMixin implements Serializable

مثال عملي: نظام شخصيات لعبة

بناء نظام شخصيات بالمزيجات

// الفئة الأساسية
class Character {
  final String name;
  int _health;
  int _level;

  Character(this.name, {int health = 100, int level = 1})
      : _health = health, _level = level;

  int get health => _health;
  int get level => _level;
  bool get isAlive => _health > 0;

  void takeDamage(int amount) {
    _health = (_health - amount).clamp(0, 999);
    if (!isAlive) print('$name هُزم!');
  }

  void heal(int amount) {
    _health = (_health + amount).clamp(0, 100 + _level * 10);
  }

  @override
  String toString() => '$name [مس.$_level ص:$_health]';
}

// مزيجات القدرات
mixin MeleeAttack on Character {
  int get meleeDamage => 10 + level * 2;

  void meleeAttack(Character target) {
    print('$name يضرب $target بهجوم قريب بقوة $meleeDamage!');
    target.takeDamage(meleeDamage);
  }
}

mixin MagicCaster on Character {
  int _mana = 100;
  int get mana => _mana;

  void castSpell(Character target, {String spell = 'كرة نارية'}) {
    if (_mana < 20) {
      print('$name: مانا غير كافية!');
      return;
    }
    _mana -= 20;
    int damage = 15 + level * 3;
    print('$name يلقي $spell على $target بقوة $damage! (مانا: $_mana)');
    target.takeDamage(damage);
  }
}

mixin Healer on Character {
  int get healPower => 20 + level * 5;

  void healAlly(Character target) {
    print('$name يشفي $target بقوة $healPower!');
    target.heal(healPower);
  }
}

// دمج المزيجات لإنشاء فئات شخصيات فريدة
class Warrior extends Character with MeleeAttack {
  Warrior(String name) : super(name, health: 120);
}

class Mage extends Character with MagicCaster, Healer {
  Mage(String name) : super(name, health: 70);
}

class Paladin extends Character with MeleeAttack, MagicCaster, Healer {
  Paladin(String name) : super(name, health: 100);
}

void main() {
  var warrior = Warrior('ثور');
  var mage = Mage('غاندالف');

  warrior.meleeAttack(mage);
  mage.castSpell(warrior);
  mage.healAlly(mage);

  print(warrior);  // ثور [مس.1 ص:102]
  print(mage);     // غاندالف [مس.1 ص:83]
}

تمرين عملي

افتح DartPad وابنِ نظام أجهزة ذكية: (1) أنشئ فئة أساسية Device بخصائص name و brand و batteryLevel. (2) أنشئ مزيج WiFiCapable مع connect(ssid) و disconnect() و getter لـ isConnected. (3) أنشئ مزيج BluetoothCapable مع pair(deviceName) و unpair() وقائمة pairedDevices. (4) أنشئ مزيج CameraCapable (مقيد بـ Device باستخدام on) مع takePhoto() و recordVideo(seconds) تنقص البطارية. (5) أنشئ مزيج GPSCapable مع getLocation() يرجع نصاً. (6) ابنِ: SmartPhone extends Device with WiFiCapable, BluetoothCapable, CameraCapable, GPSCapable و SmartWatch extends Device with BluetoothCapable, GPSCapable و Laptop extends Device with WiFiCapable, BluetoothCapable, CameraCapable. (7) أنشئ جهازاً من كل نوع واعرض جميع قدراتها.