ビルダーパターン
CoddyのSwiftジャーニー「オブジェクト指向プログラミング」セクションの一部。レッスン 48/57。
ビルダーパターンは、複雑なオブジェクトを段階的に組み立てます。各ステップで一部分を設定してビルダーを返すため、ステップをチェーンでき、最後に 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, olivesSwiftのデフォルト引数値は、すでに単純なケース(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()チャレンジ
簡単メールメッセージのビルダーを作成してください。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()
}
}
このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。
オブジェクト指向プログラミングのすべてのレッスン
自分で練習してみよう: Swiftプレイグラウンド