Error HandlingError Handling

قصة قصيرة :

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

السيناريو هنا هو بناء “خدمة” خاصة تسمى “إدارة الأخطاء”، تعمل مثل حارس يراقب هذه الأخطاء ويتعامل معها بدلاً من أن يتوقف التطبيق فجأة. الفكرة الأساسية هي:

  1. الجزء الرئيسي (ِAbstract Class ): هناك فئة عامة تسمى ErrorHandler، وهي مثل خطة عامة تقول: “إذا حدث خطأ، قم بتحويله إلى شيء يمكن فهمه ومعالجته”. هذه الفئة لا تفعل شيئاً بنفسها، لكنها توفر القواعد للأجزاء الأخرى.
  2. الأجزاء الفرعية: لكل أداة (مثل Supabase أو Firebase)، نصنع فئة صغيرة ترث من ErrorHandler وتتعامل مع أخطاء تلك الأداة بشكل خاص. على سبيل المثال، إذا فشل Supabase، تحول الخطأ إلى رسالة واضحة مثل “مشكلة في الخادم”.
  3. كيفية الاستخدام: في كود التطبيق، عندما تقوم بعملية قد تفشل (مثل جلب بيانات)، تضعها داخل دالة آمنة تسمى safeCall. إذا نجحت، تعطيك النتيجة الصحيحة. إذا فشلت، تعطيك وصفاً للخطأ باستخدام مكتبة Dartz، التي تفصل بين “النجاح” و”الفشل” بطريقة مرتبة.
  4. المدير: هناك فئة تسمى ErrorManager تساعد في اختيار الفئة الفرعية المناسبة تلقائياً، مثل قول: “هذا خطأ من Supabase، استخدم معالج Supabase”.

بهذه الطريقة، يصبح تطبيقك أقوى وأسهل في الإصلاح، ويمكنك إضافة دعم لأدوات جديدة في المستقبل دون تغيير الكثير من الكود. هذا يشبه بناء منزل مع أبواب طوارئ، حيث يستمر المنزل في العمل حتى لو حدث حريق صغير.

المقدمة: أهمية إدارة الأخطاء في البرمجة

في مجال تطوير البرمجيات، تعد عملية إدارة الأخطاء (Error Handling) عنصراً حاسماً لضمان استقرار التطبيقات وموثوقيتها. بدون آلية فعالة للتعامل مع الأخطاء، قد يتعرض البرنامج للانهيار المفاجئ، مما يؤدي إلى فقدان البيانات أو تجربة مستخدم سيئة. في سياق إطار Flutter، الذي يعتمد على لغة Dart، تبرز أهمية هذه العملية بشكل خاص نظراً لتكامل التطبيقات مع خدمات خارجية مثل قواعد البيانات (Supabase، Firebase) والتخزين المحلي (Hive).

تساهم إدارة الأخطاء الفعالة في تحقيق عدة أهداف رئيسية:

  • تعزيز الاستقرار: تمكن من التعامل مع الحالات غير المتوقعة، مثل فشل الاتصال بالشبكة أو أخطاء في قراءة البيانات، دون إيقاف التطبيق كلياً.
  • تحسين الأمان: تمنع تسرب المعلومات الحساسة أثناء معالجة الأخطاء وتحمي من الثغرات الأمنية.
  • التوسعية والصيانة: تسمح بإضافة دعم لأخطاء جديدة دون إعادة كتابة الكود الأساسي، مما يجعل التطبيق أكثر مرونة للتطورات المستقبلية.
  • تحليل الأداء: تسهل تسجيل الأخطاء لمراقبتها وتحسين الجودة العامة للبرنامج.

في هذا المقال، سنقدم نموذجاً عملياً لخدمة إدارة الأخطاء مبنية على فئة مجردة (Abstract Class) وفئات فرعية، مع استخدام مكتبة Dartz لفصل النتائج الناجحة عن الفاشلة بطريقة وظيفية. هذا التصميم يتبع أفضل الممارسات مثل مبدأ التبعية المعكوسة (Dependency Inversion Principle) وفصل المسؤوليات (Separation of Concerns)، مما يجعله مناسباً لأي مطور يرغب في دمجه مباشرة في مشروعه. الكود المقدم جاهز للنسخ واللصق، مع شرح مفصل لكل قسم.

التصميم العام للخدمة

يعتمد التصميم على:

  • فئة مجردة (ErrorHandler): توفر واجهة عامة لمعالجة الأخطاء.
  • فئات فرعية: كل واحدة مخصصة لمكتبة معينة (مثل Supabase أو Firebase).
  • استخدام Dartz: لإرجاع نتائج من نوع Either<Failure, T>، حيث يمثل Left<Failure> الفشل وRight<T> النجاح.
  • مدير الأخطاء (ErrorManager): يساعد في اختيار الـ Handler المناسب.

