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

الفئات والواجهات المعممة

50 دقيقة الدرس 11 من 24

الواجهات المعممة

في الدرس السابق تعلمت كيفية إنشاء فئات ودوال معممة. الآن نأخذها أبعد: الواجهات المعممة تعرّف عقوداً تستخدم معاملات النوع. أي فئة تنفذ واجهة معممة يجب أن توفر أنواعاً ملموسة وتنفذ جميع الأعضاء المطلوبة لتلك الأنواع. هذا هو الأساس لبناء بنيات مرنة وقابلة لإعادة الاستخدام في Dart.

في Dart لا توجد كلمة مفتاحية interface منفصلة -- أي فئة أو abstract class يمكن أن تعمل كواجهة. مع Dart 3 يمكنك أيضاً استخدام abstract interface class لإنشاء واجهة صرفة لا يمكن توسيعها (فقط تنفيذها).

تعريف واجهة معممة

// واجهة معممة لتخزين البيانات
// T هو نوع العنصر المخزّن
abstract interface class DataStore<T> {
  Future<T?> getById(String id);
  Future<List<T>> getAll();
  Future<void> save(T item);
  Future<bool> delete(String id);
  Future<int> get count;
}

// واجهة معممة للعناصر القابلة للتحويل من/إلى JSON
abstract interface class JsonConvertible<T> {
  Map<String, dynamic> toJson();
  T fromJson(Map<String, dynamic> json);
}

// التنفيذ بنوع ملموس
class User {
  final String id;
  final String name;
  final String email;

  User({required this.id, required this.name, required this.email});

  Map<String, dynamic> toJson() => {'id': id, 'name': name, 'email': email};

  factory User.fromJson(Map<String, dynamic> json) => User(
    id: json['id'] as String,
    name: json['name'] as String,
    email: json['email'] as String,
  );

  @override
  String toString() => 'User($name, $email)';
}

// تنفيذ ملموس لـ DataStore لـ User
class InMemoryUserStore implements DataStore<User> {
  final Map<String, User> _data = {};

  @override
  Future<User?> getById(String id) async => _data[id];

  @override
  Future<List<User>> getAll() async => _data.values.toList();

  @override
  Future<void> save(User item) async => _data[item.id] = item;

  @override
  Future<bool> delete(String id) async => _data.remove(id) != null;

  @override
  Future<int> get count async => _data.length;
}

void main() async {
  DataStore<User> store = InMemoryUserStore();

  await store.save(User(id: '1', name: 'Alice', email: 'alice@test.com'));
  await store.save(User(id: '2', name: 'Bob', email: 'bob@test.com'));

  var user = await store.getById('1');
  print(user);  // User(Alice, alice@test.com)

  print(await store.count);  // 2
}
نقطة أساسية: عندما تنفذ فئة implements DataStore<User> يستبدل Dart كل T في الواجهة بـ User. لذا Future<T?> getById(String id) تصبح Future<User?> getById(String id). المُجمّع يفرض هذا -- لا يمكنك إرجاع النوع الخاطئ.

تنفيذ الواجهات المعممة بالأنواع المعممة

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

تنفيذ معمم لواجهة معممة

// واجهة معممة
abstract interface class Cache<K, V> {
  V? get(K key);
  void set(K key, V value, {Duration? ttl});
  void remove(K key);
  void clear();
  int get size;
}

// تنفيذ معمم -- يعمل مع أي نوعي مفتاح وقيمة
class InMemoryCache<K, V> implements Cache<K, V> {
  final Map<K, _CacheEntry<V>> _store = {};
  final int maxSize;

  InMemoryCache({this.maxSize = 100});

  @override
  V? get(K key) {
    var entry = _store[key];
    if (entry == null) return null;
    if (entry.isExpired) {
      _store.remove(key);
      return null;
    }
    return entry.value;
  }

  @override
  void set(K key, V value, {Duration? ttl}) {
    // إخراج الأقدم عند الوصول للسعة
    if (_store.length >= maxSize && !_store.containsKey(key)) {
      _store.remove(_store.keys.first);
    }
    _store[key] = _CacheEntry(value, ttl: ttl);
  }

