الفئات والواجهات المعممة
الواجهات المعممة
في الدرس السابق تعلمت كيفية إنشاء فئات ودوال معممة. الآن نأخذها أبعد: الواجهات المعممة تعرّف عقوداً تستخدم معاملات النوع. أي فئة تنفذ واجهة معممة يجب أن توفر أنواعاً ملموسة وتنفذ جميع الأعضاء المطلوبة لتلك الأنواع. هذا هو الأساس لبناء بنيات مرنة وقابلة لإعادة الاستخدام في 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> -- كلها قابلة لإعادة الاستخدام مع أي نوع نموذج. هذا نمط المُزخرِف مدمجاً مع الأنواع المعممة.