Sparround

Failure hierarxiyası: tipli xətalar

Result uğursuzluğun olduğunu deyir, Failure isə hansı uğursuzluq olduğunu. İkisi birlikdə işləyir.

Ən sadə variant — xətanı String mesaj kimi daşımaqdır: Result.error(Exception('Şəbəkə xətası')). Bu, üç problemi həll etmir:

  • UI mesaja görə qərar verə bilmir: retry göstərmək lazımdır, yoxsa login ekranına yönəltmək?
  • Mesaj kodda yazılıb, deməli lokalizasiya mümkün deyil.
  • Yeni xəta növü əlavə etdikdə heç kim xəbər tutmur.

Həll — `sealed` Failure hierarxiyası. Hər xəta növü ayrı tipdir və sealed olduğu üçün UI-da switch tam yoxlanılır.

Vacib texniki detal: Failure `Exception`-ı implement etməlidir. Səbəb praktikdir — rəsmi Result sinfinin Error variantı Exception tipini daşıyır, ona görə Result.error(const NetworkFailure()) yalnız Failure implements Exception olduqda kompilyasiya olunur. Bu, həm də throw etməyə imkan verir — bəzi hallarda faydalıdır.

dart
// ══ lib/domain/failures/failure.dart ══

/// Tətbiqin bütün gözlənilən uğursuzluqları.
/// `sealed` → UI-da switch tam yoxlanılır.
/// `implements Exception` → `Result.error(...)` içində işləyir.
sealed class Failure implements Exception {
  const Failure();
}

// ── Şəbəkə və server ──
final class NetworkFailure extends Failure {
  const NetworkFailure();
}

final class TimeoutFailure extends Failure {
  const TimeoutFailure();
}

final class ServerFailure extends Failure {
  const ServerFailure(this.statusCode);
  final int statusCode;       // diaqnostika üçün saxlanılır
}

// ── Autentifikasiya ──
final class AuthFailure extends Failure {
  const AuthFailure();        // token bitib / etibarsızdır → login
}

final class ForbiddenFailure extends Failure {
  const ForbiddenFailure();   // giriş var, icazə yoxdur → login DEYİL
}

// ── Məlumat ──
final class NotFoundFailure extends Failure {
  const NotFoundFailure();
}

final class InvalidResponseFailure extends Failure {
  const InvalidResponseFailure();   // backend gözlənilməz format göndərdi
}

// ── Biznes qaydaları (sahəyə bağlı ola bilər) ──
final class ValidationFailure extends Failure {
  const ValidationFailure(this.fieldErrors);
  /// sahə adı → xəta açarı (mesaj deyil!)
  final Map<String, String> fieldErrors;
}

final class InsufficientPointsFailure extends Failure {
  const InsufficientPointsFailure({required this.missing});
  final int missing;
}

// ── Bilinməyən: BU VARİANT MƏCBURİDİR ──
final class UnexpectedFailure extends Failure {
  const UnexpectedFailure();
}


// ══ Repository: texniki exception → domain Failure ══
class OrderRepositoryRemote implements OrderRepository {
  @override
  Future<Result<List<Order>>> fetchMine() async {
    try {
      final dtos = await _api.getOrders();
      return Result.ok(dtos.map((d) => d.toDomain()).toList());
    } on SocketException {
      return const Result.error(NetworkFailure());
    } on TimeoutException {
      return const Result.error(TimeoutFailure());
    } on UnauthorizedException {
      return const Result.error(AuthFailure());
    } on ForbiddenException {
      return const Result.error(ForbiddenFailure());
    } on NotFoundException {
      return const Result.error(NotFoundFailure());
    } on ServerException catch (e) {
      return Result.error(ServerFailure(e.statusCode));
    } on FormatException catch (e, st) {
      // Gözlənilən (bizim baqımız deyil), lakin LOQLANMALIDIR.
      _logger.warning('Cavab formatı gözlənilməzdir', e, st);
      return const Result.error(InvalidResponseFailure());
    }
    // `Error` tipləri (RangeError, TypeError) TUTULMUR — onlar baqdır
    // və qlobal xəta tutucusuna qədər qalxmalıdır.
  }
}

