Menu
Coddy logo textTech

תבנית הבנאי

חלק מהיחידה תכנות מונחה עצמים במסלול ה-Swift של Coddy. שיעור 48 מתוך 57.

תבנית הbuilder מרכיבה אובייקט מורכב שלב אחר שלב. כל שלב מגדיר חלק אחד ומחזיר את הבנאי, כך שאפשר לשרשר את השלבים, וקריאה סופית ל־build() יוצרת את האובייקט המוגמר:

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

פלט:

POST /users 1 header

הבונה יכול לבדוק את כל התצורה לפני יצירת האובייקט, כך שאובייקט לא תקין לעולם לא נבנה. כאן build() זורקת שגיאה כאשר חסר רכיב נדרש:

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

פלט:

cannot build: missingURL

כל שלב מעתיק את המבנה ומשנה את העותק, כך שאפשר להשתמש שוב בבנאי שהוגדר בחלקו כתבנית לכמה אובייקטים:

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

פלט:

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

ערכי ברירת המחדל של פרמטרים ב-Swift כבר מכסים מקרים פשוטים (Request(url: "/", method: "GET")). מחלקת בנייה מועילה כשלאובייקט יש חלקים אופציונליים רבים, חלקים שתלויים זה בזה או כללים שצריך לבדוק לפני שהוא יכול להתקיים.

// פשוט: ארגומנטים עם ערכי ברירת מחדל מספיקים
struct Point { var x = 0, y = 0 }
// מורכב: חלקים אופציונליים רבים ואימות -> בנאי
let report = ReportBuilder().title("Q3").add(section: s1).add(section: s2).footer("draft").build()
challenge icon

אתגר

קל

כתבו בנאי להודעות דוא״ל. ל־EmailBuilder יש שלבים שניתן לשרשר: to(_:) (אפשר לקרוא לו כמה פעמים כדי להוסיף נמענים), subject(_:), body(_:) ו־urgent(). build() מחזירה Email או זורקת EmailError.noRecipients או EmailError.noSubject. הודעת דוא״ל דחופה מקבלת את [URGENT] לפני הנושא שלה.

הקוד שסופק קורא פקודות עבור כל הודעת דוא״ל, כשהן מופרדות בשורות --- (to ada@x.io, subject Hello, body text, urgent, ואז send), ומדפיס To: ada@x.io, bo@x.io | [URGENT] Hello | text, error: no recipients או error: no subject.

כתבו את הקוד שלכם בקבצים Email.swift, EmailError.swift ו־EmailBuilder.swift. הקובץ main.swift מכיל את קוד הקלט/פלט שסופק, ואי אפשר לערוך אותו.

נסו בעצמכם

// קוד הקלט/פלט שסופק: יש להשאיר אותו כפי שהוא
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 iconבחנו את עצמכם

השיעור הזה כולל חידון קצר. התחילו את השיעור כדי לענות עליו ולעקוב אחרי ההתקדמות.

כל השיעורים ביחידה תכנות מונחה עצמים

תרגלו בעצמכם: Swift אונליין