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

هيكل المشروع المعياري القائم على الميزات

16 دقيقة الدرس 10 من 12

هيكل المشروع المعياري القائم على الميزات

مع نمو تطبيقات Flutter إلى ما هو أبعد من الأمثلة البسيطة، يصبح هيكل المجلدات المسطح — حيث تجلس كل الملفات تحت lib/ مصنَّفةً حسب النوع فقط — عبئاً على الصيانة. تحديد جميع الملفات المتعلقة بميزة واحدة يتطلب القفز بين models/ وscreens/ وservices/ وwidgets/ في آنٍ واحد. والحل هو بنية معيارية قائمة على الميزات (تُعرف أيضاً بـالشريحة الرأسية): كل ميزة تمتلك ملفاتها في مجلد متماسك واحد، مقسَّم داخلياً إلى طبقات data وdomain وpresentation.

لماذا تنهار الهياكل المسطحة

تخيَّل تطبيقاً يحوي مصادقة وكتالوج منتجات وسلة تسوق. يبدو الهيكل المسطح القائم على الأنواع كالتالي:

  • lib/models/ — user.dart, product.dart, cart_item.dart …
  • lib/screens/ — login_screen.dart, product_list_screen.dart, cart_screen.dart …
  • lib/services/ — auth_service.dart, product_service.dart, cart_service.dart …
  • lib/widgets/ — product_card.dart, cart_badge.dart …

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

تخطيط الشريحة الرأسية

تصبح كل ميزة مجلداً مكتفياً بذاته يضم ثلاث طبقات فرعية:

  • data/ — المستودعات ومصادر البيانات (عملاء API البعيدة ومساعدو قاعدة البيانات المحلية) وكلاسات النماذج/DTO الخام.
  • domain/ — الكيانات (كائنات الأعمال النقية) وواجهات المستودعات (العقود المجردة) وكلاسات حالات الاستخدام التي تُشفِّر قاعدة عمل واحدة.
  • presentation/ — الشاشات وودجات الصفحات وإدارة الحالة المخصصة للميزة (Cubit/Notifier/ViewModel) والودجات الصغيرة المستخدمة حصراً داخل هذه الميزة.

شجرة المجلدات الموصى بها

lib/
├── core/                        // البنية التحتية المشتركة
│   ├── network/
│   │   └── dio_client.dart
│   ├── error/
│   │   └── failures.dart
│   └── utils/
│       └── validators.dart
│
├── features/
│   ├── auth/
│   │   ├── data/
│   │   │   ├── datasources/
│   │   │   │   └── auth_remote_datasource.dart
│   │   │   ├── models/
│   │   │   │   └── user_model.dart
│   │   │   └── repositories/
│   │   │       └── auth_repository_impl.dart
│   │   ├── domain/
│   │   │   ├── entities/
│   │   │   │   └── user.dart
│   │   │   ├── repositories/
│   │   │   │   └── auth_repository.dart   // abstract
│   │   │   └── usecases/
│   │   │       └── login_usecase.dart
│   │   └── presentation/
│   │       ├── cubit/
│   │       │   ├── auth_cubit.dart
│   │       │   └── auth_state.dart
│   │       ├── pages/
│   │       │   └── login_page.dart
│   │       └── widgets/
│   │           └── login_form.dart
│   │
│   └── cart/
│       ├── data/ ...
│       ├── domain/ ...
│       └── presentation/ ...
│
└── main.dart
ملاحظة: التقسيم بين features/ وcore/ هو الحد الرئيسي. الكود داخل مجلد الميزة خاص بالميزة؛ أما الكود داخل core/ فمشترك بين جميع الميزات. لا تستورد أبداً من ميزة إلى أخرى — مرِّر عبر core/ أو استخدم حقن التبعيات بدلاً من ذلك.

شرح الطبقات الداخلية الثلاث

لكل طبقة داخلية اتجاه تبعية صارم: presentation → domain ← data. طبقة domain لا تعرف شيئاً عن Flutter أو HTTP؛ تحتوي فقط على Dart نقي.

  • domain/entities/ — كائنات أعمال غير قابلة للتغيير بلا اقتران بأي إطار عمل (بلا fromJson ولا مكتبات تعليق copyWith).
  • domain/repositories/ — كلاسات مجردة تُعلن العمليات المتاحة. تعتمد طبقة domain على هذه العقود لا على التطبيقات الفعلية.
  • domain/usecases/ — كلاس واحد لكل إجراء عمل؛ يستدعي واجهة المستودع ويُعيد نتيجة، مُبقياً منطق الأعمال بعيداً عن طبقة العرض.
  • data/models/ — تمتد أو تُغلِّف الكيانات بالتسلسل (fromJson / toJson).
  • data/repositories/ — تطبيقات فعلية لواجهات مستودعات domain، تربط مصادر البيانات معاً.
  • presentation/ — ودجات إضافة إلى طبقة رقيقة لإدارة الحالة (Cubit أو Riverpod Notifier إلخ) تستدعي حالات الاستخدام وتكشف حالة واجهة المستخدم.

