Sparround

Repository müqaviləsi: abstract sinif və asılılığın tərsinə çevrilməsi

Repository müqaviləsi domain qatında elan olunur və yalnız bir sual cavablandırır: tətbiq bu məlumatla nə etmək istəyir? "Necə" sualı — HTTP, cache, baza — implementasiyanın işidir.

Müqavilə yazarkən üç qayda var:

  • Yalnız domain tipləri. Giriş və çıxış domain modelləri, enum-lar, sadə tiplər olur. Response, Map<String, dynamic>, DioException, DocumentSnapshot müqavilədə görünmür.
  • Metod adı niyyəti desin. fetchActive(), watchAll(), save(Order), cancel(String id) — mənbəni deyil, əməliyyatı adlandırır. getFromApi() kimi ad mənbəni sızdırır.
  • Bir məlumat növü = bir repository. Ekrana görə bölmək (HomeScreenRepository) səhvdir: ekran dəyişəndə repository dəyişməli olur və iki ekran eyni məlumatı iki dəfə yükləyir.

Dart-da müqavilə abstract class-dır. Dart 3-də abstract interface class də yazmaq olar — bu, sinfin extends edilməsini qadağan edir və yalnız implements icazə verir. Praktik fərq: implementasiya heç bir davranışı miras almır, deməli "yarım implementasiya" mümkün olmur.

dart
// ══ lib/domain/repositories/order_repository.dart ══ (müqavilə)
import 'package:my_app/domain/models/order.dart';

// `abstract interface class`: yalnız `implements` — `extends` qadağandır.
abstract interface class OrderRepository {
  /// Cari istifadəçinin sifarişləri, ən yenisi əvvəldə.
  Future<List<Order>> fetchMine({int page = 1, int pageSize = 20});

  /// Bir sifariş. Tapılmadıqda `OrderNotFoundFailure` qaytarır.
  Future<Order> fetchById(String id);

  /// Sifarişi ləğv edir və yenilənmiş obyekti qaytarır.
  Future<Order> cancel(String id);

  /// Siyahının canlı axını — lokal cache dəyişdikdə də yenilənir.
  Stream<List<Order>> watchMine();
}

// ══ lib/data/repositories/order_repository_remote.dart ══ (məhsul rejimi)
class OrderRepositoryRemote implements OrderRepository {
  OrderRepositoryRemote({required OrderApiClient apiClient})
      : _apiClient = apiClient;

  final OrderApiClient _apiClient;
  final _controller = StreamController<List<Order>>.broadcast();

  @override
  Future<List<Order>> fetchMine({int page = 1, int pageSize = 20}) async {
    final dtos = await _apiClient.getOrders(page: page, limit: pageSize);
    final orders = dtos.map((d) => d.toDomain()).toList();
    _controller.add(orders);          // canlı axını da yeniləyir
    return orders;
  }

  @override
  Future<Order> fetchById(String id) async =>
      (await _apiClient.getOrder(id)).toDomain();

  @override
  Future<Order> cancel(String id) async =>
      (await _apiClient.cancelOrder(id)).toDomain();

  @override
  Stream<List<Order>> watchMine() => _controller.stream;
}

// ══ test/fakes/fake_order_repository.dart ══ (test rejimi)
class FakeOrderRepository implements OrderRepository {
  FakeOrderRepository(this._orders);
  List<Order> _orders;

  @override
  Future<List<Order>> fetchMine({int page = 1, int pageSize = 20}) async =>
      _orders;

  @override
  Future<Order> fetchById(String id) async =>
      _orders.firstWhere((o) => o.id == id);

  @override
  Future<Order> cancel(String id) async {
    final cancelled = (await fetchById(id))
        .copyWith(status: OrderStatus.cancelled);
    _orders = [
      for (final o in _orders) if (o.id == id) cancelled else o,
    ];
    return cancelled;
  }

  @override
  Stream<List<Order>> watchMine() => Stream.value(_orders);
}

Bir müqavilə, üç implementasiya. Notifier yalnız müqaviləni tanıyır — hansı implementasiyanın işlədiyini DI qərar verir.

Müqavilədə OLA BİLƏRMüqavilədə OLA BİLMƏZNiyə
`Future<List<Order>>``Future<Response>``Response` HTTP detalıdır — lokal implementasiya onu qaytara bilməz
`Order`, `OrderStatus``OrderDto`, `Map<String, dynamic>`DTO backend formasıdır; onu sızdırmaq bütün UI-ı JSON-a bağlayır
`String id`, `int page``Uri`, `QueryParameters`Endpoint qurmaq service-in işidir
Domain `Failure` tipləri`DioException`, `SocketException`Kitabxana exception-ı domain-i həmin kitabxanaya bağlayır
`Stream<List<Order>>``Stream<QuerySnapshot>`Baza tipi mənbəni sızdırır və Firestore-dan çıxmağı qeyri-mümkün edir

`Future`, yoxsa `Stream`? Rəsmi offline-first sənədi bu suala konkret cavab verir: `Stream` daha üstündür, çünki repository iki dəyər verə bilir — əvvəlcə lokal cache-dən sürətli məlumat, sonra serverdən yenilənmiş məlumat. UI hər ikisini avtomatik alır.

Praktikada balans belədir:

  • Oxuma (read): cache və ya canlı yeniləmə varsa Stream; sadə, birdəfəlik sorğuda Future daha oxunaqlıdır.
  • Yazma (write): həmişə Future — əməliyyat bir dəfə baş verir və nəticəsi (uğur/xəta) birdir.

Riverpod tərəfində hər ikisi rahat işləyir: Future üçün notifier-in build-i Future qaytarır, Stream üçün isə Stream. AsyncValue hər iki halda eynidir.

Neçə metod? Müqavilə "tətbiqin ehtiyacı" qədər olmalıdır, nə az, nə çox. fetchAll(filter, sort, page, size, includeArchived, ...) kimi 8 parametrli "universal" metod əslində müqaviləni gizlədilmiş SQL-ə çevirir. Əvəzinə niyyət əsaslı metodlar yazmaq daha oxunaqlıdır: fetchMine(), fetchArchived(), search(String query).

Ən çox rast gəlinən səhv: müqaviləni implementasiyadan sonra yazmaq. O zaman müqavilə HTTP-nin güzgüsünə çevrilir (getOrders, postOrder, patchOrderStatus) və heç bir dəyər vermir.

Düzgün sıra: əvvəlcə notifier-in nəyə ehtiyacı olduğunu yaz, ondan müqaviləni çıxar, sonra implementasiya et.

Praktika. Layihəndən bir repository seç və müqaviləsini yenidən yaz: hər metodun adını niyyət əsaslı et, imzalardan bütün kitabxana tiplərini çıxar. Sonra həmin müqavilənin Fake... implementasiyasını yaz (yaddaşdaki siyahı üzərində).

Hazır sayılır: grep -E "Response|Dio|Map<String" lib/domain/repositories heç nə tapmır və fake implementasiya 30 sətirdən qısadır.

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