cat swift/snapshot-concettuale-architettura.md
Snapshot concettuale dell'architettura
Disegnare una mappa mentale dell'architettura Swift senza trasformarla in dogma: dipendenze, moduli, flussi e confini.
Uno snapshot concettuale è una fotografia ragionata del sistema.
Non è un diagramma decorativo. Serve a rispondere velocemente a domande pratiche:
- dove vive questa regola?
- chi chiama la rete?
- dove si mappano i DTO?
- cosa posso testare senza infrastruttura?
- quale modulo dipende da quale?
- dove entra la UI?
Architettura minima
Per il corso, useremo questa mappa:
UI / CLI
|
v
Application
|
v
Domain
Infrastructure
^
|
Composition Root
In pratica:
Domaincontiene tipi e regole;Applicationcontiene casi d’uso e protocolli;Infrastructureimplementa i protocolli con rete, cache, file, database;UI / CLIpresenta stato e comandi;Composition Rootassembla le dipendenze concrete.
Direzione delle dipendenze
Il punto più importante:
Application dipende da Domain.
Infrastructure dipende dai protocolli applicativi.
Domain non dipende da Infrastructure.
Questo permette di testare l’applicazione con fake e stub.
Composition root
Il composition root è il punto in cui costruisci il grafo reale.
In una CLI può essere main.swift:
@main
struct ProductsCLI {
static func main() async throws {
let baseURL = URL(string: "https://api.example.com")!
let client = URLSessionHTTPClient(baseURL: baseURL)
let repository = HTTPProductRepository(client: client)
let useCase = LoadProductUseCase(repository: repository)
let command = ProductCommand(loadProduct: useCase)
try await command.run()
}
}
In una app SwiftUI può essere l’entry point dell’app o un factory dedicato.
Il resto del codice non deve sapere come tutto viene assemblato.
Feature verticali
Quando il progetto cresce, una divisione solo per livelli può diventare scomoda.
Alternativa:
Products/
Domain/
Application/
Infrastructure/
Presentation/
Cart/
Domain/
Application/
Infrastructure/
Presentation/
Questo mantiene vicini i file di una feature.
La scelta dipende dalla dimensione del progetto:
- package piccolo: cartelle per livello;
- app media: feature verticali;
- sistema grande: moduli SwiftPM separati.
Moduli SwiftPM
Quando i confini diventano importanti, puoi usare target SwiftPM.
let package = Package(
name: "ShopKit",
platforms: [.macOS(.v14), .iOS(.v17)],
products: [
.library(name: "ShopDomain", targets: ["ShopDomain"]),
.library(name: "ShopAPI", targets: ["ShopAPI"])
],
targets: [
.target(name: "ShopDomain"),
.target(
name: "ShopApplication",
dependencies: ["ShopDomain"]
),
.target(
name: "ShopAPI",
dependencies: ["ShopDomain", "ShopApplication"]
),
.testTarget(
name: "ShopApplicationTests",
dependencies: ["ShopApplication"]
)
]
)
I target rendono i confini compilabili. Se ShopDomain prova a importare ShopAPI, il package non compila.
Snapshot di un client API testabile
Product
Price
ProductCategory
^
|
LoadProductUseCase ----> ProductRepository protocol
^
|
HTTPProductRepository
| |
v v
HTTPClient ProductDTO
|
v
URLSessionHTTPClient
Questo diagramma dice:
- il caso d’uso vede il protocollo;
- il repository HTTP è un dettaglio sostituibile;
- il DTO non entra nel dominio;
URLSessionresta al bordo.
Stato applicativo
Per UI o CLI, spesso serve uno stato leggibile:
enum LoadingState<Value: Equatable>: Equatable {
case idle
case loading
case loaded(Value)
case failed(String)
}
View model:
@MainActor
final class ProductViewModel: ObservableObject {
@Published private(set) var state: LoadingState<Product> = .idle
private let loadProduct: LoadProductUseCase
init(loadProduct: LoadProductUseCase) {
self.loadProduct = loadProduct
}
func load(id: Product.ID) async {
state = .loading
do {
state = .loaded(try await loadProduct.execute(id: id))
} catch {
state = .failed("Impossibile caricare il prodotto.")
}
}
}
Il view model conosce il caso d’uso, non URLSession.
Dove mettere i protocolli
Regola pratica: il protocollo vive vicino a chi lo usa, non per forza vicino a chi lo implementa.
Se LoadProductUseCase ha bisogno di ProductRepository, il protocollo può vivere in Application.
L’implementazione HTTP vive in Infrastructure.
Questo mantiene la direzione giusta:
Application definisce bisogno.
Infrastructure soddisfa bisogno.
Overengineering
Non introdurre architettura solo per imitare progetti grandi.
Segnali di overengineering:
- protocolli con una sola implementazione e nessun test;
- factory annidate senza necessità;
- nomi generici come
Manager,Coordinator,Helper; - cartelle vuote create “per il futuro”;
- mapping DTO identico al dominio senza motivo;
- test che richiedono più setup del codice reale.
L’architettura deve ridurre attrito, non produrlo.
Checklist
Uno snapshot utile risponde a:
- quali sono i tipi di dominio?
- quali sono i casi d’uso?
- quali protocolli rappresentano porte esterne?
- quali implementazioni sono infrastruttura?
- dove vengono assemblate le dipendenze?
- quali parti posso testare senza rete?
- quali target SwiftPM impediscono dipendenze sbagliate?
Esercizio
Disegna lo snapshot del tuo SDK prodotti.
Poi verifica il codice:
Productnon conosceProductDTO;LoadProductUseCasenon creaURLRequest;HTTPProductRepositorynon decide regole UI;- il view model o comando CLI riceve un caso d’uso;
- i test del caso d’uso usano un fake repository.
Obiettivo: il diagramma deve essere abbastanza semplice da stare in una schermata.