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

طرق التوسيع

45 دقيقة الدرس 12 من 24

ما هي طرق التوسيع؟

هل تمنيت يوماً أن تضيف طريقة جديدة لـ String أو int أو List دون تعديل الفئة الأصلية؟ طرق التوسيع تتيح لك ذلك بالضبط. تسمح لك بإضافة وظائف جديدة لأنواع موجودة -- بما في ذلك الأنواع المدمجة وأنواع مكتبات الطرف الثالث -- دون إنشاء فئات فرعية أو تعديل الكود الأصلي.

الصيغة هي: extension ExtensionName on TargetType { ... }. بمجرد تعريفها يمكنك استدعاء طرق التوسيع على أي نسخة من النوع المستهدف كما لو كانت طرقاً مدمجة.

توسيعك الأول

// إضافة طرق لنوع String المدمج
extension StringExtras on String {
  // تحقق مما إذا كانت السلسلة بريد إلكتروني صالح
  bool get isEmail {
    return RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(this);
  }

  // تكبير الحرف الأول
  String get capitalized {
    if (isEmpty) return this;
    return '${this[0].toUpperCase()}${substring(1)}';
  }

  // اقتطاع مع علامة حذف
  String truncate(int maxLength) {
    if (length <= maxLength) return this;
    return '${substring(0, maxLength)}...';
  }

  // عد الكلمات
  int get wordCount => trim().isEmpty ? 0 : trim().split(RegExp(r'\s+')).length;
}

void main() {
  // استخدم طرق التوسيع كأنها مدمجة
  print('hello world'.capitalized);           // Hello world
  print('test@email.com'.isEmail);              // true
  print('not-an-email'.isEmail);                // false
  print('A very long sentence here'.truncate(10)); // A very lon...
  print('Hello beautiful world'.wordCount);     // 3
  print(''.wordCount);                           // 0
}
مفهوم أساسي: داخل التوسيع يشير this إلى النسخة التي يُستدعى عليها الطريقة. لذا في 'hello'.capitalized يكون this هو السلسلة 'hello'. التوسيعات يمكنها إضافة طرق ومحصّلات ومعيّنات وعوامل -- لكن ليس حقول نسخة (حالة). هي تماماً لإضافة السلوك.

توسيع الأنواع العددية

التوسيعات على الأنواع العددية مفيدة بشكل لا يصدق لجعل كودك أكثر قراءة وتعبيراً. بدلاً من كتابة دوال مساعدة يمكنك سلسلة الطرق مباشرة على الأرقام.

توسيعات عددية

extension IntExtras on int {
  // مساعدات Duration -- تجعل إنشاء Duration قابلاً للقراءة
  Duration get seconds => Duration(seconds: this);
  Duration get minutes => Duration(minutes: this);
  Duration get hours => Duration(hours: this);
  Duration get days => Duration(days: this);
  Duration get milliseconds => Duration(milliseconds: this);

  // تحقق من الخصائص
  bool get isEven => this % 2 == 0;
  bool get isPrime {
    if (this < 2) return false;
    for (int i = 2; i * i <= this; i++) {
      if (this % i == 0) return false;
    }
    return true;
  }

  // تكرار إجراء
  void times(void Function(int) action) {
    for (int i = 0; i < this; i++) {
      action(i);
    }
  }

  // توليد نطاق
  List<int> to(int end) =>
      [for (var i = this; i <= end; i++) i];

  // تنسيق مع حشو
  String padded(int width) => toString().padLeft(width, '0');
}

extension DoubleExtras on double {
  // تنسيق العملة
  String toCurrency({String symbol = '\$'}) =>
      '$symbol${toStringAsFixed(2)}';

  // تنسيق النسبة المئوية
  String toPercent({int decimals = 1}) =>
      '${(this * 100).toStringAsFixed(decimals)}%';

  // تقييد بين 0 و 1
  double get normalized => clamp(0.0, 1.0);
}

void main() {
  // مساعدات Duration
  var timeout = 5.seconds;
  var delay = 300.milliseconds;
  print(timeout);  // 0:00:05.000000
  print(delay);    // 0:00:00.300000

  // فحوصات الأرقام
  print(7.isPrime);   // true
  print(10.isPrime);  // false

  // تكرار
  3.times((i) => print('Iteration $i'));
  // Iteration 0
  // Iteration 1
  // Iteration 2

  // نطاق
  print(1.to(5));  // [1, 2, 3, 4, 5]

  // تنسيق
  print(42.padded(5));          // 00042
  print(29.99.toCurrency());    // $29.99
  print(0.856.toPercent());     // 85.6%
}
نصيحة: نمط توسيع Duration (5.seconds و 300.milliseconds) يُستخدم في العديد من حزم Flutter الشائعة. يجعل الكود أكثر قراءة بشكل كبير: قارن await Future.delayed(5.seconds) مع await Future.delayed(Duration(seconds: 5)). هذا التوسيع الواحد يمكنه تحسين القراءة عبر قاعدة كودك بالكامل.

