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

العمارة النظيفة: الطبقات والمبادئ

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

العمارة النظيفة: الطبقات والمبادئ

العمارة النظيفة (Clean Architecture)، التي قدّمها روبرت سي. مارتن (Uncle Bob)، هي فلسفة تصميم برمجي تفصل التطبيق إلى طبقات متحدة المركز من المسؤوليات. في Flutter، يعني ذلك هيكلة قاعدة الكود بحيث تكون منطق الأعمال مستقلاً تماماً عن إطار واجهة المستخدم وقواعد البيانات والخدمات الخارجية. والنتيجة كود قابل للاختبار والصيانة والتكيّف مع التغيير.

لماذا العمارة النظيفة؟ عندما تكون كل ميزة متشابكة مع ودجات Flutter واستدعاءات API واستعلامات قاعدة البيانات في ملف واحد، فإن أي تغيير ينتشر بشكل غير متوقع في التطبيق. تفرض العمارة النظيفة حدوداً صارمة بحيث يستلزم استبدال REST API بـ GraphQL، أو استبدال قاعدة بيانات محلية بقاعدة سحابية، التعامل مع الطبقة الخارجية فقط — دون المساس بمنطق الأعمال.

الطبقات الثلاث المتحدة المركز

تُنظَّم العمارة النظيفة في Flutter عادةً في ثلاث طبقات مرتبة من الأعمق (الأكثر استقراراً) إلى الأخارج (الأكثر تقلباً):

١. طبقة النطاق (الأعمق)

طبقة النطاق (Domain) هي قلب التطبيق. تحتوي على كود Dart نقي — لا استيرادات Flutter، ولا حزم خارجية، ولا تبعيات شبكة أو قاعدة بيانات. تُعرِّف:

  • الكيانات (Entities) — فئات Dart بسيطة تُنمذج كائنات الأعمال الأساسية (مثل User وProduct وOrder)
  • واجهات المستودعات (فئات مجردة) — عقود تصف عمليات البيانات الممكنة دون تحديد كيفية تنفيذها
  • حالات الاستخدام (Use Cases) — فئات ذات مسؤولية واحدة تُنفّذ قاعدة عمل واحدة (مثل GetUserByIdUseCase وPlaceOrderUseCase)

بما أن طبقة النطاق لا تملك تبعيات خارجية، يمكن اختبارها بـ dart test البسيط — دون الحاجة لمحاكاة أي مكون من Flutter.

٢. طبقة البيانات (الوسطى)

طبقة البيانات (Data) تُنفّذ واجهات المستودعات المُعرَّفة في طبقة النطاق. هي مسؤولة عن كل عمليات البيانات وتعرف الأنظمة الخارجية كـ REST APIs وقواعد بيانات SQLite المحلية وFirebase وتخزين الجهاز. تحتوي على:

  • تنفيذات المستودعات — فئات ملموسة تُحقق عقود النطاق
  • مصادر البيانات — البعيدة (عملاء HTTP) والمحلية (مساعدو قاعدة البيانات، التفضيلات المشتركة)
  • نماذج البيانات (DTOs) — فئات بـ fromJson/toJson تُعيِّن استجابات API/قاعدة البيانات الخام إلى كيانات النطاق

٣. طبقة العرض (الخارجية)

طبقة العرض (Presentation) هي موطن Flutter. الودجات والصفحات وإدارة الحالة (Bloc وRiverpod وProvider إلخ) تنتمي هنا. تستدعي حالات الاستخدام من طبقة النطاق وتعرض نتائجها. تحتوي على:

  • الصفحات / الشاشات — فئات فرعية من Widget في Flutter تبني واجهة المستخدم
  • فئات إدارة الحالة — Cubits وViewModels وNotifiers وProviders
  • ودجات الواجهة المشتركة — مكونات قابلة لإعادة الاستخدام خاصة بهذا التطبيق

قاعدة التبعية

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

يجب أن تشير تبعيات الكود المصدري للداخل فقط. لا يجوز لأي طبقة داخلية أن تعرف أي شيء عن طبقة خارجية.

