Menu
Coddy logo textTech

Erstellermuster

Teil des Abschnitts Objektorientierte Programmierung der Swift-Journey von Coddy. Lektion 48 von 57.

Das Builder-Muster setzt ein komplexes Objekt Schritt für Schritt zusammen. Jeder Schritt legt einen Teil fest und gibt den Builder zurück, sodass die Schritte verkettet werden können, und ein abschließendes build() erstellt das fertige Objekt:

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")

Ausgabe:

POST /users 1 header

Der Builder kann die gesamte Konfiguration überprüfen, bevor er das Objekt erstellt, sodass niemals ein ungültiges Objekt erstellt wird. Hier löst build() einen Fehler aus, wenn ein erforderlicher Bestandteil fehlt:

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)")
}

Ausgabe:

cannot build: missingURL

Jeder Schritt kopiert die Struktur und ändert die Kopie, sodass ein teilweise konfigurierter Builder als Vorlage für mehrere Objekte wiederverwendet werden kann:

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())

Ausgabe:

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

Swifts Standardparameterwerte decken einfache Fälle bereits ab (Request(url: "/", method: "GET")). Ein Builder lohnt sich, wenn ein Objekt viele optionale Bestandteile hat, Bestandteile voneinander abhängen oder Regeln geprüft werden müssen, bevor es erstellt werden kann.

// einfach: Standardargumente reichen aus
struct Point { var x = 0, y = 0 }
// komplex: viele optionale Teile und Validierung -> Builder
let report = ReportBuilder().title("Q3").add(section: s1).add(section: s2).footer("draft").build()
challenge icon

Aufgabe

Einfach

Schreibe einen Builder für E-Mail-Nachrichten. EmailBuilder verfügt über verkettbare Schritte to(_:) (kann mehrmals aufgerufen werden und fügt Empfänger hinzu), subject(_:), body(_:) und urgent(). build() gibt ein Email zurück oder wirft EmailError.noRecipients beziehungsweise EmailError.noSubject. Einer dringenden E-Mail wird [URGENT] vor dem Betreff vorangestellt.

Der bereitgestellte Code liest Befehle für jede E-Mail, die durch Zeilen mit --- getrennt sind (to ada@x.io, subject Hello, body text, urgent, danach send), und gibt To: ada@x.io, bo@x.io | [URGENT] Hello | text, error: no recipients oder error: no subject aus.

Dein Code gehört in Email.swift, EmailError.swift und EmailBuilder.swift. main.swift enthält den bereitgestellten Eingabe-/Ausgabecode und kann nicht bearbeitet werden.

Probier es selbst

// Bereitgestellter Ein-/Ausgabe-Code: belasse ihn wie er ist
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 iconTeste dich selbst

Diese Lektion enthält ein kurzes Quiz. Starte die Lektion, um es zu beantworten und deinen Fortschritt zu speichern.

Alle Lektionen in Objektorientierte Programmierung

Übe selbstständig: Swift-Playground