Menu
Coddy logo textTech

Pattern Builder

Fa parte della sezione Programmazione orientata agli oggetti del percorso Swift di Coddy. Lezione 48 di 57.

Il pattern builder assembla un oggetto complesso un passaggio alla volta. Ogni passaggio imposta una parte e restituisce il builder, così i passaggi possono essere concatenati, e un build() finale crea l'oggetto completo:

struct Request {
    let url: String
    let method: String
    let headers: [String: String]
}

struct RequestBuilder {
    private var url = ""
    private var method = "GET"
    private var headers: [String: String] = [:]

    func url(_ u: String) -> RequestBuilder { var b = self; b.url = u; return b }
    func method(_ m: String) -> RequestBuilder { var b = self; b.method = m; return b }
    func header(_ k: String, _ v: String) -> RequestBuilder { var b = self; b.headers[k] = v; return b }
    func build() -> Request { Request(url: url, method: method, headers: headers) }
}

let req = RequestBuilder().url("/users").method("POST").header("Auth", "token").build()
print("\(req.method) \(req.url) \(req.headers.count) header")

Output:

POST /users 1 header

Il builder può controllare l’intera configurazione prima di creare l’oggetto, così non viene mai costruito un oggetto non valido. Qui build() genera un errore quando manca una parte obbligatoria:

enum BuildError: Error { case missingURL }

struct Request { let url: String; let method: String }

struct RequestBuilder {
    private var url: String?
    private var method = "GET"
    func url(_ u: String) -> RequestBuilder { var b = self; b.url = u; return b }
    func method(_ m: String) -> RequestBuilder { var b = self; b.method = m; return b }
    func build() throws -> Request {
        guard let url else { throw BuildError.missingURL }
        return Request(url: url, method: method)
    }
}

do {
    _ = try RequestBuilder().method("PUT").build()
} catch {
    print("cannot build: \(error)")
}

Output:

cannot build: missingURL

Ogni passaggio copia la struct e modifica la copia, così un builder configurato in parte può essere riutilizzato come modello per diversi oggetti:

struct PizzaBuilder {
    private var size = "medium"
    private var toppings: [String] = []
    func size(_ s: String) -> PizzaBuilder { var b = self; b.size = s; return b }
    func add(_ t: String) -> PizzaBuilder { var b = self; b.toppings.append(t); return b }
    func build() -> String { "\(size) pizza with " + (toppings.isEmpty ? "nothing" : toppings.joined(separator: ", ")) }
}

let base = PizzaBuilder().size("large").add("cheese")
print(base.add("ham").build())
print(base.add("olives").build())

Output:

large pizza with cheese, ham
large pizza with cheese, olives

I valori predefiniti dei parametri di Swift coprono già i casi semplici (Request(url: "/", method: "GET")). Un builder è utile quando un oggetto ha molte parti opzionali, parti che dipendono l’una dall’altra o regole da verificare prima che possa esistere.

// semplice: gli argomenti predefiniti sono sufficienti
struct Point { var x = 0, y = 0 }
// complesso: molte parti opzionali e validazione -> builder
let report = ReportBuilder().title("Q3").add(section: s1).add(section: s2).footer("draft").build()
challenge icon

Sfida

Facile

Scrivi un builder per i messaggi email. EmailBuilder ha passaggi concatenabili to(_:) (può essere chiamato più volte, aggiungendo destinatari), subject(_:), body(_:) e urgent(). build() restituisce un Email oppure genera EmailError.noRecipients o EmailError.noSubject. Un'email urgente ha [URGENT] davanti all'oggetto.

Il codice fornito legge i comandi per ogni email, separati da righe --- (to ada@x.io, subject Hello, body text, urgent, poi send) e stampa To: ada@x.io, bo@x.io | [URGENT] Hello | text, error: no recipients oppure error: no subject.

Il tuo codice va inserito in Email.swift, EmailError.swift e EmailBuilder.swift. main.swift contiene il codice di input/output fornito e non può essere modificato.

Provalo tu

// Codice di input/output fornito: lascialo così com'è
var input: [String] = []
while let line = readLine() { input.append(line) }
var builder = EmailBuilder()
for line in input {
    let p = line.split(separator: " ", maxSplits: 1).map(String.init)
    switch p[0] {
    case "to": builder = builder.to(p[1])
    case "subject": builder = builder.subject(p[1])
    case "body": builder = builder.body(p[1])
    case "urgent": builder = builder.urgent()
    case "send":
        do {
            let e = try builder.build()
            print("To: \(e.recipients.joined(separator: ", ")) | \(e.subject) | \(e.body)")
        } catch EmailError.noRecipients {
            print("error: no recipients")
        } catch {
            print("error: no subject")
        }
    default: builder = EmailBuilder()
    }
}
quiz iconMettiti alla prova

Questa lezione include un breve quiz. Inizia la lezione per rispondere e tenere traccia dei tuoi progressi.

Tutte le lezioni di Programmazione orientata agli oggetti

Esercitati da solo: Playground Swift