التوسيعات المسماة والرؤية

جميع التوسيعات يجب أن تكون مسماة. التوسيعات المسماة يمكن استيرادها بشكل انتقائي مما يتجنب التعارضات عندما يضيف توسيعان نفس الطريقة لنفس النوع. يمكنك أيضاً جعل التوسيعات خاصة بملف ببادئة الاسم بشرطة سفلية.

التوسيعات المسماة والتحكم بالاستيراد

// ملف: string_extensions.dart
extension StringValidators on String {
  bool get isNumeric => RegExp(r'^\d+$').hasMatch(this);
  bool get isAlpha => RegExp(r'^[a-zA-Z]+$').hasMatch(this);
  bool get isAlphaNumeric => RegExp(r'^[a-zA-Z0-9]+$').hasMatch(this);
}

extension StringFormatters on String {
  String get snakeCase =>
      replaceAllMapped(RegExp(r'[A-Z]'), (m) => '_${m[0]!.toLowerCase()}')
          .replaceFirst(RegExp(r'^_'), '');

  String get camelCase {
    var words = split(RegExp(r'[_\s-]+'));
    return words.first.toLowerCase() +
        words.skip(1).map((w) => w.capitalized).join();
  }

  String get capitalized {
    if (isEmpty) return this;
    return '${this[0].toUpperCase()}${substring(1).toLowerCase()}';
  }
}

// توسيع خاص -- مرئي فقط في هذا الملف
extension _InternalHelpers on String {
  String get _reversed => split('').reversed.join();
}

// استيراد انتقائي في ملف آخر:
// import 'string_extensions.dart' show StringValidators;
// -- فقط طرق StringValidators متاحة وليس StringFormatters

void main() {
  // StringValidators
  print('12345'.isNumeric);       // true
  print('hello'.isAlpha);          // true
  print('abc123'.isAlphaNumeric);  // true

  // StringFormatters
  print('myVariableName'.snakeCase);  // my_variable_name
  print('hello_world'.camelCase);     // helloWorld
  print('hello'.capitalized);         // Hello
}
تحذير: إذا عرّف توسيعان نفس الطريقة على نفس النوع تحصل على خطأ تجميع عند استدعاء تلك الطريقة. لحل هذا: (1) استخدم show/hide في الاستيرادات لاختيار التوسيع المطلوب أو (2) استدعِ طريقة التوسيع صراحة: StringValidators('test').isEmail. أعطِ دائماً توسيعاتك أسماءً وصفية لجعل الاستيراد الانتقائي واضحاً.

التوسيعات المعممة

التوسيعات يمكن أن تكون معممة مضيفة طرقاً لأنواع معممة. هذا قوي بشكل خاص لتوسيع المجموعات مثل List<T> و Map<K, V> و Iterable<T>.

توسيعات القائمة المعممة

extension ListExtras<T> on List<T> {
  // الحصول على عنصر عشوائي
  T get random => this[DateTime.now().microsecond % length];

  // تقسيم القائمة إلى أجزاء بحجم معين
  List<List<T>> chunked(int size) {
    var chunks = <List<T>>[];
    for (var i = 0; i < length; i += size) {
      var end = (i + size < length) ? i + size : length;
      chunks.add(sublist(i, end));
    }
    return chunks;
  }

  // الحصول على عناصر فريدة (مع الحفاظ على الترتيب)
  List<T> get unique {
    var seen = <T>{};
    return where((item) => seen.add(item)).toList();
  }

  // وصول آمن للعناصر -- يُرجع null بدلاً من الرمي
  T? elementAtOrNull(int index) {
    if (index < 0 || index >= length) return null;
    return this[index];
  }

  // ترتيب وإرجاع قائمة جديدة (بدون تعديل الأصلية)
  List<T> sortedBy<R extends Comparable>(R Function(T) selector) {
    var copy = List<T>.from(this);
    copy.sort((a, b) => selector(a).compareTo(selector(b)));
    return copy;
  }
}

// توسيع خاص بقوائم العناصر القابلة للمقارنة
extension ComparableListExtras<T extends Comparable> on List<T> {
  T get maxValue => reduce((a, b) => a.compareTo(b) > 0 ? a : b);
  T get minValue => reduce((a, b) => a.compareTo(b) < 0 ? a : b);
}