  @override
  void remove(K key) => _store.remove(key);

  @override
  void clear() => _store.clear();

  @override
  int get size => _store.length;
}

class _CacheEntry<V> {
  final V value;
  final DateTime createdAt;
  final Duration? ttl;

  _CacheEntry(this.value, {this.ttl}) : createdAt = DateTime.now();

  bool get isExpired =>
      ttl != null && DateTime.now().difference(createdAt) > ttl!;
}

void main() {
  // ذاكرة مؤقتة بمفاتيح نصية وقيم عددية
  var scores = InMemoryCache<String, int>(maxSize: 50);
  scores.set('alice', 95);
  scores.set('bob', 87);
  print(scores.get('alice'));  // 95

  // ذاكرة مؤقتة بمفاتيح عددية وقيم Map
  var apiCache = InMemoryCache<int, Map<String, dynamic>>();
  apiCache.set(42, {'name': 'Product 42', 'price': 9.99});
  print(apiCache.get(42));  // {name: Product 42, price: 9.99}

  // نفس الفئة وأنواع مختلفة تماماً -- آمنة النوع بالكامل
  // scores.set('charlie', 'ninety');  // خطأ تجميع: String ليس int
}
نصيحة: استخدم abstract interface class (Dart 3) عندما تريد عقداً صرفاً لا يمكن توسيعه فقط تنفيذه. استخدم abstract class عندما تريد مزيجاً من العقد والتنفيذ المشترك. الاختيار يؤثر على كيفية استخدام المطورين الآخرين لنوعك.

الطرق المعممة في الفئات

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

طرق معممة داخل فئة غير معممة

class DataProcessor {
  // هذه الفئة ليست معممة لكن هذه الطرق معممة

  // طريقة معممة: تحوّل قائمة باستخدام دالة مقدمة
  List<R> mapList<T, R>(List<T> items, R Function(T) transform) {
    return items.map(transform).toList();
  }

  // طريقة معممة: تجمع العناصر بمفتاح
  Map<K, List<T>> groupBy<T, K>(List<T> items, K Function(T) keyFn) {
    var result = <K, List<T>>{};
    for (var item in items) {
      var key = keyFn(item);
      result.putIfAbsent(key, () => []).add(item);
    }
    return result;
  }

  // طريقة معممة: تجد أول تطابق أو ترجع القيمة الافتراضية
  T findFirstOr<T>(List<T> items, bool Function(T) test, T defaultValue) {
    for (var item in items) {
      if (test(item)) return item;
    }
    return defaultValue;
  }
}

void main() {
  var processor = DataProcessor();

  // mapList: int -> String
  var labels = processor.mapList(
    [1, 2, 3],
    (n) => 'Item #$n',
  );
  print(labels);  // [Item #1, Item #2, Item #3]

  // groupBy: تجميع السلاسل النصية حسب الطول
  var words = ['cat', 'dog', 'fish', 'ant', 'bird'];
  var grouped = processor.groupBy(words, (w) => w.length);
  print(grouped);  // {3: [cat, dog, ant], 4: [fish, bird]}

  // findFirstOr مع قيمة افتراضية
  var numbers = [10, 25, 3, 42, 8];
  var big = processor.findFirstOr(numbers, (n) => n > 30, -1);
  print(big);  // 42

  var none = processor.findFirstOr(numbers, (n) => n > 100, -1);
  print(none);  // -1
}

معاملات النوع المتعددة

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

Pair و Either بمعاملات نوع متعددة

// زوج بسيط من قيمتين من أنواع مختلفة
class Pair<A, B> {
  final A first;
  final B second;

  const Pair(this.first, this.second);

  // إنشاء زوج جديد بمواقع مبدّلة
  Pair<B, A> swap() => Pair(second, first);

  // تحويل قيمة واحدة أو كلتيهما
  Pair<C, B> mapFirst<C>(C Function(A) transform) =>
      Pair(transform(first), second);

