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

طبقة البيانات: النماذج ومصادر البيانات

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

طبقة البيانات: النماذج ومصادر البيانات

في بنية Clean Architecture، طبقة البيانات هي الطبقة الخارجية الأبعد المسؤولة عن جميع عمليات البيانات. وهي تتواصل مباشرةً مع الشبكة وقواعد البيانات والتخزين المحلي. لا يحتاج باقي تطبيقك أبداً إلى معرفة ما إذا كانت البيانات قادمة من واجهة برمجة تطبيقات بعيدة أو ذاكرة تخزين مؤقت محلية — هذه التفاصيل مخفية هنا خلف واجهات محددة بوضوح.

يغطي هذا الدرس مسؤوليتين رئيسيتين لطبقة البيانات: فئات النموذج (Models) التي تتعامل مع عملية التسلسل، وفئات مصدر البيانات (DataSources) التي تنفذ عمليات الإدخال والإخراج الفعلية. يشكّلان معاً البنية التحتية التي تغذّي كائنات Entity نظيفة إلى طبقتَي المجال والعرض.

النماذج (Models) مقابل الكيانات (Entities)

نقطة ارتباك شائعة هي الفرق بين النموذج والكيان:

  • الكيان (Entity) — فئة Dart نقية في طبقة المجال. ليس لها أي معرفة بـ JSON، ولا تستورد أي إطار عمل، ولا تعتمد على أي شيء خارجي.
  • النموذج (Model) — فئة في طبقة البيانات تمتد من الكيان وتضيف تسلسل fromJson / toJson. وهي تعرف العالم الخارجي حتى لا يضطر الكيان لذلك.
ملاحظة: بامتداد النموذج من الكيان، يصبح النموذج كياناً من الناحية التقنية. يمكنك تمرير UserModel في أي مكان يُتوقع فيه كيان User، مما يبقي طبقتَي المجال والعرض منفصلتين تماماً عن تفاصيل التسلسل.

كتابة فئة النموذج

افترض أن كيان المجال يبدو هكذا:

كيان المجال (lib/features/auth/domain/entities/user.dart)

class User {
  final String id;
  final String name;
  final String email;

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

يمتد النموذج المقابل في طبقة البيانات من User ويضيف دعم JSON:

نموذج البيانات (lib/features/auth/data/models/user_model.dart)

import '../../domain/entities/user.dart';

class UserModel extends User {
  const UserModel({
    required super.id,
    required super.name,
    required super.email,
  });

  /// إنشاء نموذج من خريطة JSON (مثلاً: جسم استجابة HTTP).
  factory UserModel.fromJson(Map<String, dynamic> json) {
    return UserModel(
      id: json['id'] as String,
      name: json['name'] as String,
      email: json['email'] as String,
    );
  }

  /// تحويل النموذج إلى خريطة JSON (مثلاً: قبل الكتابة إلى التخزين المحلي).
  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'name': name,
      'email': email,
    };
  }

  /// إنشاء نسخة مع تعديل بعض الحقول.
  UserModel copyWith({String? id, String? name, String? email}) {
    return UserModel(
      id: id ?? this.id,
      name: name ?? this.name,
      email: email ?? this.email,
    );
  }
}
نصيحة: احتفظ بـ fromJson وtoJson حصراً داخل النموذج. إذا استبدلت عميل HTTP أو انتقلت من JSON إلى Protobuf في يوم ما، فستعدّل هذا الملف فقط — ويبقى الكيان وكل ما فوقه دون تغيير.

عقود مصدر البيانات المجردة

مصدر البيانات (DataSource) هو واجهة (فئة مجردة) تُعلن عن عمليات البيانات المتاحة دون أن تحدد كيفية تنفيذها. يمنحك تعريف العقد كفئة مجردة ميزتين جوهريتين:

  • يمكنك استبدال التطبيقات (مثلاً: استبدال مصدر HTTP حقيقي بمصدر وهمي في الاختبارات) دون لمس أي كود يستدعيها.
  • يعتمد المستودع (Repository) على التجريد فقط، لا على أي SDK أو مكتبة محددة.

العقود المجردة (lib/features/auth/data/datasources/auth_remote_data_source.dart)