للبدء، أضف التبعيات التالية إلى ملف pubspec.yaml في مشروعك:

YAML

dependencies:
  dartz: ^0.10.1  # للبرمجة الوظيفية
  supabase_flutter: ^2.0.0  # إذا كنت تستخدم Supabase
  firebase_core: ^2.0.0  # إذا كنت تستخدم Firebase
  hive: ^2.0.0  # إذا كنت تستخدم Hive
  # اختياري: logger: ^1.1.0 للتسجيل

الآن، دعونا نستعرض الكود خطوة بخطوة مع الشرح.

1. تعريف فئات الفشل (Failures)

تبدأ الخدمة بتعريف Abstract Class للفشل، مع Classes فرعية لتصنيف كل نوع خطأ بدقة. هذا يسمح بعرض رسائل مخصصة أو تسجيلها بشكل منهجي.

Failures هي فئة مجردة (abstract class) تعمل كقالب عام لجميع أنواع الأخطاء المحتملة في التطبيق. لا تقوم هذه الفئة بتنفيذ أي عمليات محددة بنفسها، بل توفر هيكلاً مشتركاً يشمل خصائص أساسية مثل الرسالة الوصفية للخطأ (message) ورمز الخطأ الاختياري (code). هذا الهيكل يضمن توحيد طريقة التعامل مع الأخطاء عبر التطبيق بأكمله، مما يسهل عرض رسائل واضحة للمستخدم أو تسجيل التفاصيل لأغراض التصحيح.

Dart

import 'package:dartz/dartz.dart';

// فئة مجردة لتمثيل الفشل العام
abstract class Failure {
  final String message;
  final int? code;

  Failure(this.message, {this.code});
}

// فئات فرعية لأنواع محددة من الفشل
class ServerFailure extends Failure {
  ServerFailure(String message, {int? code}) : super(message, code: code);
}

class DatabaseFailure extends Failure {
  DatabaseFailure(String message, {int? code}) : super(message, code: code);
}

class CacheFailure extends Failure {
  CacheFailure(String message, {int? code}) : super(message, code: code);
}

class UnknownFailure extends Failure {
  UnknownFailure(String message) : super(message);
}

الشرح:

  • Failure هي الفئة الأساسية التي تحتوي على رسالة الخطأ ورمز اختياري.
  • الفئات الفرعية مثل ServerFailure تُستخدم لتمييز أخطاء الخادم عن أخطاء التخزين، مما يسهل التعامل معها في واجهة المستخدم (مثل عرض تنبيهات مختلفة).

2. الفئة المجردة لإدارة الأخطاء (ErrorHandler)

هذه الفئة توفر الواجهة الرئيسية للخدمة، ويمكن توسيعها بسهولة.

Dart

abstract class ErrorHandler {
  // دالة لمعالجة الاستثناء وإرجاع فشل
  Failure handleError(dynamic error);

  // دالة مساعدة لتغليف الدوال غير المتزامنة (اختياري، لتسهيل الاستخدام)
  Future<Either<Failure, T>> safeCall<T>(Future<T> Function() call) async {
    try {
      final result = await call();
      return Right(result);
    } catch (e) {
      return Left(handleError(e));
    }
  }
}

الشرح:

  • handleError تحول أي استثناء إلى كائن Failure مناسب.
  • safeCall تغلف أي دالة غير متزامنة لإرجاع Either، مما يبسط الاستخدام في الكود الرئيسي ويمنع رمي الاستثناءات غير المتوقعة.

3. الفئات الفرعية لكل مكتبة

كل فئة ترث من ErrorHandler وتنفذ handleError بناءً على أخطاء المكتبة الخاصة بها. يمكن إضافة فئات جديدة لمكتبات أخرى دون تعديل الكود القائم.

  • SupabaseErrorHandler:

Dart

import 'package:supabase_flutter/supabase_flutter.dart';

class SupabaseErrorHandler implements ErrorHandler {
  @override
  Failure handleError(dynamic error) {
    if (error is PostgrestException) {
      return ServerFailure(error.message, code: error.code != null ? int.tryParse(error.code!) : null);
    } else if (error is AuthException) {
      return ServerFailure(error.message);
    } else {
      return UnknownFailure(error.toString());
    }
    // يمكن إضافة تسجيل: logger.e(error);
  }
}

الشرح: تتعامل مع أخطاء Supabase الشائعة مثل مشكلات PostgreSQL أو التوثيق.

  • FirebaseErrorHandler:

Dart

import 'package:firebase_core/firebase_core.dart';
import 'error_handler.dart';
import 'failures.dart';

