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

الفئات القابلة للاستدعاء وطريقة call()

40 دقيقة الدرس 15 من 24

ما الذي يجعل الفئة قابلة للاستدعاء؟

في Dart، تصبح الفئة قابلة للاستدعاء -- أي يمكنك استخدام نسخها مثل الدوال -- من خلال تنفيذ طريقة خاصة تسمى call(). عندما تعرّف طريقة call() على فئة، يسمح لك Dart باستدعاء نسخ تلك الفئة باستخدام صيغة استدعاء الدوال بالأقواس.

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

أول فئة قابلة للاستدعاء

class Greeter {
  final String greeting;

  Greeter(this.greeting);

  // طريقة call() تجعل هذه الفئة قابلة للاستدعاء
  String call(String name) {
    return '$greeting, $name!';
  }
}

void main() {
  final hello = Greeter('Hello');
  final marhaba = Greeter('مرحبا');

  // استدعاء النسخ مثل الدوال!
  print(hello('Alice'));     // Hello, Alice!
  print(marhaba('أحمد'));   // مرحبا, أحمد!

  // يمكنك أيضاً استدعاءها صراحة
  print(hello.call('Bob'));  // Hello, Bob!
}
مفهوم رئيسي: طريقة call() يمكن أن يكون لها أي نوع إرجاع وأي عدد من المعاملات ويمكن أن تكون حتى عامة. لا توجد واجهة لتنفيذها -- فقط عرّف طريقة باسم call و Dart يتولى الباقي.

الإغلاقات مقابل الفئات القابلة للاستدعاء

إغلاقات Dart (الدوال المجهولة) يمكنها التقاط متغيرات من نطاقها المحيط، مما يمنحها حالة. الفئات القابلة للاستدعاء تحقق نفس الشيء لكن مع مزيد من البنية وقابلية الاختبار وإعادة الاستخدام.

مقارنة الإغلاقات والفئات القابلة للاستدعاء

// النهج 1: إغلاق مع حالة
Function makeCounter() {
  int count = 0;
  return () {
    count++;
    return count;
  };
}

// النهج 2: فئة قابلة للاستدعاء مع حالة
class Counter {
  int _count = 0;

  int call() {
    _count++;
    return _count;
  }

  // ميزة: الفئات القابلة للاستدعاء يمكن أن تحتوي على طرق إضافية!
  void reset() => _count = 0;
  int get current => _count;

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

void main() {
  // نهج الإغلاق
  final closureCounter = makeCounter();
  print(closureCounter());  // 1
  print(closureCounter());  // 2
  // لا يمكن إعادة التعيين أو فحص الإغلاق...

  // نهج الفئة القابلة للاستدعاء
  final counter = Counter();
  print(counter());         // 1
  print(counter());         // 2
  print(counter.current);   // 2 -- فحص الحالة
  counter.reset();          // إعادة تعيين الحالة
  print(counter());         // 1 -- يبدأ من جديد
}
متى تختار أيهما: استخدم الإغلاقات للدوال البسيطة لمرة واحدة التي تحتاج حالة بسيطة. استخدم الفئات القابلة للاستدعاء عندما تحتاج: (1) طرق متعددة بجانب السلوك الرئيسي، (2) كائنات قابلة للاختبار والمحاكاة، (3) القدرة على فحص أو إعادة تعيين الحالة، أو (4) تنفيذ واجهات.

مثال عملي: المدققون

الفئات القابلة للاستدعاء تتألق كمدققين -- كل مدقق هو كائن بقواعد قابلة للتكوين تستدعيه على قيم الإدخال. هذا النمط يُستخدم بكثافة في التحقق من النماذج.

مدققو النماذج كفئات قابلة للاستدعاء

abstract class Validator {
  String? call(String? value);
}

class RequiredValidator extends Validator {
  final String message;

  RequiredValidator([this.message = 'هذا الحقل مطلوب']);

  @override
  String? call(String? value) {
    if (value == null || value.trim().isEmpty) {
      return message;
    }
    return null; // null تعني صالح
  }
}

class MinLengthValidator extends Validator {
  final int minLength;
  final String? customMessage;

  MinLengthValidator(this.minLength, [this.customMessage]);