بشكل ملموس:

  • طبقة النطاق لا تستورد شيئاً من البيانات أو العرض
  • طبقة البيانات تستورد النطاق (لتنفيذ الواجهات وإعادة الكيانات)، لكن لا تستورد العرض أبداً
  • طبقة العرض تستورد النطاق (لاستدعاء حالات الاستخدام)، وقد تستورد البيانات فقط عبر حقن التبعية — لا عبر استدعاء تنفيذ المستودع مباشرةً بالاسم
انتهاك شائع: استيراد UserModel (DTO من طبقة البيانات) مباشرةً داخل Cubit أو ودجت هو انتهاك لقاعدة التبعية. يجب أن ترى طبقة العرض User فقط (كيان النطاق). التعيين يحدث داخل تنفيذ المستودع في طبقة البيانات.

هيكل مجلدات Flutter الموصى به

يُنظِّم مشروع Flutter النظيف الكود حسب الميزة، مع احتواء كل ميزة على مجلداتها الفرعية الثلاثة:

lib/
  features/
    auth/
      domain/
        entities/
          user.dart            // كيان Dart نقي
        repositories/
          auth_repository.dart // واجهة مجردة
        usecases/
          login_usecase.dart
          logout_usecase.dart
      data/
        models/
          user_model.dart      // DTO مع fromJson/toJson
        datasources/
          auth_remote_datasource.dart
          auth_local_datasource.dart
        repositories/
          auth_repository_impl.dart
      presentation/
        pages/
          login_page.dart
        widgets/
          login_form.dart
        bloc/
          auth_bloc.dart
          auth_event.dart
          auth_state.dart
  core/
    error/
      failures.dart
      exceptions.dart
    usecases/
      usecase.dart             // واجهة UseCase الأساسية
    di/
      injection_container.dart // إعداد حقن التبعية

مثال كود طبقة النطاق

هكذا تبدو طبقة النطاق بكود Dart نقي — بلا استيرادات Flutter أو مكتبات خارجية:

// 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,
  });
}

// lib/features/auth/domain/repositories/auth_repository.dart
// عقد مجرد — طبقة البيانات يجب أن تُنفّذه
abstract class AuthRepository {
  Future<User> login({required String email, required String password});
  Future<void> logout();
  Future<User?> getCurrentUser();
}

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

  const LoginUseCase(this.repository);

  // طريقة عامة واحدة — تُغلّف إجراء عمل واحداً
  Future<User> call({required String email, required String password}) {
    return repository.login(email: email, password: password);
  }
}

مثال كود طبقة البيانات

طبقة البيانات تُنفّذ العقد وتتعامل مع تعيين JSON. لاحظ أنها تستورد كيان النطاق لكن لا تستورد طبقة العرض أبداً:

// lib/features/auth/data/models/user_model.dart
import 'package:myapp/features/auth/domain/entities/user.dart';

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

  factory UserModel.fromJson(Map<String, dynamic> json) {
    return UserModel(
      id: json['id'] as String,
      name: json['name'] as String,
      email: json['email'] as String,
    );
  }

  Map<String, dynamic> toJson() => {
    'id': id,
    'name': name,
    'email': email,
  };
}

// lib/features/auth/data/repositories/auth_repository_impl.dart
import 'package:myapp/features/auth/domain/entities/user.dart';
import 'package:myapp/features/auth/domain/repositories/auth_repository.dart';
import 'package:myapp/features/auth/data/datasources/auth_remote_datasource.dart';

class AuthRepositoryImpl implements AuthRepository {
  final AuthRemoteDataSource remoteDataSource;

  const AuthRepositoryImpl(this.remoteDataSource);

  @override
  Future<User> login({required String email, required String password}) async {
    // يُعيد UserModel (بيانات)، لكن المستدعي يرى User (نطاق) فقط
    return remoteDataSource.login(email: email, password: password);
  }

  @override
  Future<void> logout() => remoteDataSource.logout();

  @override
  Future<User?> getCurrentUser() => remoteDataSource.getCurrentUser();
}

الخلاصة

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

نصيحة: ابدأ كل ميزة جديدة بكتابة طبقة النطاق أولاً (الكيانات وواجهة المستودع وحالات الاستخدام) — دون أي استيرادات من Flutter أو حزم خارجية. إذا لم تستطع كتابة الملف باستيرادات dart:async أو مكتبات Dart الأساسية فقط، فمن المحتمل أنه لا ينتمي لطبقة النطاق.