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.
// ══ 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.
| Failure | Səbəb | UI nə etməli | Retry? |
|---|---|---|---|
| `NetworkFailure` | Şəbəkə yoxdur | "Bağlantı yoxdur" + retry düyməsi | Bəli |
| `TimeoutFailure` | Server cavab vermədi | "Çox uzun çəkdi" + retry | Bəli |
| `ServerFailure` | 5xx | "Xidmətdə problem var" + retry; status kodu loqa | Bəli |
| `AuthFailure` | Token bitib / etibarsızdır | Login ekranına yönəltmək | Xeyr |
| `ForbiddenFailure` | Giriş var, icazə yoxdur | "İcazəniz yoxdur" — login-ə yönəltməmək | Xeyr |
| `NotFoundFailure` | Obyekt silinib ya da yoxdur | "Tapılmadı" + geri qayıtmaq | Xeyr |
| `ValidationFailure` | Server validasiyası | Xətaları sahələrin altında göstərmək | Xeyr |
| `UnexpectedFailure` | Bilinməyən | Ümumi mesaj + retry; hesabat göndərmək | Bə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 switch-ə default 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. UnexpectedFailure və InvalidResponseFailure 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.