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

أنماط التصميم: المراقب والاستراتيجية

55 دقيقة الدرس 19 من 24

أنماط التصميم السلوكية

في الدرس السابق، استكشفنا أنماط الإنشاء (المفرد، المصنع) التي تركز على كيفية إنشاء الكائنات. الآن ننتقل إلى الأنماط السلوكية التي تركز على كيفية تواصل الكائنات وتقاسم المسؤوليات. نمط المراقب يتعامل مع إشعارات واحد-إلى-كثير، بينما نمط الاستراتيجية يجعل الخوارزميات قابلة للتبادل وقت التشغيل.

نمط المراقب

نمط المراقب يُعرّف اعتمادية واحد-إلى-كثير بين الكائنات: عندما يتغير حالة كائن (الموضوع)، يتم إشعار جميع تابعيه (المراقبين) تلقائياً. هذا هو أساس أنظمة الأحداث والبرمجة التفاعلية و ChangeNotifier في Flutter.

نمط المراقب الأساسي

// العقد الذي يجب على كل مراقب تحقيقه
abstract class Observer<T> {
  void onUpdate(T data);
}

// الموضوع الذي يشترك فيه المراقبون
class Subject<T> {
  final List<Observer<T>> _observers = [];
  T? _state;

  T? get state => _state;

  void addObserver(Observer<T> observer) {
    if (!_observers.contains(observer)) {
      _observers.add(observer);
    }
  }

  void removeObserver(Observer<T> observer) {
    _observers.remove(observer);
  }

  void notify(T data) {
    _state = data;
    // أنشئ نسخة لتجنب مشاكل إذا أضاف/أزال المراقبون أثناء التكرار
    for (final observer in List.of(_observers)) {
      observer.onUpdate(data);
    }
  }

  int get observerCount => _observers.length;
}

// موضوع ملموس: مستشعر حرارة
class TemperatureSensor extends Subject<double> {
  void updateReading(double celsius) {
    print('المستشعر: قراءة جديدة = ${celsius.toStringAsFixed(1)} درجة مئوية');
    notify(celsius);
  }
}

// مراقبون ملموسون
class DisplayPanel implements Observer<double> {
  final String name;
  DisplayPanel(this.name);

  @override
  void onUpdate(double celsius) {
    print('  [$name] الحرارة: ${celsius.toStringAsFixed(1)} درجة مئوية');
  }
}

class AlarmSystem implements Observer<double> {
  final double threshold;
  AlarmSystem(this.threshold);

  @override
  void onUpdate(double celsius) {
    if (celsius > threshold) {
      print('  [إنذار] الحرارة ${celsius.toStringAsFixed(1)} تتجاوز الحد ${threshold.toStringAsFixed(1)}!');
    }
  }
}

void main() {
  final sensor = TemperatureSensor();
  final display = DisplayPanel('الشاشة الرئيسية');
  final alarm = AlarmSystem(30.0);

  sensor.addObserver(display);
  sensor.addObserver(alarm);

  sensor.updateReading(25.5);
  // المستشعر: قراءة جديدة = 25.5 درجة مئوية
  //   [الشاشة الرئيسية] الحرارة: 25.5 درجة مئوية

  sensor.updateReading(35.2);
  // المستشعر: قراءة جديدة = 35.2 درجة مئوية
  //   [الشاشة الرئيسية] الحرارة: 35.2 درجة مئوية
  //   [إنذار] الحرارة 35.2 تتجاوز الحد 30.0!
}
مفهوم أساسي: المستشعر لا يعرف ولا يهتم بما يفعله مراقبوه. الشاشة تعرض الحرارة؛ الإنذار يتحقق من الحد. يمكنك إضافة مسجل أو راسم بياني أو أي مراقب آخر دون تغيير كود المستشعر. هذا هو مبدأ المفتوح/المغلق في التطبيق.

نمط ناقل الأحداث

نسخة أكثر مرونة من المراقب هي ناقل الأحداث -- مركز مركزي حيث يمكن لأي جزء من تطبيقك نشر أحداث وأي جزء آخر يمكنه الاشتراك في أنواع أحداث محددة. هذا يفصل الناشرين والمشتركين تماماً.

ناقل أحداث آمن النوع

// نوع الحدث الأساسي
abstract class AppEvent {
  final DateTime timestamp;
  AppEvent() : timestamp = DateTime.now();
}

// أحداث ملموسة
class UserLoggedIn extends AppEvent {
  final String username;
  UserLoggedIn(this.username);
}

