Sparround

Domain modelləri və immutability (freezed)

Domain modeli — tətbiqin öz lüğətidir. O, backend-in JSON-una, bazanın cədvəlinə ya da ekranın formasına yox, biznesin özünə uyğun gəlir.

Domain modelinin üç xüsusiyyəti var:

  • Immutable — yaradıldıqdan sonra dəyişmir. Dəyişiklik lazımdırsa, copyWith yeni obyekt qaytarır.
  • Dəyər bərabərliyi (value equality) — iki obyektin sahələri eynidirsə, onlar bərabərdir. Bu, referens bərabərliyindən fərqlidir və rebuild-lərə birbaşa təsir edir.
  • Yalnız biznes mənasıisExpensive, finalPrice, canBeCancelled kimi. Color, TextStyle, TimeOfDay burada olmur.

Rəsmi Flutter tövsiyələri immutable modelləri ən yüksək prioritetli bənd sayır: səbəb dəyişikliyin öz yerində baş verməsinə zəmanət verməkdir. Mutable model UI qatına məlumatı təsadüfən dəyişmək imkanı verir və tək istiqamətli axını sındırır.

dart
// fayl: lib/domain/models/product.dart
import 'package:freezed_annotation/freezed_annotation.dart';

part 'product.freezed.dart';

@freezed
abstract class Product with _$Product {
  // Private boş konstruktor: generasiya olunan kodun sinfi extend etməsinə
  // imkan verir — öz getter/metodlarını yazmaq üçün TƏLƏB OLUNUR.
  const Product._();

  const factory Product({
    required String id,
    required String title,
    required double price,          // artıq manatla, çevirmə mapper-də olub
    @Default(0) int discountPercent,
    @Default(false) bool isArchived,
    DateTime? createdAt,
  }) = _Product;

  // Biznes qaydaları modelin özündə yaşayır — hər ekranda təkrarlanmır.
  double get finalPrice => price * (100 - discountPercent) / 100;
  bool get hasDiscount => discountPercent > 0;
  bool get isPurchasable => !isArchived && price > 0;
}

// İstifadə:
const tea = Product(id: 'p1', title: 'Çay', price: 10, discountPercent: 10);

tea.finalPrice;                      // 9.0
tea.copyWith(discountPercent: 0);    // yeni obyekt, köhnəsi dəyişmir

// Dəyər bərabərliyi: freezed `==` və `hashCode`-u generasiya edir.
const same = Product(id: 'p1', title: 'Çay', price: 10, discountPercent: 10);
assert(tea == same);                 // true — sahələr eynidir

// Generasiya: dart run build_runner build --delete-conflicting-outputs

freezed ilə domain modeli. `const Product._();` sətri vacibdir — o olmadan öz getter və metodlarını əlavə edə bilmirsən.

Yanaşma`copyWith``==` / `hashCode`JSONQiyməti
Əl ilə (`final` sahələr)Özün yazırsanÖzün override edirsənÖzün yazırsanKod generasiyası yoxdur, lakin 5 sahəli modeldə ~40 sətir boilerplate və sahə əlavə edərkən unutma riski
`Equatable`Özün yazırsan`props` siyahısı ilə hazırdırÖzün yazırsanKod generasiyası yoxdur; `props`-a yeni sahəni əlavə etməyi unutmaq səssiz baq yaradır
`freezed`Generasiya olunurGenerasiya olunur`json_serializable` ilə birlikdə generasiya olunur`build_runner` lazımdır və böyük layihədə build vaxtı artır; rəsmi tövsiyə bunu açıq qeyd edir

Dəyər bərabərliyinin Riverpod ilə birlikdə çox konkret nəticəsi var: Riverpod 3-də bütün `updateShouldNotify` implementasiyaları `==` müqayisəsindən istifadə edir.

Bu, praktikada nə deməkdir:

  • Model == override etməyibsə (adi sinif, referens bərabərliyi), hər yeni obyekt "dəyişmiş" sayılır və dinləyicilər yenidən qurulur — hətta sahələr eyni olduqda.
  • Model dəyər bərabərliyinə malikdirsə, repository eyni məlumatı ikinci dəfə qaytardıqda rebuild olmur.

Yəni immutability + == təkcə "təmiz kod" məsələsi deyil, ölçülə bilən performans qərarıdır. Siyahı modellərində bu, xüsusilə hiss olunur: List<Product> müqayisəsi elementlərin ==-inə söykənir.

Seçim tövsiyəsi: JSON lazım olan yerdə freezed (DTO-lar), domain modellərində freezed ya da Equatable — layihədə artıq build_runner varsa, hər ikisində freezed işlətmək daha az qərar deməkdir.

Bir vacib nüans: freezed_annotation təmiz Dart paketidir — asılılıqları collection, json_annotationmeta-dır, Flutter-dən asılı deyil. Ona görə onu domain qatında import etmək asılılıq qaydasını pozmur. Kod generatoru (freezed, build_runner) isə dev_dependencies-də qalır və son build-ə düşmür.

Praktika. Layihəndə bir domain modeli seç və onu freezed-ə çevir: copyWith, == və bir biznes getter-i (finalPrice kimi) əlavə et. Sonra o getter üçün bir unit test yaz.

Hazır sayılır: dart run build_runner build xətasız işləyir, test yaşıldır, və həmin hesablama artıq heç bir widget faylında təkrarlanmır.

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

  • freezed paketirəsmipub.dev

    Sintaksisin mənbəyi: `abstract`/`sealed` tələbi, `const X._()` qaydası, `@Default`, JSON integrasiyası.

  • freezed_annotationrəsmipub.dev

    Domain qatında import etmək üçün vacib fakt: bu paketin Flutter asılılığı yoxdur.

  • Dart: bərabərlik və `hashCode`rəsmidart.dev

    Dart-ın dizayn tövsiyələri — `==` və `hashCode`-un birlikdə override edilməsi qaydası da burada.

  • Arxitektura tövsiyələri: immutable modellərrəsmidocs.flutter.dev

    Immutable modellərin niyə ən yüksək prioritetli tövsiyə olduğu və `freezed`/`built_value` mövqeyi.