// توسيع خاص بقوائم الأرقام
extension NumListExtras on List<num> {
  num get sum => fold(0, (total, n) => total + n);
  double get average => sum / length;
}

void main() {
  var numbers = [3, 1, 4, 1, 5, 9, 2, 6, 5];

  // التوسيعات المعممة
  print(numbers.unique);              // [3, 1, 4, 5, 9, 2, 6]
  print(numbers.chunked(3));          // [[3, 1, 4], [1, 5, 9], [2, 6, 5]]
  print(numbers.elementAtOrNull(99)); // null (بدلاً من RangeError)

  // توسيعات Comparable
  print(numbers.maxValue);  // 9
  print(numbers.minValue);  // 1

  // توسيعات num
  print(numbers.sum);      // 36
  print(numbers.average);  // 4.0

  // يعمل مع أي نوع
  var words = ['banana', 'apple', 'cherry', 'apple'];
  print(words.unique);     // [banana, apple, cherry]
  print(words.maxValue);   // cherry

  // sortedBy مع كائنات معقدة
  var users = [
    {'name': 'Charlie', 'age': 30},
    {'name': 'Alice', 'age': 25},
    {'name': 'Bob', 'age': 35},
  ];
  var sorted = users.sortedBy((u) => u['name'] as String);
  print(sorted.map((u) => u['name']));  // (Alice, Bob, Charlie)
}

عوامل التوسيع

يمكن للتوسيعات أيضاً إضافة عوامل للأنواع الموجودة. هذا مفيد لجعل العمليات الخاصة بالمجال تبدو طبيعية.

إضافة عوامل عبر التوسيعات

extension DateTimeExtras on DateTime {
  // إضافة أيام باستخدام عامل + مع Duration
  DateTime operator +(Duration duration) => add(duration);
  DateTime operator -(Duration duration) => subtract(duration);

  // تنسيق قابل للقراءة
  String get formatted =>
      '${year.toString().padLeft(4, '0')}-${month.toString().padLeft(2, '0')}-${day.toString().padLeft(2, '0')}';

  String get timeFormatted =>
      '${hour.toString().padLeft(2, '0')}:${minute.toString().padLeft(2, '0')}';

  // الوقت النسبي
  String get timeAgo {
    var diff = DateTime.now().difference(this);
    if (diff.inDays > 365) return '${diff.inDays ~/ 365}y ago';
    if (diff.inDays > 30) return '${diff.inDays ~/ 30}mo ago';
    if (diff.inDays > 0) return '${diff.inDays}d ago';
    if (diff.inHours > 0) return '${diff.inHours}h ago';
    if (diff.inMinutes > 0) return '${diff.inMinutes}m ago';
    return 'just now';
  }

  // تحقق من خصائص التاريخ
  bool get isToday {
    var now = DateTime.now();
    return year == now.year && month == now.month && day == now.day;
  }

  bool get isWeekend =>
      weekday == DateTime.saturday || weekday == DateTime.sunday;

  // بداية/نهاية اليوم
  DateTime get startOfDay => DateTime(year, month, day);
  DateTime get endOfDay => DateTime(year, month, day, 23, 59, 59);
}

extension MapExtras<K, V> on Map<K, V> {
  // دمج مع خريطة أخرى (الأخرى تفوز عند التعارض)
  Map<K, V> operator +(Map<K, V> other) => {...this, ...other};

  // الحصول على قيمة أو افتراضية بدون فحص null
  V getOr(K key, V defaultValue) => this[key] ?? defaultValue;

  // تصفية المدخلات
  Map<K, V> whereEntries(bool Function(K, V) test) {
    return Map.fromEntries(
      entries.where((e) => test(e.key, e.value)),
    );
  }
}

void main() {
  var now = DateTime.now();
  print(now.formatted);        // 2026-04-15
  print(now.timeFormatted);    // 14:30
  print(now.isWeekend);        // يعتمد على اليوم الحالي
  print(now.startOfDay);       // 2026-04-15 00:00:00.000

  var past = DateTime(2026, 4, 10);
  print(past.timeAgo);         // 5d ago

  // عوامل Map
  var map1 = {'a': 1, 'b': 2};
  var map2 = {'b': 3, 'c': 4};
  print(map1 + map2);  // {a: 1, b: 3, c: 4}

  print(map1.getOr('z', 0));  // 0

  var filtered = map1.whereEntries((k, v) => v > 1);
  print(filtered);  // {b: 2}
}

مثال عملي: بناء مكتبة تحقق