// عقد مصدر البيانات البعيد
abstract class AuthRemoteDataSource {
  /// يرسل بيانات تسجيل الدخول إلى الـ API ويعيد UserModel عند النجاح.
  /// يرمي [ServerException] عند الفشل.
  Future<UserModel> login({required String email, required String password});

  /// يجلب ملف المستخدم الحالي المصادق عليه.
  Future<UserModel> getProfile();
}

// عقد مصدر البيانات المحلي
abstract class AuthLocalDataSource {
  /// يحفظ نموذج المستخدم في SharedPreferences.
  Future<void> cacheUser(UserModel user);

  /// يعيد آخر مستخدم محفوظ في الذاكرة المؤقتة، أو يرمي [CacheException] إن لم يوجد.
  Future<UserModel> getCachedUser();
}

تطبيقات مصدر البيانات الملموسة

تنفذ الفئات الملموسة العقود المجردة. المصدر البعيد يستخدم عميل HTTP؛ والمصدر المحلي يستخدم shared_preferences. كلتا الفئتين معنيتان فقط بتقنيتهما الخاصة — لا تتسرب هنا أي منطق أعمال.

التطبيقات الملموسة

import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';

// ─── التطبيق البعيد ───────────────────────────────────────────────────────
class AuthRemoteDataSourceImpl implements AuthRemoteDataSource {
  final http.Client client;

  const AuthRemoteDataSourceImpl({required this.client});

  @override
  Future<UserModel> login({
    required String email,
    required String password,
  }) async {
    final response = await client.post(
      Uri.parse('https://api.example.com/auth/login'),
      headers: {'Content-Type': 'application/json'},
      body: jsonEncode({'email': email, 'password': password}),
    );

    if (response.statusCode == 200) {
      return UserModel.fromJson(
        jsonDecode(response.body) as Map<String, dynamic>,
      );
    } else {
      throw ServerException(message: 'Login failed: ${response.statusCode}');
    }
  }

  @override
  Future<UserModel> getProfile() async {
    final response = await client.get(
      Uri.parse('https://api.example.com/auth/me'),
    );
    if (response.statusCode == 200) {
      return UserModel.fromJson(
        jsonDecode(response.body) as Map<String, dynamic>,
      );
    }
    throw ServerException(message: 'Could not fetch profile');
  }
}

// ─── التطبيق المحلي ───────────────────────────────────────────────────────
const _kCachedUser = 'CACHED_USER';

class AuthLocalDataSourceImpl implements AuthLocalDataSource {
  final SharedPreferences prefs;

  const AuthLocalDataSourceImpl({required this.prefs});

  @override
  Future<void> cacheUser(UserModel user) async {
    await prefs.setString(_kCachedUser, jsonEncode(user.toJson()));
  }

  @override
  Future<UserModel> getCachedUser() {
    final jsonString = prefs.getString(_kCachedUser);
    if (jsonString != null) {
      return Future.value(
        UserModel.fromJson(jsonDecode(jsonString) as Map<String, dynamic>),
      );
    }
    throw CacheException(message: 'No cached user found');
  }
}
تحذير: لا تضع قواعد الأعمال أبداً داخل DataSource. إذا وجدت نفسك تكتب if (user.role == 'admin') داخل فئة DataSource، توقف — هذا المنطق ينتمي إلى Use Case في طبقة المجال. مصادر البيانات هي محوّلات إدخال/إخراج غير ذكية فقط.

كيف تتلاءم الأجزاء معاً

يتبع تدفق البيانات قاعدة اعتماد أحادية الاتجاه صارمة:

  • طبقة العرض (Presentation) تستدعي Use Case.
  • Use Case يستدعي واجهة Repository (طبقة المجال).
  • تطبيق Repository (طبقة البيانات) يستدعي عقد DataSource المجرد.
  • DataSource الملموس ينفذ استدعاء HTTP أو SharedPreferences الفعلي ويعيد نموذجاً.
  • يُحوَّل النموذج إلى كيان قبل إعادته عبر السلسلة.
النقطة الرئيسية: طبقة البيانات هي المكان الوحيد في تطبيقك الذي يعرف JSON ورموز حالة HTTP ومفاتيح SharedPreferences والـ SDKs الخارجية. كل ما فوقها يعمل مع Entities نظيفة وعقود مجردة. هذا الفصل يجعل تطبيقك سهل الاختبار والصيانة وإعادة الهيكلة.