طبقة Domain: الكيان وواجهة المستودع وحالة الاستخدام

// features/auth/domain/entities/user.dart
class User {
  final String id;
  final String email;
  final String displayName;

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

// features/auth/domain/repositories/auth_repository.dart
abstract class AuthRepository {
  Future<User> login(String email, String password);
  Future<void> logout();
  Future<User?> getCurrentUser();
}

// features/auth/domain/usecases/login_usecase.dart
class LoginUseCase {
  final AuthRepository _repository;

  const LoginUseCase(this._repository);

  Future<User> call(String email, String password) {
    return _repository.login(email, password);
  }
}

وحدة Core / Common

الكود الذي يجب إعادة استخدامه عبر الميزات يسكن في lib/core/. السكان النموذجيون:

  • network/ — عميل Dio أو http مُهيَّأ مع المعترضات وعناوين URL الأساسية.
  • error/ — هرمية Failure مغلقة حتى تُبلِّغ كل ميزة عن الأخطاء بشكل موحد.
  • di/ — إعداد حقن التبعيات (مثل تسجيلات محدد خدمة get_it).
  • router/ — أسماء المسارات وتهيئة GoRouter / AutoRoute.
  • theme/ThemeData ورموز الألوان وأنماط النص.
  • utils/ — مُحقِّقون ومُنسِّقون وأساليب امتداد عامة.
  • widgets/ — ذرات واجهة مستخدم قابلة لإعادة الاستخدام فعلاً (مؤشر التحميل وشريط الخطأ والصورة الرمزية) تستخدمها ميزتان أو أكثر.
نصيحة: عندما تجد نفسك تنسخ ودجتاً أو مساعداً إلى ميزة ثانية، فذلك هو الإشارة لترقيته إلى core/. أبقِ core/ خفيفاً — فقط ما هو مشترك فعلاً ينتمي إليه، لا ما قد يكون مشتركاً يوماً ما.

الملفات الحاوية ونظافة الاستيراد

يمكن لكل ميزة كشف ملف حاوٍ واحد (auth.dart) يُعيد تصدير السطح العام للميزة فحسب — عادةً صفحات العرض ودالة تسجيل حقن التبعيات. تظل كلاسات data وdomain الداخلية خاصة بالميزة، مما يمنع الميزات الأخرى من تجاوز البنية.

نمط الملف الحاوي

// features/auth/auth.dart  — الواجهة العامة لميزة auth
export 'presentation/pages/login_page.dart';
export 'presentation/pages/register_page.dart';
export 'di/auth_di.dart';  // يسجل تبعيات auth

// main.dart يستورد الملف الحاوي فقط:
import 'features/auth/auth.dart';
// الكلاسات الداخلية مثل AuthRepositoryImpl غير مُصدَّرة
// وبالتالي لا يمكن استيرادها عن طريق الخطأ من ميزات أخرى.
تحذير: تجنَّب الاستيرادات الدائرية بين الميزات. إذا احتاجت cart إلى كائن User من auth، انقل كيان User إلى core/ بدلاً من الاستيراد من ميزة إلى أخرى. الاستيرادات من ميزة إلى ميزة هي رائحة معمارية تُبطل الغرض من التقطيع المعياري.

الخلاصة

يُنظِّم الهيكل المعياري القائم على الميزات الكود حسب القدرة التجارية لا حسب النوع التقني. يحتوي كل مجلد ميزة على طبقاته الخاصة من data وdomain وpresentation مع قاعدة تبعية صارمة نحو الداخل. تنتقل البنية التحتية المشتركة إلى core/. والنتيجة قاعدة كود يمكن فيها تطوير الميزات الفردية واختبارها وحتى استخراجها كحزم بشكل مستقل — مما يُحسِّن قابلية التوسع والتعاون الجماعي بشكل كبير مع نمو تطبيق Flutter الخاص بك.