لنبني مكتبة تحقق واقعية باستخدام التوسيعات. هذا النمط يُستخدم في تطبيقات Flutter الإنتاجية للتحقق من مدخلات النماذج بنظافة وقابلية للسلسلة.

واقعي: مكتبة توسيع التحقق

// نتيجة التحقق الأساسية
class ValidationResult {
  final bool isValid;
  final String? error;

  const ValidationResult.valid() : isValid = true, error = null;
  const ValidationResult.invalid(this.error) : isValid = false;

  @override
  String toString() => isValid ? 'Valid' : 'Invalid: $error';
}

// مُحقق قابل للسلسلة
class Validator {
  final String value;
  final List<String> _errors = [];

  Validator(this.value);

  bool get isValid => _errors.isEmpty;
  List<String> get errors => List.unmodifiable(_errors);
  String? get firstError => _errors.isEmpty ? null : _errors.first;

  Validator required({String? message}) {
    if (value.trim().isEmpty) {
      _errors.add(message ?? 'This field is required');
    }
    return this;
  }

  Validator minLength(int min, {String? message}) {
    if (value.length < min) {
      _errors.add(message ?? 'Must be at least $min characters');
    }
    return this;
  }

  Validator maxLength(int max, {String? message}) {
    if (value.length > max) {
      _errors.add(message ?? 'Must be at most $max characters');
    }
    return this;
  }

  Validator matches(RegExp pattern, {String? message}) {
    if (!pattern.hasMatch(value)) {
      _errors.add(message ?? 'Invalid format');
    }
    return this;
  }

  Validator custom(bool Function(String) test, String message) {
    if (!test(value)) {
      _errors.add(message);
    }
    return this;
  }
}

// توسيع يربط String بـ Validator
extension StringValidation on String {
  Validator get validate => Validator(this);

  // مُحققات سريعة
  ValidationResult validateEmail() {
    var v = validate
        .required(message: 'Email is required')
        .matches(
          RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$'),
          message: 'Invalid email format',
        );
    return v.isValid
        ? ValidationResult.valid()
        : ValidationResult.invalid(v.firstError!);
  }

  ValidationResult validatePassword() {
    var v = validate
        .required(message: 'Password is required')
        .minLength(8, message: 'Password must be at least 8 characters')
        .matches(
          RegExp(r'[A-Z]'),
          message: 'Must contain an uppercase letter',
        )
        .matches(
          RegExp(r'[0-9]'),
          message: 'Must contain a number',
        );
    return v.isValid
        ? ValidationResult.valid()
        : ValidationResult.invalid(v.firstError!);
  }

  ValidationResult validateUsername() {
    var v = validate
        .required(message: 'Username is required')
        .minLength(3, message: 'Username must be at least 3 characters')
        .maxLength(20, message: 'Username must be at most 20 characters')
        .matches(
          RegExp(r'^[a-zA-Z0-9_]+$'),
          message: 'Only letters, numbers, and underscores',
        );
    return v.isValid
        ? ValidationResult.valid()
        : ValidationResult.invalid(v.firstError!);
  }
}

void main() {
  // تحقق سريع
  print('alice@test.com'.validateEmail());   // Valid
  print('not-email'.validateEmail());          // Invalid: Invalid email format
  print(''.validateEmail());                    // Invalid: Email is required

  print('SecurePass1'.validatePassword());     // Valid
  print('weak'.validatePassword());             // Invalid: Password must be at least 8 characters
  print('alllowercase1'.validatePassword());    // Invalid: Must contain an uppercase letter

  print('alice_123'.validateUsername());        // Valid
  print('ab'.validateUsername());                // Invalid: Username must be at least 3 characters

  // سلسلة تحقق مخصصة
  var result = 'my-input'.validate
      .required()
      .minLength(5)
      .custom(
        (s) => !s.contains('bad-word'),
        'Contains prohibited content',
      );

  if (result.isValid) {
    print('Input is valid!');
  } else {
    print('Errors: ${result.errors}');
  }
}
أفضل ممارسة: طرق التوسيع مثالية لإنشاء مفردات خاصة بالمجال. في تطبيق Flutter قد يكون لديك توسيعات لتنسيق التواريخ والتحقق من المدخلات وتحويل الألوان وإنشاء عناصر واجهة من البيانات وبناء المدد الزمنية. نظّم التوسيعات في ملفات منفصلة حسب الفئة (string_extensions.dart و date_extensions.dart و list_extensions.dart) واستورد فقط ما تحتاجه. هذا يحافظ على نظافة قاعدة كودك ويتجنب تعارضات الأسماء.