بنية التطبيق وأنماط التصميم

نمط المستودع

15 دقيقة الدرس 5 من 12

نمط المستودع

يُعدّ نمط المستودع (Repository Pattern) نمطَ تصميمٍ يعمل وسيطاً بين طبقة النطاق (Domain) وطبقة تعيين البيانات (Data Mapping). يُقدّم هذا النمط طبقةَ تجريدٍ نظيفة فوق مصادر البيانات، بحيث يعتمد باقي التطبيق — حالات الاستخدام، ونماذج العرض، وواجهة المستخدم — على واجهةٍ محددة بوضوح فقط، لا على تفاصيل تنفيذية ملموسة كعملاء HTTP أو قواعد البيانات المحلية أو وحدات التخزين.

في تطبيق Flutter يتبع بنية نظيفة، يقع المستودع عند حدود طبقة النطاق (منطق الأعمال الخالص) وطبقة البيانات (الشبكة، والتخزين المؤقت، والتخزين المحلي). تُعلن طبقة النطاق العقد، وتُوفيه طبقة البيانات.

المبدأ الأساسي: اعتمد على التجريدات لا على التنفيذات الملموسة. حين يستدعي نموذجُ العرض userRepository.getUserById(id)، فهو لا يعلم — ولا ينبغي له أن يعلم — ما إذا كانت النتيجة تأتي من واجهة REST API، أو SQLite، أو بيانات وهمية في الذاكرة. هذه هي قوة نمط المستودع.

تعريف واجهة المستودع (طبقة النطاق)

تعيش الواجهة في طبقة النطاق وتصف ما يمكن للمستودع فعله، دون أي تفاصيل تنفيذية. في Dart، تستخدم abstract class (أو abstract interface class في Dart 3) لهذا الغرض.

طبقة النطاق — المستودع المجرد

// lib/domain/repositories/user_repository.dart

import '../entities/user.dart';

abstract class UserRepository {
  /// جلب مستخدم واحد بمعرّفه الفريد.
  Future<User> getUserById(String id);

  /// إرجاع جميع المستخدمين المنتمين إلى فريق معين.
  Future<List<User>> getUsersByTeam(String teamId);

  /// حفظ سجل مستخدم جديد أو محدَّث.
  Future<void> saveUser(User user);

  /// إزالة مستخدم نهائياً.
  Future<void> deleteUser(String id);
}

لاحظ ثلاثة أمور في هذه الواجهة:

  • تُرجع كيانات النطاق (User)، لا خرائط JSON الخام ولا صفوف قواعد البيانات.
  • هي غير متزامنة بطبيعتها — كل العمليات تُرجع Future لأن أي مصدر بيانات قد ينطوي على عمليات إدخال/إخراج.
  • لا تحتوي على أي استيرادات من حزم كـ dio أو sqflite أو hive. طبقة النطاق مستقلة تماماً عن أي إطار عمل.

تنفيذ المستودع (طبقة البيانات)

تُقدّم طبقة البيانات فئةً ملموسة تُنفّذ الواجهة. يُنسّق تنفيذ المستودع الواحد عادةً مصدرَي بياناتٍ أو أكثر — مصدر API بعيد ومصدر تخزين مؤقت محلي — لتطبيق استراتيجيات مثل "التخزين المؤقت أولاً" أو "الشبكة ثم التخزين المؤقت".

طبقة البيانات — التنفيذ الملموس

// lib/data/repositories/user_repository_impl.dart

import '../../domain/entities/user.dart';
import '../../domain/repositories/user_repository.dart';
import '../datasources/remote/user_remote_datasource.dart';
import '../datasources/local/user_local_datasource.dart';

class UserRepositoryImpl implements UserRepository {
  final UserRemoteDataSource _remote;
  final UserLocalDataSource _local;

  const UserRepositoryImpl({
    required UserRemoteDataSource remote,
    required UserLocalDataSource local,
  })  : _remote = remote,
        _local = local;

  @override
  Future<User> getUserById(String id) async {
    // استراتيجية التخزين المؤقت أولاً: جرب المحلي، ثم ارجع للبعيد
    final cached = await _local.findUserById(id);
    if (cached != null) return cached;

    final dto = await _remote.fetchUser(id);
    final user = dto.toDomain();           // تحويل DTO → كيان النطاق
    await _local.cacheUser(user);
    return user;
  }

