DTO vs domain modeli və mapper-lər
Backend-in JSON-u və tətbiqin modeli eyni şey deyil və eyni tempo ilə dəyişmir. Bu iki dünyanı bir tipdə birləşdirdikdə backend-in hər dəyişikliyi bütün tətbiqə yayılır.
Ayrı saxladıqda iki tip yaranır:
- DTO (API modeli) — məftil formatının güzgüsü.
snake_caseadlar, hər şey nullable ola bilər, tarixlərString, qiymətlərint(qəpik). O,dataqatında yaşayır vəfromJson/toJsonbilir. - Domain modeli — tətbiqin istədiyi forma. Non-nullable sahələr,
DateTime,double price,enumstatus, biznes getter-ləri. O,domainqatında yaşayır və JSON haqqında heç nə bilmir.
Aralarında mapper durur: dto.toDomain(). Bu, bir funksiyadır və onun testi ən dəyərli testlərdən biridir — backend dəyişəndə ilk sınan yer məhz o olur.
Rəsmi tövsiyələr bu ayrılığı şərtli (conditional) sayır: böyük tətbiqlərdə tövsiyə olunur, kiçikdə əlavə söz-söhbətdir. Yəni qərar layihənin ölçüsündən asılıdır.
// ══ lib/data/dto/product_dto.dart ══ (məftil formatının güzgüsü)
import 'package:freezed_annotation/freezed_annotation.dart';
part 'product_dto.freezed.dart';
part 'product_dto.g.dart';
@freezed
abstract class ProductDto with _$ProductDto {
const factory ProductDto({
required String id,
// Backend-in adları olduğu kimi saxlanılır.
@JsonKey(name: 'name') String? name,
@JsonKey(name: 'price_cents') int? priceCents,
@JsonKey(name: 'discount_percent') int? discountPercent,
@JsonKey(name: 'created_at') String? createdAt,
@JsonKey(name: 'is_archived') bool? isArchived,
}) = _ProductDto;
factory ProductDto.fromJson(Map<String, dynamic> json) =>
_$ProductDtoFromJson(json);
}
// ══ lib/data/dto/product_dto_mapper.dart ══ (yeganə çevirmə nöqtəsi)
import 'package:my_app/data/dto/product_dto.dart';
import 'package:my_app/domain/models/product.dart';
extension ProductDtoMapper on ProductDto {
Product toDomain() => Product(
id: id,
// null-un müdafiəsi BURADA olur, UI-da yox.
title: name ?? 'Adsız məhsul',
// Qəpik → manat: biznes vahidinə çevrilmə.
price: (priceCents ?? 0) / 100,
discountPercent: discountPercent ?? 0,
createdAt: DateTime.tryParse(createdAt ?? ''),
isArchived: isArchived ?? false,
);
}
// ══ Repository-də istifadə ══
final dtos = await _apiClient.getActiveProducts();
return dtos.map((dto) => dto.toDomain()).toList();
// Nəticə: `priceCents`, `is_archived` və `null` sözləri
// data qatından KƏNARA çıxmır.DTO, mapper və domain modeli. Diqqət et: `null`, `snake_case` və qəpik-manat çevrilməsi — hamısı mapper-də bitir.
| Sual | DTO (API modeli) | Domain modeli |
|---|---|---|
| Formanı kim müəyyən edir | Backend | Tətbiqin biznes məntiqi |
| Nullability | Səxavətli: sahə gəlməyə bilər | Sərt: sahə varsa, o var |
| Adlandırma | Backend-in adları (`price_cents`) | Tətbiqin lüğəti (`price`) |
| Tarix | `String` (ISO mətn) | `DateTime` |
| Status | `String` (`"pending"`) | `enum OrderStatus` |
| Hansı qatda yaşayır | `data/dto/` | `domain/models/` |
| Nə vaxt dəyişir | Backend dəyişdikdə | Biznes qaydaları dəyişdikdə |
Nə vaxt bir model kifayətdir? Rəsmi mövqe bu ayrılığı şərtli sayır, ona görə qərarı bilərəkdən vermək lazımdır.
Bir model kifayətdir əgər: backend-i sən özün idarə edirsən, JSON forması tətbiqin lüğətinə uyğundur, layihə kiçikdir və nullability problemi yoxdur. Bu halda freezed modeli hem fromJson, hem biznes getter-lərini daşıyır.
İki model lazımdır əgər ən azı biri doğrudur:
- Backend başqa komandanın nəzarətindədir və forma dəyişir.
- JSON "çirklidir": hər şey nullable,
Stringtarixlər,"1"/"0"boolean-lar. - Bir neçə mənbə (REST + lokal baza) eyni domain modelinə çevrilir.
- Domain modelinin sahələri API-də olmayan hesablanmış dəyərlər daşıyır.
Ortadaki variant da var: DTO-nu yalnız problemli endpoint-lər üçün yazmaq. Bu, praqmatik və tez-tez ən düzgün seçimdir — hər şeyi eyni qaydaya salmaq məcburiyyəti yoxdur.
Mapper-in yeri data qatıdır, çünki o, backend-in formasını bilir. Domain modelinin fromJson metodu olmamalıdır — o zaman domain JSON haqqında bilməyə başlayır və asılılıq qaydası pozulur.
İki texniki variant var: extension ProductDtoMapper on ProductDto { Product toDomain() } (yuxarıdaki kimi) və ya ayrı ProductMapper sinifi. Extension daha yığcamdır; ayrı sinif isə mapper-in özü asılılıq qəbul etdikdə (məsələn valyuta kursu) daha rahat olur.
Praktika. Ən problemli endpoint-in üçün DTO + mapper yaz və mapper üçün 3 test hazırla: (1) tam JSON, (2) sahələrin yarısı null, (3) tarix formatı yanlış. Hazır sayılır: üç test də yaşıldır və null yoxlaması UI faylında qalmır.
📚 Mənbələr və sənədlər
- json_serializablerəsmipub.dev
`@JsonKey`, `fromJson`/`toJson` generasiyası və `part '*.g.dart'` direktivi — DTO-ların əsas aləti.
- Flutter: JSON və seriyalaşdırmarəsmidocs.flutter.dev
Əl ilə parse etmək və kod generasiyası arasındaki seçimin rəsmi izahı.
- Arxitektura tövsiyələri: ayrı API və domain modellərirəsmidocs.flutter.dev
Bu ayrılığın niyə "conditional" sayıldığı — böyük tətbiqlərdə tövsiyə, kiçikdə əlavə yük.
- Case study: data qatırəsmidocs.flutter.dev
API modellərinin real kodda necə saxlandığı və repository-nin onları necə çevirdiyi.