cat swift/condividere-dominio-tra-tracce.md
Condividere il dominio tra app, server, CLI e librerie
Progettare moduli Swift condivisi tra destinazioni diverse, mantenendo il dominio portabile e gli adattatori ai bordi.
La promessa più interessante di Swift multi-traccia è questa: puoi condividere dominio e casi d’uso tra destinazioni diverse.
Ma condividere codice non significa condividere tutto.
Cosa condividere
Buoni candidati:
- value types di dominio;
- enum di stato;
- errori di dominio;
- validazione input;
- casi d’uso puri;
- protocolli applicativi;
- test fixture;
- mapping indipendente dalla piattaforma;
- formattazione non UI.
Esempio:
public struct TaskDraft: Equatable, Sendable {
public let title: String
public init(title: String) throws {
let trimmed = title.trimmingCharacters(in: .whitespacesAndNewlines)
guard trimmed.isEmpty == false else {
throw TaskError.emptyTitle
}
self.title = trimmed
}
}
Questa regola serve ovunque.
Cosa non condividere
Da evitare nel core:
SwiftUI.View;UIViewController;Requestdi Vapor;FileManager.default;URLSession.shared;- modelli database;
- DTO HTTP pubblici;
UserDefaults;- codice con assunzioni di piattaforma non dichiarate.
Il core deve essere noioso e portabile.
Target SwiftPM
Una struttura utile:
TaskDomain
tipi e regole
TaskApplication
casi d'uso e protocolli
TaskHTTP
client API
TaskServer
route Vapor e database
TaskCLI
ArgumentParser e file system
TaskUI
SwiftUI view model è view
Non devi sempre creare tutti questi target. Ma la mappa aiuta a decidere.
Manifest
targets: [
.target(name: "TaskDomain"),
.target(
name: "TaskApplication",
dependencies: ["TaskDomain"]
),
.target(
name: "TaskHTTP",
dependencies: ["TaskDomain", "TaskApplication"]
),
.executableTarget(
name: "TaskCLI",
dependencies: ["TaskApplication", "TaskHTTP"]
)
]
La direzione delle dipendenze è visibile.
Conditional compilation
Quando serve codice specifico piattaforma:
#if canImport(UIKit)
import UIKit
#endif
Oppure:
#if os(macOS)
let defaultDirectory = FileManager.default.homeDirectoryForCurrentUser
#endif
Usa conditional compilation ai bordi, non nel dominio principale.
Se il dominio è pieno di #if, probabilmente sta facendo troppo.
Codable condiviso
Codable può essere comodo nel dominio, ma va valutato.
Va bene quando:
- il formato è davvero stabile;
- il dominio è usato anche come modello locale;
- non stai esponendo direttamente contratti esterni fragili.
Meglio separare quando:
- JSON cambia spesso;
- backend usa naming incoerente;
- campi null/mancanti sono frequenti;
- vuoi dominio più rigoroso del payload.
Non mettere Codable su tutto per abitudine.
Concorrenza condivisa
Se il core viene usato da app, server e CLI, Sendable diventa importante.
public struct TaskItem: Equatable, Codable, Sendable {
public let id: ID
public var title: String
}
Per protocolli:
public protocol TaskRepository: Sendable {
func all() async throws -> [TaskItem]
}
Questo rende il codice più adatto a Swift 6 e alle destinazioni concorrenti.
Errori portabili
Errore di dominio:
public enum TaskError: Error, Equatable, Sendable {
case emptyTitle
case notFound(TaskItem.ID)
}
Traduzioni:
SwiftUI -> messaggio utente
Vapor -> status code
CLI -> ValidationError o stderr
Library -> errore pubblico documentato
L’errore resta lo stesso. Cambia la presentazione.
Testing condiviso
Test del dominio:
@Test
func draftRifiutaTitoloVuoto() {
#expect(throws: TaskError.emptyTitle) {
try TaskDraft(title: " ")
}
}
Questo test protegge tutte le tracce.
Poi ogni destinazione aggiunge i suoi test:
- SwiftUI view model;
- route Vapor;
- comando CLI;
- client HTTP;
- API pubblica della libreria.
Shared fixtures
Puoi creare fixture condivise per test:
extension TaskItem {
static let sample = TaskItem(
id: TaskItem.ID("t-1"),
title: "Scrivere Swift",
isCompleted: false
)
}
Attenzione a non trasformare le fixture in un secondo dominio implicito.
Checklist
Il dominio è davvero condivisibile quando:
- non importa framework UI o server;
- usa value types;
- espone errori chiari;
- rispetta
Sendable; - non dipende da singleton;
- ha test propri;
- non assume formato JSON esterno;
- lascia file system, database e HTTP ai bordi.
Esercizio
Riorganizza il progetto task in target:
TaskDomain;TaskApplication;TaskCLI;TaskServer;TaskUI.
Poi verifica:
TaskDomaincompila senza SwiftUI e Vapor;TaskApplicationnon importaURLSession;TaskCLIpuò usareTaskApplication;TaskServertraduceTaskErrorin HTTP;TaskUItraduceTaskErrorin messaggio utente.