العمارة النظيفة: الطبقات والمبادئ
العمارة النظيفة: الطبقات والمبادئ
العمارة النظيفة (Clean Architecture)، التي قدّمها روبرت سي. مارتن (Uncle Bob)، هي فلسفة تصميم برمجي تفصل التطبيق إلى طبقات متحدة المركز من المسؤوليات. في Flutter، يعني ذلك هيكلة قاعدة الكود بحيث تكون منطق الأعمال مستقلاً تماماً عن إطار واجهة المستخدم وقواعد البيانات والخدمات الخارجية. والنتيجة كود قابل للاختبار والصيانة والتكيّف مع التغيير.
الطبقات الثلاث المتحدة المركز
تُنظَّم العمارة النظيفة في 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 وإدارة الحالة). تُلزم قاعدة التبعية بأن يشير كل سهم تبعية للداخل — الطبقات الخارجية تعتمد على الداخلية، لا العكس. تطبيق هذا الحد يُبقي النطاق وحالات استخدامه قابلة للاختبار المعزول ويجعل قاعدة الكود بأكملها مرنة أمام التغيير.
dart:async أو مكتبات Dart الأساسية فقط، فمن المحتمل أنه لا ينتمي لطبقة النطاق.