  Pair<A, C> mapSecond<C>(C Function(B) transform) =>
      Pair(first, transform(second));

  @override
  String toString() => 'Pair($first, $second)';

  @override
  bool operator ==(Object other) =>
      other is Pair<A, B> && other.first == first && other.second == second;

  @override
  int get hashCode => Object.hash(first, second);
}

// Either: يحمل قيمة من النوع A أو B وليس كليهما
sealed class Either<L, R> {
  const Either();

  T fold<T>({
    required T Function(L) left,
    required T Function(R) right,
  });

  bool get isLeft;
  bool get isRight;
}

class Left<L, R> extends Either<L, R> {
  final L value;
  const Left(this.value);

  @override
  T fold<T>({required T Function(L) left, required T Function(R) right}) =>
      left(value);

  @override
  bool get isLeft => true;
  @override
  bool get isRight => false;
}

class Right<L, R> extends Either<L, R> {
  final R value;
  const Right(this.value);

  @override
  T fold<T>({required T Function(L) left, required T Function(R) right}) =>
      right(value);

  @override
  bool get isLeft => false;
  @override
  bool get isRight => true;
}

void main() {
  // استخدام Pair
  var nameAge = Pair('Alice', 30);
  print(nameAge);             // Pair(Alice, 30)
  print(nameAge.swap());      // Pair(30, Alice)
  print(nameAge.mapFirst((n) => n.toUpperCase())); // Pair(ALICE, 30)

  // Either: يمثل نجاح (Right) أو خطأ (Left)
  Either<String, int> result = Right(42);

  var message = result.fold(
    left: (error) => 'Error: $error',
    right: (value) => 'Success: $value',
  );
  print(message);  // Success: 42

  Either<String, int> error = Left('Not found');
  print(error.fold(
    left: (e) => 'Error: $e',
    right: (v) => 'Value: $v',
  ));  // Error: Not found
}
تحذير: لا تفرط في استخدام معاملات النوع المتعددة. إذا وجدت نفسك تكتب MyClass<A, B, C, D, E> فتصميمك ربما معقد جداً. معظم الأنواع المعممة الواقعية تستخدم معامل نوع واحد أو اثنين. ثلاثة غير شائع. أربعة أو أكثر علامة على أنه يجب تقسيم النوع إلى أجزاء أصغر.

المصانع المعممة

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

نمط المصنع المعمم

// غلاف استجابة API معمم
class ApiResponse<T> {
  final T? data;
  final String? error;
  final int statusCode;
  final DateTime timestamp;

  ApiResponse._({
    this.data,
    this.error,
    required this.statusCode,
  }) : timestamp = DateTime.now();

  // مُنشئات مصنع
  factory ApiResponse.success(T data, {int statusCode = 200}) =>
      ApiResponse._(data: data, statusCode: statusCode);

  factory ApiResponse.error(String message, {int statusCode = 500}) =>
      ApiResponse._(error: message, statusCode: statusCode);

  bool get isSuccess => error == null && data != null;
  bool get isError => error != null;

  // تحويل نوع البيانات
  ApiResponse<R> map<R>(R Function(T) transform) {
    if (isSuccess) {
      return ApiResponse.success(transform(data as T), statusCode: statusCode);
    }
    return ApiResponse.error(error!, statusCode: statusCode);
  }

  @override
  String toString() => isSuccess
      ? 'ApiResponse(status: $statusCode, data: $data)'
      : 'ApiResponse(status: $statusCode, error: $error)';
}

void main() {
  // استجابة نجاح ببيانات User
  var userResponse = ApiResponse.success(
    {'id': 1, 'name': 'Alice'},
    statusCode: 200,
  );
  print(userResponse);
  // ApiResponse(status: 200, data: {id: 1, name: Alice})

  // استجابة خطأ
  var errorResponse = ApiResponse<Map>.error(
    'User not found',
    statusCode: 404,
  );
  print(errorResponse);
  // ApiResponse(status: 404, error: User not found)

  // تحويل: Map -> String (استخراج الاسم فقط)
  var nameResponse = userResponse.map(
    (data) => data['name'] as String,
  );
  print(nameResponse);
  // ApiResponse(status: 200, data: Alice)
}