class OrderPlaced extends AppEvent {
  final String orderId;
  final double total;
  OrderPlaced(this.orderId, this.total);
}

class ErrorOccurred extends AppEvent {
  final String message;
  ErrorOccurred(this.message);
}

// اسم مستعار للنوع لمعالجات الأحداث
typedef EventHandler<T extends AppEvent> = void Function(T event);

// ناقل الأحداث
class EventBus {
  // خريطة من نوع الحدث إلى قائمة المعالجات
  final Map<Type, List<Function>> _handlers = {};

  // الاشتراك في نوع حدث محدد
  void on<T extends AppEvent>(EventHandler<T> handler) {
    _handlers.putIfAbsent(T, () => []).add(handler);
  }

  // إلغاء اشتراك معالج
  void off<T extends AppEvent>(EventHandler<T> handler) {
    _handlers[T]?.remove(handler);
  }

  // نشر حدث لجميع المشتركين من ذلك النوع
  void emit<T extends AppEvent>(T event) {
    final handlers = _handlers[T];
    if (handlers != null) {
      for (final handler in List.of(handlers)) {
        (handler as EventHandler<T>)(event);
      }
    }
  }

  // مسح جميع المعالجات
  void dispose() => _handlers.clear();
}

void main() {
  final bus = EventBus();

  // الاشتراك في أنواع أحداث مختلفة
  bus.on<UserLoggedIn>((event) {
    print('مرحباً، ${event.username}!');
  });

  bus.on<OrderPlaced>((event) {
    print('الطلب ${event.orderId}: \$${event.total.toStringAsFixed(2)}');
  });

  bus.on<ErrorOccurred>((event) {
    print('خطأ: ${event.message}');
  });

  // أي جزء من التطبيق يمكنه إصدار أحداث
  bus.emit(UserLoggedIn('alice'));
  bus.emit(OrderPlaced('ORD-001', 49.99));
  bus.emit(ErrorOccurred('فشل الدفع'));

  // مرحباً، alice!
  // الطلب ORD-001: $49.99
  // خطأ: فشل الدفع
}
نصيحة: في Flutter، فئة ChangeNotifier هي في الأساس تنفيذ للمراقب. عندما تستدعي notifyListeners()، جميع المستمعين المسجلين (العناصر) تُعاد بناؤها. مكتبات مثل bloc و riverpod تستخدم بنيات مشابهة مدفوعة بالأحداث داخلياً.

نمط الاستراتيجية

نمط الاستراتيجية يُعرّف عائلة من الخوارزميات، يُغلّف كل واحدة كفئة منفصلة، ويجعلها قابلة للتبادل. كود العميل يمكنه تبديل الخوارزميات وقت التشغيل دون تغيير منطقه الخاص. هذا مثالي عندما لديك طرق متعددة لفعل شيء وتريد الاختيار وقت التشغيل.

استراتيجية الدفع

// واجهة الاستراتيجية
abstract class PaymentStrategy {
  String get name;
  bool validate();
  Future<bool> processPayment(double amount);
}

// استراتيجيات ملموسة
class CreditCardPayment implements PaymentStrategy {
  final String cardNumber;
  final String expiryDate;

  CreditCardPayment(this.cardNumber, this.expiryDate);

  @override
  String get name => 'بطاقة ائتمان';

  @override
  bool validate() {
    return cardNumber.length == 16 && expiryDate.contains('/');
  }

  @override
  Future<bool> processPayment(double amount) async {
    print('معالجة \$$amount عبر بطاقة ائتمان تنتهي بـ ${cardNumber.substring(12)}');
    await Future.delayed(Duration(milliseconds: 100));
    return true;
  }
}

class PayPalPayment implements PaymentStrategy {
  final String email;

  PayPalPayment(this.email);

  @override
  String get name => 'PayPal';

  @override
  bool validate() => email.contains('@');

  @override
  Future<bool> processPayment(double amount) async {
    print('معالجة \$$amount عبر PayPal ($email)');
    await Future.delayed(Duration(milliseconds: 100));
    return true;
  }
}

class CryptoPayment implements PaymentStrategy {
  final String walletAddress;

  CryptoPayment(this.walletAddress);

  @override
  String get name => 'عملة مشفرة';

  @override
  bool validate() => walletAddress.startsWith('0x') && walletAddress.length == 42;

  @override
  Future<bool> processPayment(double amount) async {
    print('معالجة \$$amount عبر العملة المشفرة إلى ${walletAddress.substring(0, 10)}...');
    await Future.delayed(Duration(milliseconds: 200));
    return true;
  }
}

// فئة السياق التي تستخدم الاستراتيجية
class PaymentProcessor {
  PaymentStrategy? _strategy;

  void setStrategy(PaymentStrategy strategy) {
    _strategy = strategy;
  }

  Future<bool> checkout(double amount) async {
    if (_strategy == null) {
      print('لم يتم اختيار طريقة دفع!');
      return false;
    }

    print('--- الدفع: \$$amount ---');
    print('الطريقة: ${_strategy!.name}');

    if (!_strategy!.validate()) {
      print('فشل التحقق لـ ${_strategy!.name}');
      return false;
    }

    return await _strategy!.processPayment(amount);
  }
}

void main() async {
  final processor = PaymentProcessor();

  // المستخدم يختار بطاقة ائتمان
  processor.setStrategy(CreditCardPayment('4111111111111234', '12/25'));
  await processor.checkout(99.99);
  // --- الدفع: $99.99 ---
  // الطريقة: بطاقة ائتمان
  // معالجة $99.99 عبر بطاقة ائتمان تنتهي بـ 1234

  // المستخدم يبدل إلى PayPal
  processor.setStrategy(PayPalPayment('alice@example.com'));
  await processor.checkout(49.50);
  // --- الدفع: $49.50 ---
  // الطريقة: PayPal
  // معالجة $49.50 عبر PayPal (alice@example.com)
}
تحذير: لا تخلط بين الاستراتيجية والتفريع البسيط بـ if/else. إذا كان لديك جملة switch تختار السلوك بناءً على سلسلة نوع، وهذا المنطق يُستخدم في أماكن متعددة، فهذه رائحة كود يمكن لنمط الاستراتيجية إصلاحها. لكن إذا كان المنطق موجوداً في مكان واحد فقط وبسيط، فجملة switch مقبولة تماماً.

مثال استراتيجية الفرز

حالة استخدام كلاسيكية أخرى هي خوارزميات الفرز القابلة للتبادل:

الفرز بنمط الاستراتيجية

// واجهة الاستراتيجية للفرز
abstract class SortStrategy<T> {
  String get algorithmName;
  List<T> sort(List<T> items, int Function(T a, T b) compare);
}

class BubbleSort<T> implements SortStrategy<T> {
  @override
  String get algorithmName => 'فرز الفقاعة';

  @override
  List<T> sort(List<T> items, int Function(T a, T b) compare) {
    final result = List.of(items);
    for (int i = 0; i < result.length - 1; i++) {
      for (int j = 0; j < result.length - i - 1; j++) {
        if (compare(result[j], result[j + 1]) > 0) {
          final temp = result[j];
          result[j] = result[j + 1];
          result[j + 1] = temp;
        }
      }
    }
    return result;
  }
}

class QuickSort<T> implements SortStrategy<T> {
  @override
  String get algorithmName => 'الفرز السريع';

  @override
  List<T> sort(List<T> items, int Function(T a, T b) compare) {
    final result = List.of(items);
    _quickSort(result, 0, result.length - 1, compare);
    return result;
  }

  void _quickSort(List<T> arr, int low, int high, int Function(T, T) compare) {
    if (low < high) {
      int pivotIndex = _partition(arr, low, high, compare);
      _quickSort(arr, low, pivotIndex - 1, compare);
      _quickSort(arr, pivotIndex + 1, high, compare);
    }
  }

  int _partition(List<T> arr, int low, int high, int Function(T, T) compare) {
    T pivot = arr[high];
    int i = low - 1;
    for (int j = low; j < high; j++) {
      if (compare(arr[j], pivot) <= 0) {
        i++;
        final temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
      }
    }
    final temp = arr[i + 1];
    arr[i + 1] = arr[high];
    arr[high] = temp;
    return i + 1;
  }
}

// السياق
class DataProcessor<T> {
  SortStrategy<T> _strategy;

  DataProcessor(this._strategy);

  void setStrategy(SortStrategy<T> strategy) {
    _strategy = strategy;
  }

  List<T> process(List<T> data, int Function(T a, T b) compare) {
    print('الفرز بـ ${_strategy.algorithmName}...');
    final sorted = _strategy.sort(data, compare);
    return sorted;
  }
}

void main() {
  final data = [38, 27, 43, 3, 9, 82, 10];

  final processor = DataProcessor<int>(BubbleSort());
  print(processor.process(data, (a, b) => a.compareTo(b)));
  // الفرز بـ فرز الفقاعة...
  // [3, 9, 10, 27, 38, 43, 82]

  // التبديل إلى الفرز السريع لمجموعات بيانات أكبر
  processor.setStrategy(QuickSort());
  print(processor.process(data, (a, b) => a.compareTo(b)));
  // الفرز بـ الفرز السريع...
  // [3, 9, 10, 27, 38, 43, 82]
}

دمج المراقب والاستراتيجية

التطبيقات الحقيقية غالباً ما تدمج الأنماط. إليك نظام إشعارات يستخدم المراقب للتسليم والاستراتيجية للتنسيق:

نمط مدمج -- نظام الإشعارات

// الاستراتيجية: كيف يتم تنسيق الرسالة
abstract class MessageFormatter {
  String format(String title, String body, DateTime time);
}

class PlainTextFormatter implements MessageFormatter {
  @override
  String format(String title, String body, DateTime time) {
    return '$title\n$body\n-- ${time.toIso8601String()}';
  }
}

class HtmlFormatter implements MessageFormatter {
  @override
  String format(String title, String body, DateTime time) {
    return '<h1>$title</h1><p>$body</p><small>$time</small>';
  }
}

class JsonFormatter implements MessageFormatter {
  @override
  String format(String title, String body, DateTime time) {
    return '{"title":"$title","body":"$body","time":"$time"}';
  }
}

// المراقب: من يستقبل الإشعار
abstract class NotificationChannel {
  final String name;
  MessageFormatter formatter;

  NotificationChannel(this.name, this.formatter);

  void receive(String title, String body) {
    final formatted = formatter.format(title, body, DateTime.now());
    deliver(formatted);
  }

  void deliver(String formattedMessage);
}

class EmailChannel extends NotificationChannel {
  EmailChannel(MessageFormatter fmt) : super('بريد', fmt);

  @override
  void deliver(String msg) => print('  [بريد] $msg');
}

class SlackChannel extends NotificationChannel {
  SlackChannel(MessageFormatter fmt) : super('Slack', fmt);

  @override
  void deliver(String msg) => print('  [Slack] $msg');
}

// الموضوع: موزع الإشعارات
class NotificationHub {
  final List<NotificationChannel> _channels = [];

  void addChannel(NotificationChannel channel) => _channels.add(channel);
  void removeChannel(NotificationChannel channel) => _channels.remove(channel);

  void broadcast(String title, String body) {
    print('البث: "$title"');
    for (final channel in _channels) {
      channel.receive(title, body);
    }
  }
}

void main() {
  final hub = NotificationHub();

  // كل قناة يمكنها استخدام استراتيجية تنسيق مختلفة
  hub.addChannel(EmailChannel(HtmlFormatter()));
  hub.addChannel(SlackChannel(PlainTextFormatter()));

  hub.broadcast('اكتمل النشر', 'الإصدار 2.1.0 مباشر الآن!');
  // البث: "اكتمل النشر"
  //   [بريد] <h1>اكتمل النشر</h1><p>الإصدار 2.1.0 مباشر الآن!</p>...
  //   [Slack] اكتمل النشر\nالإصدار 2.1.0 مباشر الآن!\n-- ...
}
تآزر الأنماط: نمط المراقب يتعامل مع من يتم إشعاره (القنوات تشترك/تلغي الاشتراك). نمط الاستراتيجية يتعامل مع كيف يتم تنسيق الرسائل (كل قناة يمكنها استخدام منسق مختلف). دمجهما ينشئ نظاماً مرناً وقابلاً للتوسيع حيث يمكنك إضافة قنوات جديدة ومنسقات جديدة بشكل مستقل.
Flutter في العالم الحقيقي: في Flutter، ChangeNotifier + Provider هو في الأساس مراقب. AnimationController مع كائنات Curve مختلفة هو في الأساس استراتيجية. ListView.builder مع callback itemBuilder مخصص هو أيضاً استراتيجية (القائمة تفوض إنشاء العناصر لـ callback الخاص بك). تستخدم هذه الأنماط كل يوم في Flutter دون أن تدرك ذلك!
خطأ شائع: تجنب إنشاء نظام أحداث معقد بشكل مفرط عندما تكفي callbacks بسيطة. إذا كان لديك مشترك واحد فقط، فإن callback مباشر (VoidCallback أو ValueChanged<T>) أبسط وأوضح من إعداد مراقب كامل. استخدم نمط المراقب عندما يكون لديك حقاً مشتركون متعددون يتغيرون ديناميكياً وقت التشغيل.