  @override
  Future<List<User>> getUsersByTeam(String teamId) async {
    final dtos = await _remote.fetchTeamUsers(teamId);
    return dtos.map((dto) => dto.toDomain()).toList();
  }

  @override
  Future<void> saveUser(User user) async {
    await _remote.updateUser(user.id, user.toDto());
    await _local.cacheUser(user);
  }

  @override
  Future<void> deleteUser(String id) async {
    await _remote.deleteUser(id);
    await _local.removeUser(id);
  }
}
نصيحة: احتفظ بمنطق تعيين البيانات (dto.toDomain()، user.toDto()) في توابع الامتداد أو فئات التعيين (mapper classes)، لا متناثراً في أرجاء المستودع. هذا يُبقي المستودع مركّزاً على التنسيق لا التحويل.

الربط معاً عبر حقن التبعيات

تُعلن طبقة النطاق UserRepository. تُقدّم طبقة البيانات UserRepositoryImpl. يقوم حاوي حقن التبعيات (أو محدد خدمة بسيط مثل get_it) بربط الواجهة بالتنفيذ عند بدء التشغيل، بحيث لا يُنشئ باقي التطبيق UserRepositoryImpl مباشرةً أبداً.

إعداد حقن التبعيات (get_it)

// lib/injection_container.dart
import 'package:get_it/get_it.dart';
import 'data/datasources/remote/user_remote_datasource.dart';
import 'data/datasources/local/user_local_datasource.dart';
import 'data/repositories/user_repository_impl.dart';
import 'domain/repositories/user_repository.dart';
import 'domain/usecases/get_user_by_id.dart';

final sl = GetIt.instance;

void configureDependencies() {
  // مصادر البيانات
  sl.registerLazySingleton<UserRemoteDataSource>(
    () => UserRemoteDataSourceImpl(client: sl()),
  );
  sl.registerLazySingleton<UserLocalDataSource>(
    () => UserLocalDataSourceImpl(db: sl()),
  );

  // المستودع — ربط الواجهة بالتنفيذ
  sl.registerLazySingleton<UserRepository>(
    () => UserRepositoryImpl(remote: sl(), local: sl()),
  );

  // حالات الاستخدام تعتمد فقط على UserRepository (الفئة المجردة)
  sl.registerFactory(() => GetUserById(repository: sl()));
}

استبدال التنفيذات دون المساس بمنطق الأعمال

أبرز فائدة لهذا النمط هي قابلية الاستبدال. إن قررت الانتقال من REST API إلى GraphQL، فأنشئ فئة تنفيذ جديدة وغيّر سطراً واحداً في إعداد حقن التبعيات. جميع حالات الاستخدام ونماذج العرض وودجات واجهة المستخدم تبقى دون تغيير.

  • الاختبار: أدخل FakeUserRepository أو محاكاة Mockito — لا حاجة إلى شبكة.
  • وضع عدم الاتصال: أدخل LocalOnlyUserRepository الذي يتجاوز المصدر البعيد.
  • الهجرة: استبدل RestUserRepositoryImpl بـ GraphQLUserRepositoryImpl في مكان واحد.
  • اختبار A/B: بدّل بين تنفيذَين عبر علامة ميزة (feature flag) عند بدء التشغيل.
خطأ شائع: إرجاع Map<String, dynamic> الخام أو كائنات نموذج الاستجابة من المستودع. يجب على المستودع دائماً إرجاع كيانات النطاق. إذا تلقّى نموذج العرض DTO، فهو مرتبط بشدة بطبقة البيانات وقد انكسر التجريد.

الخلاصة

يفرض نمط المستودع فصلاً صارماً بين ما يحتاجه التطبيق (واجهة النطاق) وكيفية جلبه (تنفيذ البيانات). إعلان الواجهة في طبقة النطاق يُبقي منطق الأعمال نقياً وقابلاً للنقل. تنفيذها في طبقة البيانات يحصر كل تعقيدات الوصول إلى البيانات — HTTP، والتخزين المؤقت، ومعالجة الأخطاء، والتعيين — في مكان واحد. يجمع حقن التبعيات الطبقتين معاً في وقت التشغيل، تاركاً كل جزء آخر من قاعدة الكود حراً في الاختبار والإعادة الهيكلية والتوسع باستقلالية.