مثال عملي: خدمة API معممة

لنجمع كل شيء في بنية واقعية ستستخدمها في تطبيق Flutter إنتاجي -- خدمة API معممة باستجابات آمنة النوع وتخزين مؤقت ومعالجة أخطاء.

واقعي: بنية API معممة كاملة

// واجهة النموذج الأساسي
abstract class Identifiable {
  String get id;
}

abstract class Serializable<T> {
  Map<String, dynamic> toJson();
}

// واجهة CRUD معممة
abstract interface class CrudService<T extends Identifiable> {
  Future<ApiResponse<T>> getById(String id);
  Future<ApiResponse<List<T>>> getAll({int page, int perPage});
  Future<ApiResponse<T>> create(T item);
  Future<ApiResponse<T>> update(T item);
  Future<ApiResponse<bool>> delete(String id);
}

// تنفيذ معمم مع تخزين مؤقت
class CachedCrudService<T extends Identifiable> implements CrudService<T> {
  final CrudService<T> _inner;
  final Cache<String, T> _cache;

  CachedCrudService(this._inner, this._cache);

  @override
  Future<ApiResponse<T>> getById(String id) async {
    // تحقق من الذاكرة المؤقتة أولاً
    var cached = _cache.get(id);
    if (cached != null) {
      return ApiResponse.success(cached);
    }

    // جلب من الخدمة الداخلية
    var response = await _inner.getById(id);
    if (response.isSuccess && response.data != null) {
      _cache.set(id, response.data as T, ttl: Duration(minutes: 5));
    }
    return response;
  }

  @override
  Future<ApiResponse<List<T>>> getAll({int page = 1, int perPage = 20}) =>
      _inner.getAll(page: page, perPage: perPage);

  @override
  Future<ApiResponse<T>> create(T item) async {
    var response = await _inner.create(item);
    if (response.isSuccess) {
      _cache.set(item.id, item, ttl: Duration(minutes: 5));
    }
    return response;
  }

  @override
  Future<ApiResponse<T>> update(T item) async {
    var response = await _inner.update(item);
    if (response.isSuccess) {
      _cache.set(item.id, item, ttl: Duration(minutes: 5));
    }
    return response;
  }

  @override
  Future<ApiResponse<bool>> delete(String id) async {
    var response = await _inner.delete(id);
    if (response.isSuccess) {
      _cache.remove(id);
    }
    return response;
  }
}

// نموذج ملموس
class Product implements Identifiable {
  @override
  final String id;
  final String name;
  final double price;

  Product({required this.id, required this.name, required this.price});

  @override
  String toString() => 'Product($name, \$$price)';
}

// الاستخدام يوضح كيف تمكّن الأنواع المعممة التركيب المرن
void main() {
  // نفس CachedCrudService يعمل لـ Product و User و Order وغيرها
  // تكتب منطق التخزين المؤقت مرة واحدة وتعيد استخدامه في كل مكان
  print('بنية معممة: اكتب مرة واحدة واستخدم لأي نوع نموذج');
  print('CrudService<Product> -- CRUD آمن النوع للمنتجات');
  print('CrudService<User> -- نفس الواجهة ونوع مختلف');
  print('CachedCrudService يغلف أي CrudService بتخزين مؤقت');
}
رؤية معمارية: هذا النمط يوضح القوة الحقيقية للواجهات المعممة: يمكنك إنشاء مُزخرِفات مثل CachedCrudService<T> تضيف سلوكاً (تخزين مؤقت) لأي خدمة CRUD دون معرفة النوع الملموس. يمكنك أيضاً إنشاء LoggingCrudService<T> و RetryingCrudService<T> أو AuthenticatedCrudService<T> -- كلها قابلة لإعادة الاستخدام مع أي نوع نموذج. هذا نمط المُزخرِف مدمجاً مع الأنواع المعممة.