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

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

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

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

في البنية النظيفة (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

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

سوء الفهم الشائع هو أن المستودعات جزء من طبقة البيانات. في البنية النظيفة، الواجهة (الفئة المجردة) تعيش في طبقة المجال، بينما التنفيذ يعيش في طبقة البيانات. هذا الانعكاس في التحكم (مبدأ انعكاس التبعية) يضمن أن طبقة المجال لا تعتمد أبداً على طبقة البيانات — بل العكس دائماً هو الصحيح.

تحذير: إذا استورد Use Case الخاص بك أي شيء من lib/data/، فقد كسرت قاعدة التبعية. يجب على كود المجال أن يرجع فقط إلى كود مجال آخر أو مكتبات Dart النقية. انقل الاستيراد إلى فئة ملموسة في طبقة البيانات وأدخله عبر واجهة المستودع.

هيكلة طبقة المجال

تتبع طبقة المجال المنظمة جيداً هيكلاً ثابتاً للمجلدات:

  • lib/domain/entities/ — جميع فئات الكيانات
  • lib/domain/repositories/ — واجهات المستودع المجردة
  • lib/domain/usecases/ — ملف واحد لكل فئة Use Case
  • lib/domain/value_objects/ — (اختياري) أغلفة صغيرة مُتحقَّق منها مثل Email أو Money

ملخص

طبقة المجال هي أهم طبقة في تطبيقك لأنها تُشفِّر ما يفعله العمل، بصرف النظر عن أي تقنية. الكيانات هي كائنات Dart نقية غير قابلة للتغيير تحمل قواعد الأعمال كخصائص محسوبة وتأكيدات في المنشئ. حالات الاستخدام هي فئات ذات طريقة واحدة تستدعي واجهات المستودع لتنفيذ عملية أعمال واحدة. لا يجوز لأي من الفئتين استيراد Flutter أو أي حزمة بنية تحتية. هذا الانضباط يؤتي ثماره في أول مرة تكتب فيها اختبار وحدة يعمل في ملي ثانية دون الحاجة إلى محاكٍ.

النقطة الرئيسية: طبقة المجال = Dart نقي. الكيانات تحمل بيانات الأعمال وقواعدها. حالات الاستخدام تحمل منطق الأعمال. واجهات المستودع تُعرِّف العقد. كل شيء آخر (HTTP وSQLite وودجات Flutter) يعيش خارج طبقة المجال.