cat swift/dto-dominio-separare-api-modello.md
DTO e dominio: separare API e modello interno
Il modello ricevuto dalla rete non deve contaminare il dominio: DTO, mapping e validazione tengono pulita l'architettura.
Una API esterna non è il tuo dominio. È un confine.
Se usi direttamente i modelli JSON in tutta l’app, stai permettendo alla forma della API di decidere la forma del tuo codice.
Meglio separare:
- DTO: Data Transfer Object, rappresenta il formato esterno;
- dominio: rappresenta valori validi per la tua applicazione.
DTO
struct ProductDTO: Decodable {
let id: String
let name: String
let priceCents: Int
let category: String?
}
Questo tipo risponde alla domanda: cosa manda la API?
Dominio
struct Product: Equatable, Sendable {
let id: ProductID
let name: String
let price: Price
let category: ProductCategory
}
Questo tipo risponde alla domanda: cosa considera valido il mio programma?
Mapping
extension ProductDTO {
func toDomain() throws -> Product {
guard let id = ProductID(id) else {
throw ProductMappingError.invalidID
}
guard let price = Price(priceCents) else {
throw ProductMappingError.invalidPrice
}
guard let category = ProductCategory(rawValue: category ?? "") else {
throw ProductMappingError.invalidCategory
}
return Product(
id: id,
name: name,
price: price,
category: category
)
}
}
La conversione è il luogo giusto per validare.
Dove vive il mapping?
Il mapping dovrebbe stare vicino al DTO o nel target infrastruttura.
Infrastructure
ProductDTO
ProductDTO.toDomain()
Domain
Product
Price
ProductCategory
Il dominio può essere conosciuto dall’infrastruttura, ma non il contrario. Se Product inizia a conoscere ProductDTO, il dominio dipende dalla forma della API.
Regola pratica:
DTO -> dominio: si
dominio -> DTO remoto: solo se stai costruendo una request e lo fai in un adapter
Errori di mapping
enum ProductMappingError: Error, Equatable {
case invalidID
case invalidPrice
case invalidCategory
}
Non tutto e DecodingError. Il JSON può essere decodificabile ma non valido per il dominio.
Esempio:
{ "id": "p-1", "price_cents": -100 }
Decoding riuscito. Mapping fallito.
Perché non usare solo Codable nel dominio?
A volte va bene. Se la API è sotto il tuo controllo e coincide col dominio, puoi evitare DTO separati.
Ma separa quando:
- la API e esterna;
- i nomi non coincidono;
- ci sono campi sporchi;
- devi validare invarianti;
- il dominio è usato anche da CLI/server/app;
- vuoi testare mapping indipendentemente.
Mapping collection
let prodotti = try dtos.map { dto in
try dto.toDomain()
}
Se vuoi scartare invalidi:
let prodottiValidi = dtos.compactMap { dto in
try? dto.toDomain()
}
Attenzione: try? perde errori. Usalo solo se accetti di perdere diagnostica.
Esito ricco
Per mantenere errori:
let risultati = dtos.map { dto in
Result {
try dto.toDomain()
}
}
Ora puoi sapere quali elementi sono falliti e perché.
Mapping parziale
A volte una lista contiene elementi validi e invalidi. Decidere cosa fare è una regola di prodotto.
Fallire tutto:
let prodotti = try dtos.map { try $0.toDomain() }
Restituire validi e diagnostica:
struct MappingReport<Value> {
let values: [Value]
let failures: [Error]
}
let report = dtos.reduce(into: MappingReport<Product>(values: [], failures: [])) { report, dto in
do {
report.values.append(try dto.toDomain())
} catch {
report.failures.append(error)
}
}
Non usare compactMap { try? ... } se gli errori sono importanti per debug, analytics o supporto.
Test del mapping
Il mapping merita test propri:
@Test
func productDTORifiutaPrezzoNegativo() {
let dto = ProductDTO(
id: "p-1",
name: "Manuale Swift",
priceCents: -100,
category: "books"
)
#expect(throws: ProductMappingError.invalidPrice) {
try dto.toDomain()
}
}
Questo test non coinvolge rete, cache o UI. Protegge il confine più importante: solo valori validi entrano nel dominio.
DTO in uscita
Se devi inviare dati alla API, crea DTO di request separati.
struct CreateProductRequestDTO: Encodable {
let name: String
let priceCents: Int
enum CodingKeys: String, CodingKey {
case name
case priceCents = "price_cents"
}
}
Non forzare il dominio a conformarsi al JSON solo per comodità.
Checklist
Separare DTO e dominio ti aiuta a:
- proteggere invarianti;
- evitare nomi API nel codice interno;
- testare mapping;
- gestire versioni API;
- evitare optionals inutili nel dominio;
- distinguere decoding da validazione.
In review, cerca questi segnali:
snake_casefuori dai DTO;- DTO passati alla UI;
DecodingErrorusato per regole di dominio;- optionals nel dominio solo perché la API manda
null; - mapping non testato.
Esercizio
Crea:
ProductDTO;Product;ProductID;Price;ProductCategory;ProductMappingError;toDomain() throws -> Product.
Poi scrivi tre casi:
- DTO valido;
- prezzo negativo;
- categoria sconosciuta.
Ogni caso deve produrre un risultato chiaro.