Menu
Coddy logo textTech

Wzorzec budowniczy

Część sekcji Programowanie obiektowe ścieżki Swift w Coddy. Lekcja 48 z 57.

Wzorzec buildera tworzy złożony obiekt krok po kroku. Każdy krok ustawia jedną jego część i zwraca buildera, dzięki czemu kroki można łączyć, a końcowe wywołanie build() tworzy gotowy obiekt:

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

Wynik:

POST /users 1 header

Konstruktor może sprawdzić całą konfigurację przed utworzeniem obiektu, dzięki czemu nie zostanie utworzony nieprawidłowy obiekt. Tutaj build() zgłasza błąd, gdy brakuje wymaganego elementu:

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

Wynik:

cannot build: missingURL

Każdy krok kopiuje strukturę i zmienia kopię, więc częściowo skonfigurowany konstruktor można ponownie wykorzystać jako szablon dla kilku obiektów:

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

Wynik:

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

Domyślne wartości parametrów w Swift już wystarczają w prostych przypadkach (Request(url: "/", method: "GET")). Konstruktor przydaje się, gdy obiekt ma wiele opcjonalnych części, części zależne od siebie lub reguły, które trzeba sprawdzić, zanim będzie mógł istnieć.

// proste: argumenty domyślne wystarczą
struct Point { var x = 0, y = 0 }
// złożone: wiele opcjonalnych części i walidacja -> builder
let report = ReportBuilder().title("Q3").add(section: s1).add(section: s2).footer("draft").build()
challenge icon

Wyzwanie

Łatwy

Napisz konstruktor wiadomości e-mail. EmailBuilder udostępnia łańcuchowe kroki to(_:) (można wywołać kilka razy, dodając odbiorców), subject(_:), body(_:) i urgent(). build() zwraca Email albo zgłasza EmailError.noRecipients lub EmailError.noSubject. Pilna wiadomość e-mail ma przed tematem dodany tekst [URGENT] .

Dostarczony kod odczytuje polecenia dla każdej wiadomości e-mail, rozdzielone wierszami --- (to ada@x.io, subject Hello, body text, urgent, a następnie send), i wyświetla To: ada@x.io, bo@x.io | [URGENT] Hello | text, error: no recipients lub error: no subject.

Twój kod umieść w plikach Email.swift, EmailError.swift i EmailBuilder.swift. Plik main.swift zawiera dostarczony kod wejścia/wyjścia i nie można go edytować.

Spróbuj swoich sił

// Dostarczony kod wejścia/wyjścia: pozostaw go bez zmian
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 iconSprawdź się

Ta lekcja zawiera krótki quiz. Zacznij lekcję, żeby na niego odpowiedzieć i śledzić swoje postępy.

Wszystkie lekcje w sekcji Programowanie obiektowe

Poćwicz samodzielnie: Edytor online Swift