class FirebaseErrorHandler implements ErrorHandler {
  @override
  Failure handleError(dynamic error) {
    if (error is FirebaseException) {
      // محاولة تحويل code من String إلى int
      // إذا فشل التحويل، نستخدم hashCode كبديل
      final int? errorCode = error.code != null 
          ? int.tryParse(error.code!) ?? error.code!.hashCode 
          : null;
      
      return ServerFailure(
        error.message ?? 'Firebase error', 
        code: errorCode,
      );
    } else {
      return UnknownFailure(error.toString());
    }
  }
}

الشرح: تركز على أخطاء Firebase، مثل مشكلات التخزين أو التوثيق.

  • HiveErrorHandler:

Dart

import 'package:hive/hive.dart';

class HiveErrorHandler implements ErrorHandler {
  @override
  Failure handleError(dynamic error) {
    if (error is HiveError) {
      return CacheFailure(error.message);
    } else {
      return UnknownFailure(error.toString());
    }
  }
}

الشرح: تُعالج أخطاء التخزين المحلي في Hive.

  • GeneralErrorHandler (للأخطاء العامة):

Dart

class GeneralErrorHandler implements ErrorHandler {
  @override
  Failure handleError(dynamic error) {
    if (error is Exception) {
      return UnknownFailure(error.toString());
    } else {
      return UnknownFailure('Unexpected error: $error');
    }
  }
}

الشرح: تستخدم كبديل لأي خطأ غير محدد.

4. مدير الأخطاء (ErrorManager)

يساعد في اختيار الـ Handler المناسب ديناميكياً.

Dart

class ErrorManager {
  static ErrorHandler getHandler(String type) {
    switch (type) {
      case 'supabase':
        return SupabaseErrorHandler();
      case 'firebase':
        return FirebaseErrorHandler();
      case 'hive':
        return HiveErrorHandler();
      default:
        return GeneralErrorHandler();
    }
  }
}

الشرح: يعتمد على نمط المصنع (Factory Pattern) لتوفير المرونة.

مثال على الاستخدام في التطبيق

لنفترض أن لديك دالة لجلب بيانات مستخدم من Supabase:

Dart

Future<Either<Failure, User>> fetchUser() async {
  final handler = ErrorManager.getHandler('supabase');
  return handler.safeCall(() async {
    // كود Supabase: await Supabase.instance.client.from('users').select();
    return User(); // افتراضي، استبدله بكودك الفعلي
  });
}

