معالجة الأخطاء مع البرمجة كائنية التوجه
لماذا تهم معالجة الأخطاء مع OOP
في أي تطبيق حقيقي، تسوء الأمور: طلبات الشبكة تفشل، مدخلات المستخدم غير صالحة، الملفات مفقودة، وقواعد العمل مُنتهكة. فئات Exception و Error المدمجة في Dart توفر أساساً، لكن الكود الاحترافي يتطلب تسلسلات هرمية مخصصة للاستثناءات تجعل معالجة الأخطاء دقيقة وقابلة للقراءة والصيانة. بدمج مبادئ OOP مع معالجة الأخطاء، تُنشئ أنظمة حيث الأخطاء مُنمّطة وقابلة للالتقاط على المستوى الصحيح وغنية بالمعلومات.
في هذا الدرس، ستتعلم بناء تسلسلات هرمية لفئات الاستثناءات، واستخدام نمط Result لمعالجة الأخطاء الوظيفية، وتطبيق الفئات المختومة لأنواع الأخطاء الشاملة -- تقنيات مستخدمة في تطبيقات Flutter و Dart الإنتاجية.
Error مقابل Exception في Dart
قبل بناء أنواع مخصصة، افهم التمييز الذي يجريه Dart بين Error و Exception:
Error-- يمثل خطأ برمجي (علّة). أمثلة:TypeError،RangeError،StateError. هذه لا يجب التقاطها في الكود العادي؛ فهي تشير إلى أن البرنامج معطوب.Exception-- يمثل حالة قابلة للتعافي. أمثلة:FormatException،IOException،HttpException. هذه يجب التقاطها ومعالجتها بأناقة.
Error مقابل Exception
void main() {
// Error -- خطأ برمجي، لا يجب التقاطه عادة
List<int> numbers = [1, 2, 3];
// numbers[10]; // RangeError -- هذه علّة في كودك
// Exception -- حالة قابلة للتعافي، يجب التقاطها
try {
int result = int.parse('not_a_number');
} on FormatException catch (e) {
print('صيغة غير صالحة: $e'); // معالجة بأناقة
}
}
Exception للأشياء التي قد يرغب المُستدعي في التعافي منها. احتفظ بفئات Error الفرعية للأخطاء البرمجية التي يجب إصلاحها في الكود.إنشاء فئات استثناء مخصصة
الاستثناءات المخصصة تتيح لك حمل بيانات خطأ محددة وإنشاء أنواع أخطاء ذات معنى لنطاق تطبيقك. نفّذ دائماً واجهة Exception (وليس Error).
استثناء مخصص أساسي
class ValidationException implements Exception {
final String field;
final String message;
const ValidationException({
required this.field,
required this.message,
});
@override
String toString() => 'ValidationException: $field -- $message';
}
class NotFoundException implements Exception {
final String entityType;
final String id;
const NotFoundException({
required this.entityType,
required this.id,
});
@override
String toString() => '$entityType بالمعرف "$id" غير موجود';
}
void main() {
try {
throw ValidationException(
field: 'email',
message: 'صيغة بريد إلكتروني غير صالحة',
);
} on ValidationException catch (e) {
print(e); // ValidationException: email -- صيغة بريد إلكتروني غير صالحة
print(e.field); // email
print(e.message); // صيغة بريد إلكتروني غير صالحة
}
}
بناء تسلسلات هرمية للاستثناءات
التطبيقات الحقيقية تحتاج عائلات من الاستثناءات المترابطة. التسلسل الهرمي للاستثناءات يتيح لك التقاط الأخطاء على مستويات مختلفة من التحديد -- التقاط فئة عامة أو نوع فرعي محدد.
تسلسل هرمي للاستثناءات لعميل API
// استثناء أساسي لجميع أخطاء API
abstract class ApiException implements Exception {
final String message;
final int? statusCode;
final String? requestUrl;
const ApiException({
required this.message,
this.statusCode,
this.requestUrl,
});
@override
String toString() => 'ApiException($statusCode): $message';
}
// أخطاء مستوى الشبكة
class NetworkException extends ApiException {
final Duration? timeout;
const NetworkException({
required super.message,
super.requestUrl,
this.timeout,
}) : super(statusCode: null);
}
// الخادم أرجع استجابة خطأ
class ServerException extends ApiException {
final String? responseBody;
const ServerException({
required super.message,
required int super.statusCode,
super.requestUrl,
this.responseBody,
});
}
// أخطاء المصادقة/التفويض
class AuthException extends ApiException {
final bool tokenExpired;
const AuthException({
required super.message,
super.statusCode,
super.requestUrl,
this.tokenExpired = false,
});
}
// تحديد المعدل
class RateLimitException extends ApiException {
final Duration retryAfter;
const RateLimitException({
required this.retryAfter,
super.requestUrl,
}) : super(message: 'تم تجاوز حد المعدل', statusCode: 429);
}
// الاستخدام -- التقاط على مستويات مختلفة
void handleApiCall() {
try {
// ... إجراء استدعاء API
throw ServerException(
message: 'خطأ داخلي في الخادم',
statusCode: 500,
requestUrl: '/api/users',
responseBody: '{"error": "قاعدة البيانات غير متوفرة"}',
);
} on AuthException catch (e) {
// معالجة أخطاء المصادقة بشكل محدد
if (e.tokenExpired) {
print('انتهت صلاحية الرمز، جاري التحديث...');
} else {
print('غير مصرح: ${e.message}');
}
} on RateLimitException catch (e) {
// معالجة تحديد المعدل
print('تم تحديد المعدل. أعد المحاولة بعد ${e.retryAfter.inSeconds} ثانية');
} on ServerException catch (e) {
// معالجة أخطاء الخادم
print('خطأ الخادم ${e.statusCode}: ${e.message}');
} on NetworkException catch (e) {
// معالجة أخطاء الشبكة
print('خطأ الشبكة: ${e.message}');
} on ApiException catch (e) {
// التقاط شامل لأي استثناء API
print('خطأ API: ${e.message}');
}
}
catch من الأكثر تحديداً إلى الأكثر عمومية. Dart يطابق أول نوع مطابق، لذا وضع ApiException أولاً سيلتقط كل شيء ولن تعمل المعالجات المحددة أبداً.Try-Catch-Finally مع OOP
كتلة finally تُنفَّذ بغض النظر عما إذا تم رمي استثناء. هذا ضروري لعمليات التنظيف مثل إغلاق الاتصالات وتحرير الموارد أو إعادة تعيين الحالة.
إدارة الموارد مع Try-Catch-Finally
class DatabaseConnection {
final String connectionString;
bool _isOpen = false;
DatabaseConnection(this.connectionString);
void open() {
print('فتح اتصال بـ $connectionString');
_isOpen = true;
}
void close() {
if (_isOpen) {
print('إغلاق اتصال بـ $connectionString');
_isOpen = false;
}
}
List<Map<String, dynamic>> query(String sql) {
if (!_isOpen) {
throw StateError('الاتصال غير مفتوح');
}
// محاكاة استعلام قد يفشل
if (sql.contains('invalid_table')) {
throw DatabaseException(
message: 'الجدول "invalid_table" غير موجود',
query: sql,
);
}
return [
{'id': 1, 'name': 'أليس'},
];
}
}
class DatabaseException implements Exception {
final String message;
final String? query;
const DatabaseException({required this.message, this.query});
@override
String toString() => 'DatabaseException: $message';
}
void fetchUsers() {
final db = DatabaseConnection('localhost:5432/mydb');
try {
db.open();
final results = db.query('SELECT * FROM users');
print('تم العثور على ${results.length} مستخدمين');
} on DatabaseException catch (e) {
print('خطأ قاعدة بيانات: ${e.message}');
if (e.query != null) {
print('الاستعلام الفاشل: ${e.query}');
}
} on StateError catch (e) {
print('خطأ حالة: $e');
} finally {
// يُنفَّذ دائماً -- يضمن إغلاق الاتصال
db.close();
}
}
return داخل كتلة finally. بينما يسمح Dart بذلك، فإن return في finally ستتجاوز أي استثناء كان يتم رميه، مما يبتلع الخطأ بصمت.الفئات المختومة لأنواع الأخطاء
قدّم Dart 3 الفئات المختومة (sealed classes)، وهي مثالية لنمذجة مجموعة مغلقة من أنواع الأخطاء. يمكن للمترجم التحقق من أنك تعالج كل نوع خطأ ممكن في تعبير switch -- بدون حالات مفقودة.
أنواع أخطاء مختومة
sealed class AppError {
final String message;
const AppError(this.message);
}
class NetworkError extends AppError {
final int? statusCode;
const NetworkError(super.message, {this.statusCode});
}
class ValidationError extends AppError {
final Map<String, String> fieldErrors;
const ValidationError(super.message, {required this.fieldErrors});
}
class StorageError extends AppError {
final String path;
const StorageError(super.message, {required this.path});
}
class AuthenticationError extends AppError {
final bool sessionExpired;
const AuthenticationError(super.message, {this.sessionExpired = false});
}
// المترجم يضمن معالجة جميع الأنواع الفرعية
String handleError(AppError error) {
return switch (error) {
NetworkError(statusCode: var code) =>
'مشكلة شبكة${code != null ? " (HTTP $code)" : ""}',
ValidationError(fieldErrors: var errors) =>
'إدخال غير صالح: ${errors.entries.map((e) => "${e.key}: ${e.value}").join(", ")}',
StorageError(path: var p) =>
'خطأ تخزين في $p: ${error.message}',
AuthenticationError(sessionExpired: true) =>
'انتهت جلستك. يرجى تسجيل الدخول مرة أخرى.',
AuthenticationError() =>
'فشل المصادقة: ${error.message}',
};
// لا حاجة لـ default -- المترجم يعلم أن جميع الحالات مغطاة!
}
PermissionError)، سيعرض المترجم أخطاء في كل عبارة switch لا تعالج النوع الجديد. هذا يجعل إعادة الهيكلة آمنة.نمط Result
بدلاً من رمي الاستثناءات، يمكنك إرجاع نوع Result يمثل صراحةً إما النجاح أو الفشل. هذا يجعل معالجة الأخطاء جزءاً من توقيع الدالة -- المستدعون لا يمكنهم نسيان معالجة الأخطاء لأن نوع الإرجاع يجبرهم على الفحص.
تنفيذ نمط Result
// نوع Result عام باستخدام فئات مختومة
sealed class Result<T> {
const Result();
}
class Success<T> extends Result<T> {
final T value;
const Success(this.value);
}
class Failure<T> extends Result<T> {
final AppError error;
const Failure(this.error);
}
// طرق إضافية للراحة
extension ResultExtensions<T> on Result<T> {
bool get isSuccess => this is Success<T>;
bool get isFailure => this is Failure<T>;
T? get valueOrNull => switch (this) {
Success(value: var v) => v,
Failure() => null,
};
Result<R> map<R>(R Function(T value) transform) => switch (this) {
Success(value: var v) => Success(transform(v)),
Failure(error: var e) => Failure(e),
};
T getOrElse(T Function(AppError error) orElse) => switch (this) {
Success(value: var v) => v,
Failure(error: var e) => orElse(e),
};
}
// الاستخدام في خدمة
class UserService {
final Map<String, Map<String, String>> _users = {
'1': {'name': 'أليس', 'email': 'alice@example.com'},
'2': {'name': 'بوب', 'email': 'bob@example.com'},
};
Result<Map<String, String>> getUserById(String id) {
final user = _users[id];
if (user == null) {
return Failure(NetworkError('المستخدم غير موجود', statusCode: 404));
}
return Success(user);
}
Result<String> getUserEmail(String id) {
return getUserById(id).map((user) => user['email']!);
}
}
void main() {
final service = UserService();
// يجب معالجة كلتا الحالتين -- لا يمكن تجاهل الأخطاء
final result = service.getUserById('1');
switch (result) {
case Success(value: var user):
print('تم العثور على: ${user["name"]}');
case Failure(error: var error):
print('خطأ: ${error.message}');
}
// أو استخدم طرق الراحة
final email = service.getUserEmail('99').getOrElse(
(error) => 'unknown@example.com',
);
print('البريد الإلكتروني: $email'); // البريد الإلكتروني: unknown@example.com
}
Result مفيد بشكل خاص في Flutter حيث تريد عرض حالات واجهة مستخدم مختلفة (تحميل، نجاح، خطأ) بناءً على نتيجة عملية غير متزامنة. مع الفئات المختومة، يضمن المترجم معالجة كل حالة.مثال عملي: معالجة أخطاء API كاملة
لنبنِ نظام معالجة أخطاء كاملاً بجودة إنتاجية لعميل REST API. هذا يجمع بين التسلسلات الهرمية للاستثناءات والفئات المختومة ونمط Result.
معالجة أخطاء API إنتاجية
// ---- أنواع الأخطاء (مختومة) ----
sealed class ApiError {
final String message;
final String? requestUrl;
final DateTime timestamp;
ApiError(this.message, {this.requestUrl})
: timestamp = DateTime.now();
}
class ConnectionError extends ApiError {
final String reason;
ConnectionError({required this.reason, String? requestUrl})
: super('فشل الاتصال: $reason', requestUrl: requestUrl);
}
class HttpError extends ApiError {
final int statusCode;
final Map<String, dynamic>? body;
HttpError({
required this.statusCode,
required String message,
this.body,
String? requestUrl,
}) : super(message, requestUrl: requestUrl);
bool get isClientError => statusCode >= 400 && statusCode < 500;
bool get isServerError => statusCode >= 500;
}
class ParseError extends ApiError {
final String rawResponse;
ParseError({required this.rawResponse, String? requestUrl})
: super('فشل تحليل الاستجابة', requestUrl: requestUrl);
}
class TimeoutError extends ApiError {
final Duration duration;
TimeoutError({required this.duration, String? requestUrl})
: super('انتهت مهلة الطلب بعد ${duration.inSeconds} ثانية',
requestUrl: requestUrl);
}
// ---- نوع النتيجة ----
sealed class ApiResult<T> {
const ApiResult();
}
class ApiSuccess<T> extends ApiResult<T> {
final T data;
final int statusCode;
const ApiSuccess(this.data, {this.statusCode = 200});
}
class ApiFailure<T> extends ApiResult<T> {
final ApiError error;
const ApiFailure(this.error);
}
// ---- عميل API ----
class ApiClient {
final String baseUrl;
ApiClient(this.baseUrl);
Future<ApiResult<Map<String, dynamic>>> get(String path) async {
final url = '$baseUrl$path';
try {
// محاكاة استدعاء شبكة
await Future.delayed(Duration(milliseconds: 100));
// محاكاة استجابات مختلفة
if (path.contains('timeout')) {
return ApiFailure(TimeoutError(
duration: Duration(seconds: 30),
requestUrl: url,
));
}
if (path.contains('not-found')) {
return ApiFailure(HttpError(
statusCode: 404,
message: 'المورد غير موجود',
requestUrl: url,
));
}
return ApiSuccess({'id': 1, 'name': 'مستخدم تجريبي'});
} catch (e) {
return ApiFailure(ConnectionError(
reason: e.toString(),
requestUrl: url,
));
}
}
}
// ---- استخدام النظام ----
Future<void> main() async {
final client = ApiClient('https://api.example.com');
final result = await client.get('/users/1');
final output = switch (result) {
ApiSuccess(data: var user, statusCode: var code) =>
'[$code] المستخدم: ${user["name"]}',
ApiFailure(error: ConnectionError(reason: var r)) =>
'لا اتصال: $r',
ApiFailure(error: HttpError(statusCode: var code, :var message)) =>
'HTTP $code: $message',
ApiFailure(error: ParseError(rawResponse: var raw)) =>
'استجابة سيئة: ${raw.substring(0, 50)}...',
ApiFailure(error: TimeoutError(duration: var d)) =>
'انتهت المهلة بعد ${d.inSeconds} ثانية',
};
print(output); // [200] المستخدم: مستخدم تجريبي
}
Error (مثل StackOverflowError أو OutOfMemoryError) في معالجة أخطاء API. هذه تمثل فشلاً حرجاً لا يمكن لتطبيقك التعافي منه بشكل معقول. التقط فقط أنواع Exception وفئات الأخطاء الخاصة بك.