  @override
  String? call(String? value) {
    if (value != null && value.length < minLength) {
      return customMessage ?? 'يجب أن يكون $minLength أحرف على الأقل';
    }
    return null;
  }
}

class EmailValidator extends Validator {
  @override
  String? call(String? value) {
    if (value == null || value.isEmpty) return null;
    final emailRegex = RegExp(r'^[a-zA-Z0-9.]+@[a-zA-Z0-9]+\.[a-zA-Z]+$');
    if (!emailRegex.hasMatch(value)) {
      return 'الرجاء إدخال عنوان بريد إلكتروني صالح';
    }
    return null;
  }
}

class PatternValidator extends Validator {
  final RegExp pattern;
  final String message;

  PatternValidator(this.pattern, this.message);

  @override
  String? call(String? value) {
    if (value != null && !pattern.hasMatch(value)) {
      return message;
    }
    return null;
  }
}

// تركيب عدة مدققين
class CompositeValidator extends Validator {
  final List<Validator> validators;

  CompositeValidator(this.validators);

  @override
  String? call(String? value) {
    for (final validator in validators) {
      final error = validator(value);
      if (error != null) return error;
    }
    return null;
  }
}

void main() {
  // إنشاء المدققين
  final required = RequiredValidator();
  final minLength = MinLengthValidator(8);
  final email = EmailValidator();

  // استخدامها مثل الدوال
  print(required(''));        // هذا الحقل مطلوب
  print(required('hello'));   // null (صالح)
  print(minLength('abc'));    // يجب أن يكون 8 أحرف على الأقل
  print(email('bad-email'));  // الرجاء إدخال عنوان بريد إلكتروني صالح
  print(email('a@b.com'));    // null (صالح)

  // تركيب مدققين لحقل كلمة المرور
  final passwordValidator = CompositeValidator([
    RequiredValidator('كلمة المرور مطلوبة'),
    MinLengthValidator(8, 'كلمة المرور يجب أن تكون 8 أحرف على الأقل'),
    PatternValidator(
      RegExp(r'[A-Z]'),
      'كلمة المرور يجب أن تحتوي على حرف كبير واحد على الأقل',
    ),
    PatternValidator(
      RegExp(r'[0-9]'),
      'كلمة المرور يجب أن تحتوي على رقم واحد على الأقل',
    ),
  ]);

  print(passwordValidator(''));          // كلمة المرور مطلوبة
  print(passwordValidator('abc'));        // كلمة المرور يجب أن تكون 8 أحرف على الأقل
  print(passwordValidator('abcdefgh'));   // كلمة المرور يجب أن تحتوي على حرف كبير واحد على الأقل
  print(passwordValidator('Abcdefg1'));   // null (صالح!)
}
رؤية النمط: CompositeValidator هو نفسه فئة قابلة للاستدعاء تحتوي على فئات قابلة للاستدعاء أخرى. هذا هو نمط التركيب -- التعامل مع الكائنات الفردية ومجموعات الكائنات بشكل موحد. يمكنك تداخل المدققين بعمق تعسفي.

مثال عملي: المنسقون

الفئات القابلة للاستدعاء تعمل بشكل جميل كمنسقين -- كائنات تحول البيانات إلى نص جاهز للعرض. كل منسق يمكن أن يكون له تكوين يؤثر على كيفية تنسيقه.

منسقو البيانات كفئات قابلة للاستدعاء

class CurrencyFormatter {
  final String symbol;
  final int decimalPlaces;
  final String thousandsSeparator;

  CurrencyFormatter({
    this.symbol = '\$',
    this.decimalPlaces = 2,
    this.thousandsSeparator = ',',
  });

  String call(num amount) {
    final fixed = amount.toStringAsFixed(decimalPlaces);
    final parts = fixed.split('.');
    final intPart = parts[0];
    final decPart = parts.length > 1 ? '.${parts[1]}' : '';

    // إضافة فواصل الآلاف
    final buffer = StringBuffer();
    for (int i = 0; i < intPart.length; i++) {
      if (i > 0 && (intPart.length - i) % 3 == 0) {
        buffer.write(thousandsSeparator);
      }
      buffer.write(intPart[i]);
    }

    return '$symbol${buffer.toString()}$decPart';
  }
}

class DateFormatter {
  final String format;

  DateFormatter([this.format = 'yyyy-MM-dd']);

  String call(DateTime date) {
    return format
        .replaceAll('yyyy', date.year.toString().padLeft(4, '0'))
        .replaceAll('MM', date.month.toString().padLeft(2, '0'))
        .replaceAll('dd', date.day.toString().padLeft(2, '0'))
        .replaceAll('HH', date.hour.toString().padLeft(2, '0'))
        .replaceAll('mm', date.minute.toString().padLeft(2, '0'));
  }
}

class Slugify {
  final String separator;

  Slugify([this.separator = '-']);

