Menu
Coddy logo textTech

Patron Builder

Fait partie de la section Programmation orientée objet du Journey Swift de Coddy. Leçon 48 sur 57.

Le builder assemble un objet complexe étape par étape. Chaque étape définit une partie et renvoie le builder, ce qui permet d’enchaîner les étapes, et un dernier build() crée l’objet final :

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

Sortie :

POST /users 1 header

Le builder peut vérifier l’ensemble de la configuration avant de créer l’objet, de sorte qu’un objet invalide n’est jamais construit. Ici, build() lève une erreur lorsqu’un élément requis est manquant :

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

Sortie :

cannot build: missingURL

Chaque étape copie la structure et modifie la copie, de sorte qu’un constructeur partiellement configuré peut être réutilisé comme modèle pour plusieurs objets :

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

Sortie :

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

Les valeurs de paramètres par défaut de Swift couvrent déjà les cas simples (Request(url: "/", method: "GET")). Un builder devient utile lorsqu’un objet comporte de nombreuses parties facultatives, des parties qui dépendent les unes des autres ou des règles à vérifier avant qu’il puisse être créé.

// simple : les arguments par défaut suffisent
struct Point { var x = 0, y = 0 }
// complexe : de nombreuses parties optionnelles et validation -> builder
let report = ReportBuilder().title("Q3").add(section: s1).add(section: s2).footer("draft").build()
challenge icon

Défi

Facile

Écrivez un constructeur pour les messages électroniques. EmailBuilder comporte les étapes chaînables to(_:) (peut être appelé plusieurs fois pour ajouter des destinataires), subject(_:), body(_:) et urgent(). build() renvoie un Email ou lève EmailError.noRecipients ou EmailError.noSubject. Un e-mail urgent reçoit [URGENT] devant son objet.

Le code fourni lit les commandes pour chaque e-mail, séparées par des lignes --- (to ada@x.io, subject Hello, body text, urgent, puis send) et affiche To: ada@x.io, bo@x.io | [URGENT] Hello | text, error: no recipients ou error: no subject.

Votre code doit être placé dans Email.swift, EmailError.swift et EmailBuilder.swift. main.swift contient le code fourni pour les entrées et sorties et ne peut pas être modifié.

Essayez vous-même

// Code d'entrée/sortie fourni : le garder tel quel
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 iconTestez-vous

Cette leçon comprend un petit quiz. Commencez la leçon pour y répondre et suivre votre progression.

Toutes les leçons de Programmation orientée objet

Entraînez-vous par vous-même : Playground Swift