طبقة المجال: الكيانات وحالات الاستخدام
طبقة المجال: الكيانات وحالات الاستخدام
في البنية النظيفة (Clean Architecture)، طبقة المجال (Domain layer) هي الدائرة الأعمق — قلب التطبيق. تحتوي على منطق الأعمال النقي وليس لديها أي تبعيات على Flutter أو أي حزمة خارجية أو أي طبقة أخرى. إذا استبدلت يوماً إطار واجهة المستخدم أو محرك قاعدة البيانات، فإن طبقة المجال لا تتغير أبداً. وهذا يجعلها الجزء الأكثر استقراراً والأسهل اختباراً في قاعدة الكود.
تتكون طبقة المجال من مكونين أساسيين: الكيانات (Entities) (نماذج البيانات التي تحمل قواعد الأعمال) وحالات الاستخدام (Use Cases) (فئات ذات مسؤولية واحدة تُنسِّق تلك القواعد). معاً يشكلان مواصفة مكتفية ذاتياً لما يفعله التطبيق، بصرف النظر تماماً عن كيفية فعله.
import) أي شيء من Flutter (package:flutter/...) أو من حزم البنية التحتية مثل Dio أو Hive أو SharedPreferences. يجوز لها فقط استخدام مكتبة Dart الأساسية (dart:core وdart:async وما إلى ذلك).ما هو الكيان (Entity)؟
الكيان هو فئة Dart بسيطة تنمذج مفهوم أعمال جوهرياً. يحمل فقط الحقول وقواعد التحقق التي يهتم بها العمل — ليس أعمدة قاعدة البيانات، وليس مفاتيح JSON، وليس خصائص الودجات. الكيانات عادةً ما تكون غير قابلة للتغيير (immutable): تستخدم حقول final ومنشئ const، وتنشئ نسخة جديدة بدلاً من تغيير النسخة الموجودة.
الخصائص الرئيسية للكيان المُصمَّم جيداً:
- يحتوي فقط على حقول ذات معنى لـمجال الأعمال
- قد يتضمن تحقق على مستوى المجال (مثلاً: السعر لا يمكن أن يكون سالباً)
- غير قابل للتغيير — يفضل حقول
finalويكشف طريقةcopyWithللتحديثات - لا يحتوي على
toJsonأوfromJsonأو تعيين قاعدة بيانات — تلك تنتمي لطبقة البيانات - قد يتجاوز
==وhashCodeوtoStringللراحة
مثال على الكيان — Product
// lib/domain/entities/product.dart
// Dart نقي — بدون Flutter أو Dio أو Hive
class Product {
final String id;
final String name;
final double price;
final int stockQuantity;
final bool isActive;
const Product({
required this.id,
required this.name,
required this.price,
required this.stockQuantity,
this.isActive = true,
}) : assert(price >= 0, 'Price cannot be negative'),
assert(stockQuantity >= 0, 'Stock cannot be negative');
// قاعدة الأعمال: المنتج قابل للشراء فقط إذا كان نشطاً وفي المخزون
bool get isPurchasable => isActive && stockQuantity > 0;
Product copyWith({
String? id,
String? name,
double? price,
int? stockQuantity,
bool? isActive,
}) {
return Product(
id: id ?? this.id,
name: name ?? this.name,
price: price ?? this.price,
stockQuantity: stockQuantity ?? this.stockQuantity,
isActive: isActive ?? this.isActive,
);
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Product && runtimeType == other.runtimeType && id == other.id;
@override
int get hashCode => id.hashCode;
@override
String toString() => 'Product(id: $id, name: $name, price: $price)';
}
assert) في منشئ الكيان لفرض الثوابت وقت البناء. بهذه الطريقة، لا يمكن أن يوجد Product غير صالح في أي مكان في التطبيق — وليس فقط في واجهة المستخدم.ما هي حالة الاستخدام (Use Case)؟
حالة الاستخدام (تُسمى أيضاً Interactor) هي فئة ذات مسؤولية واحدة تُغلِّف قطعة واحدة من منطق الأعمال. اصطلاح التسمية هو عبارة فعلية: GetProductById أو PlaceOrder أو AuthenticateUser. كل حالة استخدام:
- تفعل شيئاً واحداً فقط — مبدأ المسؤولية الواحدة مُطبَّق بصرامة
- تستدعي واجهة المستودع (Repository interface) (فئة مجردة مُعرَّفة في طبقة المجال) لجلب البيانات أو حفظها
- تُعيد قيمة بسيطة أو
EntityأوFuture/Streamمن تلك - لا تحتوي على كود واجهة مستخدم ولا كود شبكة/قاعدة بيانات
- سهلة الاختبار لأن كل تبعية يمكن محاكاتها
مثال واجهة المستودع وحالة الاستخدام
// lib/domain/repositories/product_repository.dart
// عقد مجرد — التنفيذ يعيش في طبقة البيانات
abstract class ProductRepository {
Future<List<Product>> fetchAll();
Future<Product> fetchById(String id);
Future<void> save(Product product);
}
// lib/domain/usecases/get_purchasable_products.dart
class GetPurchasableProducts {
final ProductRepository _repository;
const GetPurchasableProducts(this._repository);
// الطريقة العامة الوحيدة — call() تجعل الفئة قابلة للاستدعاء
Future<List<Product>> call() async {
final all = await _repository.fetchAll();
// قاعدة الأعمال: تصفية المنتجات القابلة للشراء فقط
return all.where((p) => p.isPurchasable).toList();
}
}
// الاستخدام (مثلاً في ViewModel أو Cubit):
// final useCase = GetPurchasableProducts(productRepository);
// final products = await useCase(); // يستدعي call() عبر صيغة الاستدعاء في Dart
واجهات المستودع تنتمي لطبقة المجال
سوء الفهم الشائع هو أن المستودعات جزء من طبقة البيانات. في البنية النظيفة، الواجهة (الفئة المجردة) تعيش في طبقة المجال، بينما التنفيذ يعيش في طبقة البيانات. هذا الانعكاس في التحكم (مبدأ انعكاس التبعية) يضمن أن طبقة المجال لا تعتمد أبداً على طبقة البيانات — بل العكس دائماً هو الصحيح.
lib/data/، فقد كسرت قاعدة التبعية. يجب على كود المجال أن يرجع فقط إلى كود مجال آخر أو مكتبات Dart النقية. انقل الاستيراد إلى فئة ملموسة في طبقة البيانات وأدخله عبر واجهة المستودع.هيكلة طبقة المجال
تتبع طبقة المجال المنظمة جيداً هيكلاً ثابتاً للمجلدات:
lib/domain/entities/— جميع فئات الكياناتlib/domain/repositories/— واجهات المستودع المجردةlib/domain/usecases/— ملف واحد لكل فئة Use Caselib/domain/value_objects/— (اختياري) أغلفة صغيرة مُتحقَّق منها مثلEmailأوMoney
ملخص
طبقة المجال هي أهم طبقة في تطبيقك لأنها تُشفِّر ما يفعله العمل، بصرف النظر عن أي تقنية. الكيانات هي كائنات Dart نقية غير قابلة للتغيير تحمل قواعد الأعمال كخصائص محسوبة وتأكيدات في المنشئ. حالات الاستخدام هي فئات ذات طريقة واحدة تستدعي واجهات المستودع لتنفيذ عملية أعمال واحدة. لا يجوز لأي من الفئتين استيراد Flutter أو أي حزمة بنية تحتية. هذا الانضباط يؤتي ثماره في أول مرة تكتب فيها اختبار وحدة يعمل في ملي ثانية دون الحاجة إلى محاكٍ.