الشرح:

  • في حالة النجاح، يُرجع Right<User>.
  • في حالة الفشل، يُرجع Left<Failure>، ويمكن التعامل معه باستخدام fold من Dartz:Dartresult.fold( (failure) => print('Error: ${failure.message}'), // عرض الخطأ (user) => print('Success: $user'), // معالجة النجاح );

اليك الكود مجمع في كلاسات جاهزة باسم الملف :

1. اسم الملف: failures.dart

هذا الملف يحتوي على الفئة المجردة Failure وفئاتها الفرعية، التي تمثل أنواع الفشل المختلفة.

Dart

import 'package:dartz/dartz.dart';

// فئة مجردة لتمثيل الفشل العام
abstract class Failure {
  final String message;
  final int? code;

  Failure(this.message, {this.code});
}

// فئات فرعية لأنواع محددة من الفشل
class ServerFailure extends Failure {
  ServerFailure(String message, {int? code}) : super(message, code: code);
}

class DatabaseFailure extends Failure {
  DatabaseFailure(String message, {int? code}) : super(message, code: code);
}

class CacheFailure extends Failure {
  CacheFailure(String message, {int? code}) : super(message, code: code);
}

class UnknownFailure extends Failure {
  UnknownFailure(String message) : super(message);
}

2. اسم الملف: error_handler.dart

هذا الملف يحتوي على الفئة المجردة ErrorHandler، التي توفر الواجهة العامة لمعالجة الأخطاء.

Dart

import 'package:dartz/dartz.dart';
import 'failures.dart'; // استيراد failures.dart إذا كان في نفس المجلد

abstract class ErrorHandler {
  // دالة لمعالجة الاستثناء وإرجاع فشل
  Failure handleError(dynamic error);

  // دالة مساعدة لتغليف الدوال غير المتزامنة (اختياري، لتسهيل الاستخدام)
  Future<Either<Failure, T>> safeCall<T>(Future<T> Function() call) async {
    try {
      final result = await call();
      return Right(result);
    } catch (e) {
      return Left(handleError(e));
    }
  }
}

3. اسم الملف: supabase_error_handler.dart

هذا الملف يحتوي على الفئة SupabaseErrorHandler، المخصصة لأخطاء Supabase.

Dart

import 'package:supabase_flutter/supabase_flutter.dart';
import 'error_handler.dart'; // استيراد error_handler.dart
import 'failures.dart'; // استيراد failures.dart

class SupabaseErrorHandler implements ErrorHandler {
  @override
  Failure handleError(dynamic error) {
    if (error is PostgrestException) {
      return ServerFailure(error.message, code: error.code != null ? int.tryParse(error.code!) : null);
    } else if (error is AuthException) {
      return ServerFailure(error.message);
    } else {
      return UnknownFailure(error.toString());
    }
    // يمكن إضافة تسجيل: logger.e(error);
  }
}

4. اسم الملف: firebase_error_handler.dart

هذا الملف يحتوي على الفئة FirebaseErrorHandler، المخصصة لأخطاء Firebase.

Dart

import 'package:firebase_core/firebase_core.dart';
import 'error_handler.dart'; // استيراد error_handler.dart
import 'failures.dart'; // استيراد failures.dart

class FirebaseErrorHandler implements ErrorHandler {
  @override
  Failure handleError(dynamic error) {
    if (error is FirebaseException) {
      return ServerFailure(error.message ?? 'Firebase error', code: error.code.hashCode);
    } else {
      return UnknownFailure(error.toString());
    }
  }
}

5. اسم الملف: hive_error_handler.dart

هذا الملف يحتوي على الفئة HiveErrorHandler، المخصصة لأخطاء Hive.

Dart

import 'package:hive/hive.dart';
import 'error_handler.dart'; // استيراد error_handler.dart
import 'failures.dart'; // استيراد failures.dart

class HiveErrorHandler implements ErrorHandler {
  @override
  Failure handleError(dynamic error) {
    if (error is HiveError) {
      return CacheFailure(error.message);
    } else {
      return UnknownFailure(error.toString());
    }
  }
}

6. اسم الملف: general_error_handler.dart

هذا الملف يحتوي على الفئة GeneralErrorHandler، المخصصة للأخطاء العامة غير المحددة.

Dart

import 'error_handler.dart'; // استيراد error_handler.dart
import 'failures.dart'; // استيراد failures.dart

class GeneralErrorHandler implements ErrorHandler {
  @override
  Failure handleError(dynamic error) {
    if (error is Exception) {
      return UnknownFailure(error.toString());
    } else {
      return UnknownFailure('Unexpected error: $error');
    }
  }
}

7. اسم الملف: error_manager.dart

هذا الملف يحتوي على الفئة ErrorManager، التي تدير اختيار الـ Handler المناسب.

Dart

import 'error_handler.dart'; // استيراد error_handler.dart
import 'supabase_error_handler.dart'; // استيراد supabase_error_handler.dart
import 'firebase_error_handler.dart'; // استيراد firebase_error_handler.dart
import 'hive_error_handler.dart'; // استيراد hive_error_handler.dart
import 'general_error_handler.dart'; // استيراد general_error_handler.dart

class ErrorManager {
  static ErrorHandler getHandler(String type) {
    switch (type) {
      case 'supabase':
        return SupabaseErrorHandler();
      case 'firebase':
        return FirebaseErrorHandler();
      case 'hive':
        return HiveErrorHandler();
      default:
        return GeneralErrorHandler();
    }
  }
}

نصائح إضافية للاستخدام

  • التنظيم في المشروع: قم بإنشاء مجلد مثل lib/error_handling/ وضع هذه الملفات بداخله للحفاظ على ترتيب الكود.
  • التكامل: كما في المثال السابق، استخدم ErrorManager.getHandler(‘supabase’) للحصول على الـ Handler المناسب في دوال تطبيقك.
  • التوسع: إذا أردت إضافة Handler جديد (مثل لـ SQLite)، أنشئ ملفاً جديداً يرث من ErrorHandler وأضفه إلى ErrorManager.
  • الاختبار: يُفضل اختبار كل فئة بشكل منفصل باستخدام أدوات مثل flutter test لضمان تغطية الحالات المختلفة.

هذا التقسيم يجعل الكود سهل الرفع والصيانة على GitHub Gist. إذا كنت بحاجة إلى تعديلات إضافية، فأخبرني.

الخاتمة

يوفر هذا النموذج حلاً قابلاً للتوسع وسهل الاستخدام لإدارة الأخطاء في تطبيقات Flutter. بمجرد نسخ الكلاسات إلى مشروعك، يمكنك توسيعها بإضافة Handlers جديدة. يُنصح باختبار الخدمة باستخدام وحدات الاختبار (Unit Tests) لضمان تغطية جميع الحالات. هذا النهج يعزز جودة التطبيق ويقلل من الوقت المستغرق في الصيانة المستقبلية.

By احمد علي

مطور تطبيقات هواتف ذكية باستخدام Flutter، وصانع محتوى تقني يكتب عن الذكاء الاصطناعي والبرمجة وتطورات التكنولوجيا الحديثة. أسعى لتبسيط الأفكار المعقدة ومشاركة خبرتي مع المهتمين بالمجال.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *