cat swift/milestone-dominio-use-case-confini.md
Milestone 1: dominio, use case e confini
Costruire il nucleo del progetto finale: tipi di dominio, errori, invarianti, protocolli e primi use case testabili.
La prima milestone non deve toccare rete, UI o database.
Deve rispondere a questa domanda:
Qual è il cuore del problema, espresso in Swift?
Se il dominio è debole, tutto il resto diventerà compensazione.
Output della milestone
Alla fine devi avere:
Sources/
ProjectDomain/
tipi
errori
invarianti
ProjectApplication/
protocolli
use case
Tests/
ProjectDomainTests/
ProjectApplicationTests/
E almeno:
- 4 tipi di dominio;
- 2 errori custom;
- 2 use case;
- 5 test;
- nessuna rete;
- nessuna UI;
- nessun database reale.
Esempio: Hacker News client
Dominio:
public struct Story: Equatable, Sendable, Identifiable {
public let id: ID
public let title: String
public let url: URL?
public let score: Int
public struct ID: RawRepresentable, Equatable, Hashable, Sendable {
public let rawValue: Int
public init(rawValue: Int) {
self.rawValue = rawValue
}
}
}
Invariante:
public struct SearchQuery: Equatable, Sendable {
public let value: String
public init(_ value: String) throws {
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
guard trimmed.count >= 2 else {
throw SearchError.queryTooShort
}
self.value = trimmed
}
}
Errore:
public enum SearchError: Error, Equatable, Sendable {
case queryTooShort
case storyNotFound(Story.ID)
}
Repository protocol
Il caso d’uso non sa da dove arrivano i dati:
public protocol StoryRepository: Sendable {
func search(query: SearchQuery) async throws -> [Story]
func story(id: Story.ID) async throws -> Story
}
Questo protocollo è una porta. L’infrastruttura arriverà dopo.
Use case
public struct SearchStoriesUseCase: Sendable {
private let repository: StoryRepository
public init(repository: StoryRepository) {
self.repository = repository
}
public func execute(query: SearchQuery) async throws -> [Story] {
let stories = try await repository.search(query: query)
return stories.sorted { left, right in
left.score > right.score
}
}
}
La regola “ordinare per score” è applicativa. Non deve stare nella view.
Fake repository
public actor InMemoryStoryRepository: StoryRepository {
private var stories: [Story.ID: Story]
public init(stories: [Story]) {
self.stories = Dictionary(uniqueKeysWithValues: stories.map { ($0.id, $0) })
}
public func search(query: SearchQuery) async throws -> [Story] {
stories.values.filter { story in
story.title.localizedCaseInsensitiveContains(query.value)
}
}
public func story(id: Story.ID) async throws -> Story {
guard let story = stories[id] else {
throw SearchError.storyNotFound(id)
}
return story
}
}
Un fake actor è perfetto per test async con stato.
Test dominio
@Test
func queryRifiutaTestoTroppoCorto() {
#expect(throws: SearchError.queryTooShort) {
try SearchQuery("s")
}
}
@Test
func queryRimuoveSpazi() throws {
let query = try SearchQuery(" swift ")
#expect(query.value == "swift")
}
Questi test proteggono regole che valgono ovunque.
Test use case
@Test
func ricercaOrdinaPerScoreDecrescente() async throws {
let repository = InMemoryStoryRepository(stories: [
Story(id: .init(rawValue: 1), title: "Swift", url: nil, score: 10),
Story(id: .init(rawValue: 2), title: "Swift Server", url: nil, score: 40)
])
let useCase = SearchStoriesUseCase(repository: repository)
let result = try await useCase.execute(query: try SearchQuery("swift"))
#expect(result.map(\\.score) == [40, 10])
}
La rete non serve per sapere se la regola funziona.
Confini
In questa milestone sono vietati:
URLSession;JSONDecoderper payload remoti;SwiftUI.View;Vapor.Request;FileManager.default;- singleton;
- global mutable state.
Se ti sembra impossibile, hai probabilmente messo nel dominio qualcosa che appartiene all’infrastruttura.
Errori comuni
Evita:
- tipi anemici con tutte
StringeInt; Optionaldove serve un errore preciso;- protocollo repository enorme;
- use case che restituisce DTO;
- test che copiano l’implementazione;
- modelli chiamati
Manager; - dominio guidato dal JSON.
Checklist
La milestone 1 è completa quando:
- il dominio compila da solo;
- gli invarianti sono nei tipi;
- gli errori sono espliciti;
- i use case dipendono da protocolli;
- i test non usano rete o disco;
- un fake repository permette flussi async;
- puoi spiegare il nucleo in 2 minuti.
Esercizio
Per il tuo progetto:
- crea target
DomaineApplication; - definisci 4-6 tipi;
- crea almeno 2 errori custom;
- crea 2 protocolli o 1 protocollo piccolo;
- implementa 2 use case;
- scrivi 5 test.
Obiettivo: far passare i test prima di creare qualsiasi interfaccia.