Failure hierarxiyası və repository-də exception → Failure çevrilməsi. Diqqət et: `Unexpected` variantı məcburidir — bilinməyən hal həmişə olur.

FailureSəbəbUI nə etməliRetry?
`NetworkFailure`Şəbəkə yoxdur"Bağlantı yoxdur" + retry düyməsiBəli
`TimeoutFailure`Server cavab vermədi"Çox uzun çəkdi" + retryBəli
`ServerFailure`5xx"Xidmətdə problem var" + retry; status kodu loqaBəli
`AuthFailure`Token bitib / etibarsızdırLogin ekranına yönəltməkXeyr
`ForbiddenFailure`Giriş var, icazə yoxdur"İcazəniz yoxdur" — login-ə yönəltməməkXeyr
`NotFoundFailure`Obyekt silinib ya da yoxdur"Tapılmadı" + geri qayıtmaqXeyr
`ValidationFailure`Server validasiyasıXətaları sahələrin altında göstərməkXeyr
`UnexpectedFailure`BilinməyənÜmumi mesaj + retry; hesabat göndərməkBəli

Mesajlar `Failure`-də yaşamır. Bu, ən çox pozulan qaydadır. NetworkFailure('Şəbəkə yoxdur') yazdıqda:

  • Mətn domain qatına düşür və lokalizasiya mümkün olmur.
  • Eyni xəta iki ekranda fərqli göstərilə bilmir (birində snackbar, digərində tam ekran).
  • Testdə mətn müqayisə edilir — mətn dəyişdikdə test sınır, halbuki davranış dəyişməyib.

Düzgün bölgü: Failure növü deyir, presentation qatı isə mətni lokalizasiyadan (AppLocalizations) götürür. ValidationFailure isə mesaj deyil, açar daşıyır: {'email': 'invalid_format'} — və UI açarı tərcümə edir.

`UnexpectedFailure` niyə məcburidir. Backend yeni xəta kodu qaytarır, kitabxana yeni exception atır, FormatException gözlənilməz yerdən gəlir — bunların hamısı normal həyatdır. Bilinməyən hal üçün variant olmadıqda kod ya crash olur, ya da switchdefault yazmaq lazım gəlir — və default sealed-in bütün faydasını ləğv edir: yeni Failure əlavə etdikdə kompilyator artıq xəbərdarlıq etmir.

Loqlama. UnexpectedFailureInvalidResponseFailure həmişə loqlanmalıdır (Crashlytics, Sentry): bu, "bilinməyən" halların əslində nə olduğunu öyrənməyin yeganə yoludur.

Praktika. İki addım:

1. lib/domain/failures/failure.dart faylını yarat: sealed class Failure implements Exception + layihənə uyğun 5-7 alt tip + mütləq UnexpectedFailure. 2. Bir repository metodunu bu hierarxiyaya keçir və presentation-da switch yaz: hər Failure üçün fərqli mesaj + retry düyməsinin olub-olmaması.

Sonra yeni bir Failure alt tipi əlavə et və flutter analyze işlət — kompilyator hansı switch-lərin natamam olduğunu göstərməlidir. Hazır sayılır: bu mexanizmi gözlə görmüsən və hər Failure üçün mesaj lokalizasiya faylındadır, kodda deyil.

📚 Mənbələr və sənədlər

  • Result obyektləri ilə xəta idarəsirəsmidocs.flutter.dev

    `Result.error`-un `Exception` daşıması — `Failure implements Exception` qərarının səbəbi.

  • Dart: sinif modifikatorlarırəsmidart.dev

    `sealed` + `final` kombinasiyası: hierarxiya bağlanır, `switch` tam yoxlanılır.

  • Flutter: lokalizasiyarəsmidocs.flutter.dev

    Xəta mesajlarının kodda deyil, `AppLocalizations`-da saxlanması üçün rəsmi mexanizm.

  • Dart: `Exception` sinfirəsmiapi.dart.dev

    `Exception` interfeysinin nə olduğu — `Failure`-un onu implement etməsi buna söykənir.