Menu
Coddy logo textTech
flag Ar iconالعربيةdown icon

نمط المستودع

جزء من قسم Object Oriented Programming في رحلة Dart على Coddy — الدرس 100 من 110.

يفصل نمط المستودع (Repository pattern) منطق الوصول إلى البيانات عن منطق العمل من خلال إنشاء طبقة تجريد بين تطبيقك ومصادر البيانات. فبدلاً من جعل الفئات (classes) الخاصة بك تتفاعل مباشرة مع قواعد البيانات أو واجهات برمجة التطبيقات (APIs) أو الملفات، فإنها تتعامل مع مستودع يتولى كافة عمليات البيانات.

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

// الكيان
class User {
  final int id;
  final String name;
  User(this.id, this.name);
}

// واجهة المستودع
abstract class UserRepository {
  User? getById(int id);
  List<User> getAll();
  void save(User user);
  void delete(int id);
}

// تنفيذ في الذاكرة
class InMemoryUserRepository implements UserRepository {
  final Map<int, User> _users = {};

  @override
  User? getById(int id) => _users[id];

  @override
  List<User> getAll() => _users.values.toList();

  @override
  void save(User user) => _users[user.id] = user;

  @override
  void delete(int id) => _users.remove(id);
}

void main() {
  UserRepository repo = InMemoryUserRepository();
  
  repo.save(User(1, 'Alice'));
  repo.save(User(2, 'Bob'));
  
  print(repo.getById(1)?.name);  // Alice
  print(repo.getAll().length);    // 2
}

يعتمد منطق العمل الخاص بك فقط على واجهة UserRepository. يمكنك لاحقاً إنشاء DatabaseUserRepository أو ApiUserRepository تنفذ نفس الواجهة، وتبديل التنفيذات دون تعديل أي كود يستخدم المستودع. هذا النمط ضروري لبناء تطبيقات قابلة للاختبار والصيانة حيث قد تتغير مصادر البيانات بمرور الوقت.

challenge icon

التحدي

سهل

لنقم ببناء نظام مخزون منتجات باستخدام نمط المستودع (Repository pattern)! ستقوم بإنشاء طبقة تجريد تفصل بين كيفية تخزين المنتجات وكيفية استخدامها، مما يسهل تبديل تطبيقات التخزين دون تغيير منطق العمل (business logic).

ستقوم بتنظيم الكود الخاص بك في ملفين:

  • product_repository.dart: يحتوي هذا الملف على الكيان (entity) وفئات المستودع. ابدأ بفئة Product التي تحتوي على حقول final لكل من id (int)، و name (String)، و price (double). ثم قم بتعريف فئة مجردة (abstract class) باسم ProductRepository تعلن عن العقد لعمليات البيانات: getById(int id) التي تعيد Product قابلاً للقيمة null، و getAll() التي تعيد قائمة من المنتجات، و save(Product product) لتخزين منتج، و delete(int id) لحذف منتج. أخيرًا، قم بإنشاء InMemoryProductRepository الذي ينفذ هذه الواجهة باستخدام Map<int, Product> للتخزين.
  • main.dart: قم باستيراد ملف المستودع الخاص بك واستعرض نمط المستودع قيد التنفيذ. أنشئ InMemoryProductRepository ولكن قم بتخزينه في متغير من نوع ProductRepository - وهذا يوضح كيف يعتمد الكود الخاص بك على الواجهة، وليس على التنفيذ. قم بحفظ ثلاثة منتجات: Laptop (id: 1, price: 999.99)، و Mouse (id: 2, price: 29.99)، و Keyboard (id: 3, price: 79.99). ثم استرجع واطبع المنتج الذي يحمل المعرف 2 بالتنسيق Found: [name] - $[price]. احذف المنتج الذي يحمل المعرف 1. أخيرًا، اطبع عدد المنتجات المتبقية كـ Products in stock: [count].

تكمن قوة هذا النمط في أن الكود الرئيسي الخاص بك لا يعرف سوى واجهة ProductRepository. يمكنك لاحقًا إنشاء DatabaseProductRepository أو ApiProductRepository وتبديلهما دون تغيير أي من منطق العمل!

المخرجات المتوقعة:

Found: Mouse - $29.99
Products in stock: 2

ورقة مرجعية

يفصل نمط المستودع (Repository pattern) منطق الوصول إلى البيانات عن منطق العمل من خلال إنشاء طبقة تجريد بين تطبيقك ومصادر البيانات.

قم بتعريف واجهة (interface) لعمليات البيانات:

// Entity (كيان)
class User {
  final int id;
  final String name;
  User(this.id, this.name);
}

// Repository interface (واجهة المستودع)
abstract class UserRepository {
  User? getById(int id);
  List<User> getAll();
  void save(User user);
  void delete(int id);
}

قم بإنشاء تطبيقات ملموسة (concrete implementations) تتعامل مع آلية التخزين الفعلية:

// In-memory implementation (تطبيق في الذاكرة)
class InMemoryUserRepository implements UserRepository {
  final Map<int, User> _users = {};

  @override
  User? getById(int id) => _users[id];

  @override
  List<User> getAll() => _users.values.toList();

  @override
  void save(User user) => _users[user.id] = user;

  @override
  void delete(int id) => _users.remove(id);
}

استخدم المستودع من خلال واجهته:

void main() {
  UserRepository repo = InMemoryUserRepository();
  
  repo.save(User(1, 'Alice'));
  repo.save(User(2, 'Bob'));
  
  print(repo.getById(1)?.name);  // Alice
  print(repo.getAll().length);    // 2
}

يعتمد منطق العمل فقط على الواجهة، مما يسمح لك بتبديل التطبيقات (على سبيل المثال، DatabaseUserRepository، ApiUserRepository) دون تعديل الكود الذي يستخدم المستودع.

جرّب بنفسك

import 'product_repository.dart';

void main() {
  // إنشاء المستودع - لاحظ أن النوع هو الواجهة (interface)، وليس التنفيذ (implementation)
  // TODO: قم بإنشاء InMemoryProductRepository وتخزينه في متغير ProductRepository
  
  // TODO: قم بحفظ ثلاثة منتجات:
  // - Laptop (id: 1, price: 999.99)
  // - Mouse (id: 2, price: 29.99)
  // - Keyboard (id: 3, price: 79.99)
  
  // TODO: استرجع المنتج الذي يحمل id 2 واطبع: "Found: [name] - $[price]"
  
  // TODO: احذف المنتج الذي يحمل id 1
  
  // TODO: اطبع عدد المنتجات المتبقية: "Products in stock: [count]"
}
quiz iconاختبر نفسك

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

جميع دروس Object Oriented Programming