  String call(String text) {
    return text
        .toLowerCase()
        .trim()
        .replaceAll(RegExp(r'[^\w\s-]'), '')
        .replaceAll(RegExp(r'[\s_]+'), separator);
  }
}

void main() {
  final usd = CurrencyFormatter();
  final eur = CurrencyFormatter(symbol: '€', thousandsSeparator: '.');
  final formatDate = DateFormatter('dd/MM/yyyy');
  final slugify = Slugify();

  print(usd(1234567.89));   // $1,234,567.89
  print(eur(1234567.89));   // €1.234.567.89
  print(formatDate(DateTime(2024, 3, 15)));  // 15/03/2024
  print(slugify('Hello World! This is Dart'));  // hello-world-this-is-dart
}

مثال عملي: الوسيط ومعالجو الأحداث

في هندسة التطبيقات، الفئات القابلة للاستدعاء مثالية للوسيط (خطوط أنابيب المعالجة) ومعالجي الأحداث -- كل خطوة هي كائن قابل للتكوين تسلسله معاً.

خط أنابيب الوسيط مع فئات قابلة للاستدعاء

class Request {
  final String path;
  final Map<String, String> headers;
  bool isAuthenticated;
  String? userId;

  Request(this.path, {Map<String, String>? headers})
      : headers = headers ?? {},
        isAuthenticated = false;

  @override
  String toString() => 'Request($path, auth: $isAuthenticated, user: $userId)';
}

class Response {
  final int statusCode;
  final String body;
  const Response(this.statusCode, this.body);

  @override
  String toString() => 'Response($statusCode: $body)';
}

// كل وسيط هو فئة قابلة للاستدعاء
typedef NextHandler = Response Function(Request);

class LoggingMiddleware {
  Response call(Request request, NextHandler next) {
    print('[سجل] ${DateTime.now()}: ${request.path}');
    final response = next(request);
    print('[سجل] الاستجابة: ${response.statusCode}');
    return response;
  }
}

class AuthMiddleware {
  final Set<String> validTokens;

  AuthMiddleware(this.validTokens);

  Response call(Request request, NextHandler next) {
    final token = request.headers['Authorization'];
    if (token != null && validTokens.contains(token)) {
      request.isAuthenticated = true;
      request.userId = 'user_${token.hashCode}';
      return next(request);
    }
    return Response(401, 'غير مصرح');
  }
}

class RateLimiter {
  final int maxRequests;
  final Map<String, int> _requestCounts = {};

  RateLimiter(this.maxRequests);

  Response call(Request request, NextHandler next) {
    final ip = request.headers['X-Forwarded-For'] ?? 'unknown';
    _requestCounts[ip] = (_requestCounts[ip] ?? 0) + 1;
    if (_requestCounts[ip]! > maxRequests) {
      return Response(429, 'طلبات كثيرة جداً');
    }
    return next(request);
  }
}

void main() {
  final logger = LoggingMiddleware();
  final auth = AuthMiddleware({'token-abc', 'token-xyz'});
  final limiter = RateLimiter(100);

  // بناء خط أنابيب المعالج
  Response handler(Request req) => Response(200, 'مرحباً، ${req.userId}!');

  final request = Request(
    '/api/profile',
    headers: {'Authorization': 'token-abc', 'X-Forwarded-For': '192.168.1.1'},
  );

  // تسلسل الوسيط يدوياً
  final response = logger(
    request,
    (req) => auth(req, (req) => limiter(req, handler)),
  );

  print(response);  // Response(200: مرحباً, user_...)
}
نصيحة هندسية: الفئات القابلة للاستدعاء كوسيط تعطيك قابلية الاختبار (اختبار كل وسيط بمعزل)، وقابلية التركيب (مزج الوسيط ومطابقته)، وقابلية التكوين (كل وسيط يمكن أن يكون له إعداداته الخاصة مثل حدود المعدل والرموز المسموحة، إلخ).

الفئات القابلة للاستدعاء العامة

طريقة call() يمكن أن تستخدم الأنواع العامة، مما يجعل فئاتك القابلة للاستدعاء أكثر مرونة.

محول عام ومذكرة

// محول عام يحول نوعاً إلى آخر
class Transformer<TInput, TOutput> {
  final TOutput Function(TInput) _transform;

  Transformer(this._transform);

  TOutput call(TInput input) => _transform(input);
}

// قابل للاستدعاء مع تذكير -- يخزن النتائج للمدخلات المتكررة
class Memoize<TInput, TOutput> {
  final TOutput Function(TInput) _fn;
  final Map<TInput, TOutput> _cache = {};

