cat swift/condividere-dominio-tra-tracce.md

Lezione 70 55 min

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;
  • Request di 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:

  1. TaskDomain;
  2. TaskApplication;
  3. TaskCLI;
  4. TaskServer;
  5. TaskUI.

Poi verifica:

  • TaskDomain compila senza SwiftUI e Vapor;
  • TaskApplication non importa URLSession;
  • TaskCLI può usare TaskApplication;
  • TaskServer traduce TaskError in HTTP;
  • TaskUI traduce TaskError in messaggio utente.