نمط المستودع
جزء من قسم 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 تنفذ نفس الواجهة، وتبديل التنفيذات دون تعديل أي كود يستخدم المستودع. هذا النمط ضروري لبناء تطبيقات قابلة للاختبار والصيانة حيث قد تتغير مصادر البيانات بمرور الوقت.
التحدي
سهللنقم ببناء نظام مخزون منتجات باستخدام نمط المستودع (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]"
}
يتضمن هذا الدرس اختبارًا قصيرًا. ابدأ الدرس للإجابة عليه وتتبّع تقدمك.
جميع دروس Object Oriented Programming
1أساسيات الـ OOP
الملفات الخارجيةالمكتبات والـ Importsمقدمة في الـ OOPالـ Classes مقابل الـ Objectsالكلمة المفتاحية thisالـ Methodsمتغيرات الـ Instanceأساسيات الـ Constructorمراجعة - آلة حاسبة بسيطة4أمان الـ Null
مقدمة في أمان الـ NullNullable مقابل Non-Nullableعاملا الـ ? و !الكلمة المفتاحية Late وأمان الـ Nullالعوامل المدركة للـ Nullأمان الـ Null في الـ Classesمراجعة - نظام ملف تعريف المستخدم7الفئات المجردة والواجهات
الفئات المجردةالدوال المجردةالواجهات في Dartالواجهات الضمنيةالتنفيذ مقابل التوسيعالواجهات المتعددةمراجعة - حاسبة الأشكال10المجموعات والأنماط العامة
نظرة عامة على List و Set و Mapالمجموعات آمنة النوعالفئات العامةالدوال العامةقيود الأنماط العامةIterable و Iteratorملخص - التخزين العام13مفاهيم OOP المتقدمة
التركيب مقابل الوراثة (Composition vs Inheritance)دوال التوسعة (Extension Methods)الأصناف القابلة للاستدعاء (Callable Classes)الأصناف المختومة (Sealed Classes - Dart 3)السجلات (Records - Dart 3)الأنماط والمطابقة (Patterns & Matching - 3.0)التعدادات مع الدوال (Enums with Methods)2المنشئات في Dart
المنشئ الافتراضيالمنشئات المسماةقوائم التهيئةالمنشئات الثابتةالمنشئات المصنعيةالمنشئات الموجهةملخص - بناء الأشكال5التغليف (Encapsulation)
الأعضاء العامة مقابل الخاصةاتفاقية البادئة _الخصوصية على مستوى المكتبةتعمق في Getters و Settersإخفاء المعلوماتمراجعة - سجلات الطلاب8Mixins
مقدمة إلى Mixinsإنشاء Mixinsاستخدام Mixins متعددةالكلمة المفتاحية on في MixinsMixin مقابل الوراثة (Inheritance)Mixin مقابل الواجهة (Interface)مراجعة - نظام الحيوانات (Animal System)3خصائص الـ Class
أعضاء الـ Instance مقابل الـ Staticحقول Final و Constمتغيرات Lateالـ Static Methods والحقولدوال الـ Getters والـ Settersمراجعة - مدير الحساب البنكي6الوراثة
أساسيات الوراثةالكلمة المفتاحية superإعادة تعريف الدوال (Method Overriding)الوسم @overrideالكلمة المفتاحية final Classالدوال البانية والوراثةمراجعة - هيكلية الموظفين9تعدد الأشكال
أساسيات تعدد الأشكالتعدد الأشكال عبر الـ Interfacesالتحقق من النوع أثناء التشغيلالمعاملات is و asالكلمة المفتاحية Covariantملخص - معالج الدفع12البرمجة الكائنية غير المتزامنة (Async OOP)
Futures و async/awaitأساسيات الـ Streamsمتحكمات الـ Streamsالـ Constructors غير المتزامنةالعمليات غير المتزامنة في دوال الكلاسمراجعة - جالب البيانات (Data Fetcher)15أنماط التصميم - الجزء الثاني
نمط الأمرنمط المحولنمط المزيننمط طريقة القالبنمط الحالةنمط التركيبنمط المستودع