  Memoize(this._fn);

  TOutput call(TInput input) {
    return _cache.putIfAbsent(input, () => _fn(input));
  }

  void clearCache() => _cache.clear();
  int get cacheSize => _cache.length;
}

void main() {
  // محول آمن النوع
  final toUpper = Transformer<String, String>((s) => s.toUpperCase());
  final toLength = Transformer<String, int>((s) => s.length);

  print(toUpper('hello'));    // HELLO
  print(toLength('hello'));   // 5

  // حساب مكلف مع تذكير
  final fibonacci = Memoize<int, int>((n) {
    if (n <= 1) return n;
    // ملاحظة: هذه النسخة البسيطة للعرض فقط
    int a = 0, b = 1;
    for (int i = 2; i <= n; i++) {
      final temp = a + b;
      a = b;
      b = temp;
    }
    return b;
  });

  print(fibonacci(10));           // 55
  print(fibonacci(10));           // 55 (من الذاكرة المؤقتة)
  print(fibonacci.cacheSize);    // 1
}
تنبيه: بينما الفئات القابلة للاستدعاء قوية، لا تفرط في استخدامها. إذا كانت دالة عادية كافية ولا تحتاج حالة أو تكوين أو طرق إضافية، استخدم دالة عادية. الفئات القابلة للاستدعاء تضيف تعقيداً يجب أن يبرره الفوائد التي تقدمها.

مثال واقعي: نظام الأحداث

إليك مثال كامل يوضح الفئات القابلة للاستدعاء كمعالجي أحداث في نظام نشر-اشتراك -- نمط موجود في العديد من تطبيقات Flutter.

نظام الأحداث مع معالجين قابلين للاستدعاء

class Event {
  final String type;
  final Map<String, dynamic> data;
  final DateTime timestamp;

  Event(this.type, [Map<String, dynamic>? data])
      : data = data ?? {},
        timestamp = DateTime.now();

  @override
  String toString() => 'Event($type, $data)';
}

// معالج أحداث قابل للاستدعاء
abstract class EventHandler {
  void call(Event event);
}

class LogHandler extends EventHandler {
  @override
  void call(Event event) {
    print('[${event.timestamp}] ${event.type}: ${event.data}');
  }
}

class ThrottledHandler extends EventHandler {
  final EventHandler _inner;
  final Duration cooldown;
  DateTime? _lastCall;

  ThrottledHandler(this._inner, this.cooldown);

  @override
  void call(Event event) {
    final now = DateTime.now();
    if (_lastCall == null || now.difference(_lastCall!) >= cooldown) {
      _lastCall = now;
      _inner(event); // استدعاء المعالج الداخلي
    }
  }
}

class FilteredHandler extends EventHandler {
  final EventHandler _inner;
  final bool Function(Event) _predicate;

  FilteredHandler(this._inner, this._predicate);

  @override
  void call(Event event) {
    if (_predicate(event)) {
      _inner(event);
    }
  }
}

class EventBus {
  final Map<String, List<EventHandler>> _handlers = {};

  void on(String eventType, EventHandler handler) {
    _handlers.putIfAbsent(eventType, () => []).add(handler);
  }

  void emit(String eventType, [Map<String, dynamic>? data]) {
    final event = Event(eventType, data);
    final handlers = _handlers[eventType] ?? [];
    for (final handler in handlers) {
      handler(event); // استدعاء كل معالج مثل دالة
    }
  }
}

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

  // تسجيل المعالجين
  bus.on('user.login', logger);
  bus.on('user.login', FilteredHandler(
    LogHandler(),
    (e) => e.data['role'] == 'admin',
  ));

  // إطلاق الأحداث
  bus.emit('user.login', {'name': 'Alice', 'role': 'user'});
  // فقط المسجل العادي يعمل

  bus.emit('user.login', {'name': 'Bob', 'role': 'admin'});
  // كلا المسجلين يعملان لتسجيل دخول المدير
}
ملخص: الفئات القابلة للاستدعاء تستخدم طريقة call() لجعل النسخ قابلة للاستدعاء بصيغة الدوال. هي أفضل من الإغلاقات عندما تحتاج إدارة الحالة أو طرق إضافية أو قابلية الاختبار أو الامتثال للواجهات. الاستخدامات الشائعة تشمل المدققين والمنسقين والوسيط والمحولات ومعالجي الأحداث. استخدمها عندما تحتاج الكائنات للتصرف مثل الدوال مع قدرات مضافة.