Sparround

Use-case: nə vaxt lazımdır, nə vaxt zərərlidir

Use-case (digər adı interactor) — bir biznes əməliyyatını təmsil edən sinif: "səbətin yekununu hesabla", "sifarişi ver", "profili abunə statusu ilə birlikdə yüklə".

Rəsmi Flutter bələdçisi use-case-i opsional qat sayır və onu yalnız üç şərtdən biri ödəndikdə tövsiyə edir:

1. Əməliyyat bir neçə repository-dən məlumat tələb edir. 2. Məntiq həqiqətən mürəkkəbdir. 3. Eyni məntiq bir neçə view model tərəfindən istifadə olunur.

Bələdçi qazancı və qiyməti də açıq sayır.

Qazanc: view model-lərdə təkrarın qarşısını alır, mürəkkəb biznes məntiqini UI məntiqindən ayırdığı üçün testi asanlaşdırır, view model-i oxunaqlı saxlayır.

Qiymət: arxitekturanın mürəkkəbliyi artır (daha çox sinif, daha çox koqnitiv yük), test üçün əlavə mock-lar lazım gəlir, boilerplate artır.

Bələdçinin tövsiyəsi birbaşadır: use-case-ləri yalnız lazım olduqda əlavə et. Sonradan view model-lərin çoxu use-case vasitəsilə məlumat almağa başlayırsa, o zaman hamısını use-case-ə keçirmək olar.

dart
// fayl: lib/domain/usecases/calculate_cart_total.dart
import 'package:my_app/domain/models/cart.dart';
import 'package:my_app/domain/models/cart_total.dart';
import 'package:my_app/domain/repositories/cart_repository.dart';
import 'package:my_app/domain/repositories/coupon_repository.dart';
import 'package:my_app/domain/repositories/delivery_repository.dart';

class CalculateCartTotal {
  const CalculateCartTotal({
    required CartRepository cartRepository,
    required CouponRepository couponRepository,
    required DeliveryRepository deliveryRepository,
  })  : _cart = cartRepository,
        _coupons = couponRepository,
        _delivery = deliveryRepository;

  final CartRepository _cart;
  final CouponRepository _coupons;
  final DeliveryRepository _delivery;

  /// Üç mənbəni birləşdirir — buna görə use-case-dir.
  Future<CartTotal> call({String? couponCode, required String cityId}) async {
    final cart = await _cart.fetchCurrent();

    final subtotal = cart.items.fold<double>(
      0,
      (sum, item) => sum + item.product.finalPrice * item.quantity,
    );

    // Kupon: yoxdursa, ya da etibarsızdırsa endirim sıfırdır.
    final coupon = couponCode == null
        ? null
        : await _coupons.findValid(couponCode, subtotal: subtotal);
    final discount = coupon?.discountFor(subtotal) ?? 0;

    // Çatdırılma: pulsuz həddi keçildikdə sıfırlanır.
    final tariff = await _delivery.tariffFor(cityId);
    final shipping =
        subtotal - discount >= tariff.freeFrom ? 0.0 : tariff.price;

    return CartTotal(
      subtotal: subtotal,
      discount: discount,
      shipping: shipping,
      total: subtotal - discount + shipping,
      appliedCoupon: coupon,
    );
  }
}

// ── Riverpod-da bağlanması ──
@riverpod
CalculateCartTotal calculateCartTotal(Ref ref) => CalculateCartTotal(
      cartRepository: ref.watch(cartRepositoryProvider),
      couponRepository: ref.watch(couponRepositoryProvider),
      deliveryRepository: ref.watch(deliveryRepositoryProvider),
    );

// ── Notifier sadə qalır: bir çağırış ──
@riverpod
class CartSummary extends _$CartSummary {
  @override
  Future<CartTotal> build({String? couponCode, required String cityId}) =>
      ref.watch(calculateCartTotalProvider)(
        couponCode: couponCode,
        cityId: cityId,
      );
}

Şərt №1-ə uyğun real use-case: yekun məbləğ üç ayrı mənbədən asılıdır. Bu məntiqi notifier-də saxlamaq onu test edilməz edərdi.

ƏməliyyatŞərtlərdən hansı ödənirQərar
"Sifarişlərimi gətir"Heç biri — bir repository, bir çağırışUse-case yazma; notifier birbaşa repository-ni çağırsın
"Sifarişi ləğv et"Heç biriUse-case yazma
"Səbətin yekununu hesabla"№1 (üç repository) + №2 (mürəkkəb qayda)Use-case yaz
"Profili + abunə statusunu göstər"№1 (iki repository)Use-case yaz (ya da bir ekran üçünsə notifier-də birləşdir)
"Qiyməti valyutaya görə formatla"Heç biri — bu, göstərmə məntiqidirUse-case yazma; presentation qatına aiddir
"Endirim faizini hesabla"Heç biri — bir modelin sahələrindən asılıdırUse-case yazma; domain modelinin getter-i olsun
"Ödəniş axını: yoxla, rezerv et, təsdiqlə"№2 (çox addımlı) + №3 (iki ekran)Use-case yaz

Use-case-in forması. Üç variant yayılıb:

  • `call()` metodu olan sinifcalculateTotal(couponCode: ...) kimi çağırılır. Yığcamdır, lakin çağırış yerində metod adı görünmür; naviqasiyada "bu funksiya nədir?" sualı yaranır.
  • Adlandırılmış metodu olan sinifCalculateCartTotal().execute(...) ya da PlaceOrder().placeOrder(...). Daha açıqdır, xüsusilə IDE-də axtarış zamanı.
  • Sadə funksiyaFuture<CartTotal> calculateCartTotal({required CartRepository cart, ...}). Asılılıqlar parametr olduğu üçün ən sadə testə malikdir, lakin çağırış yerində uzun olur.

Seçim komanda konvensiyasıdır; vacib olan layihə boyu bir formanın olmasıdır.

Adlandırma. Use-case adı fel + isim olur: PlaceOrder, CalculateCartTotal, RefreshSession. OrderUseCase, CartManager, OrderService kimi adlar əməliyyatı gizlədir.

Anti-pattern. Bir repository çağırışını sarımaq: class GetProductsUseCase { Future<List<Product>> call() => _repo.fetchActive(); }. Bu sinif heç nə əlavə etmir — yalnız bir fayl, bir provider, bir mock və bir test faylı artırır. Rəsmi bələdçinin "yalnız lazım olduqda" tövsiyəsi məhz bunu qarşısını almaq üçündür.

Use-case qatı hamısı ya heç biri qərarı deyil. Rəsmi mövqe də bunu təsdiqləyir: use-case-lər tədricən əlavə olunur, və əgər sonda view model-lərin əksəriyyəti onlardan istifadə edirsə, o zaman qalanlarını da köçürmək olar.

Praktik nəticə: lib/domain/usecases/ qovluğunda 3 fayl olmaq tamamilə normaldır — 30 ekranlıq tətbiqdə də. Bu, "yarımçıq arxitektura" deyil, ölçülmüş qərardır.

Praktika. Layihəndə iki repository-nin məlumatını birləşdirən (ya da mürəkkəb hesablama edən) bir yer tap və onu use-case-ə çıxar. Sonra həmin use-case üçün 2 test yaz: uğurlu yol və bir sərhəd halı (məsələn kupon etibarsızdır).

Hazır sayılır: notifier-də yalnız bir çağırış qalıb, testlər fake repository-lərlə işləyir və şəbəkəyə çıxmır.

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