Budowniczy to kreacyjny wzorzec projektowy umożliwiający tworzenie złożonych obiektów krok po kroku.
W przeciwieństwie do innych wzorców kreacyjnych Budowniczy nie zakłada definiowania wspólnego interfejsu dla produktów. Dzięki temu da się wytwarzać różne produkty stosując ten sam proces konstrukcyjny.
Przykład koncepcyjny
Poniższy przykład ilustruje strukturę wzorca Budowniczy ze szczególnym naciskiem na następujące zagadnienia:
Z jakich składa się klas?
Jakie role pełnią te klasy?
W jaki sposób elementy wzorca są ze sobą powiązane?
Poznawszy strukturę wzorca będzie ci łatwiej zrozumieć poniższy przykład, oparty na prawdziwym przypadku użycia Swift.
Example.swift: Przykład koncepcyjny
import XCTest
/// The Builder interface specifies methods for creating the different parts of
/// the Product objects.
protocol Builder {
func producePartA()
func producePartB()
func producePartC()
}
/// The Concrete Builder classes follow the Builder interface and provide
/// specific implementations of the building steps. Your program may have
/// several variations of Builders, implemented differently.
class ConcreteBuilder1: Builder {
/// A fresh builder instance should contain a blank product object, which is
/// used in further assembly.
private var product = Product1()
func reset() {
product = Product1()
}
/// All production steps work with the same product instance.
func producePartA() {
product.add(part: "PartA1")
}
func producePartB() {
product.add(part: "PartB1")
}
func producePartC() {
product.add(part: "PartC1")
}
/// Concrete Builders are supposed to provide their own methods for
/// retrieving results. That's because various types of builders may create
/// entirely different products that don't follow the same interface.
/// Therefore, such methods cannot be declared in the base Builder interface
/// (at least in a statically typed programming language).
///
/// Usually, after returning the end result to the client, a builder
/// instance is expected to be ready to start producing another product.
/// That's why it's a usual practice to call the reset method at the end of
/// the `getProduct` method body. However, this behavior is not mandatory,
/// and you can make your builders wait for an explicit reset call from the
/// client code before disposing of the previous result.
func retrieveProduct() -> Product1 {
let result = self.product
reset()
return result
}
}
/// The Director is only responsible for executing the building steps in a
/// particular sequence. It is helpful when producing products according to a
/// specific order or configuration. Strictly speaking, the Director class is
/// optional, since the client can control builders directly.
class Director {
private var builder: Builder?
/// The Director works with any builder instance that the client code passes
/// to it. This way, the client code may alter the final type of the newly
/// assembled product.
func update(builder: Builder) {
self.builder = builder
}
/// The Director can construct several product variations using the same
/// building steps.
func buildMinimalViableProduct() {
builder?.producePartA()
}
func buildFullFeaturedProduct() {
builder?.producePartA()
builder?.producePartB()
builder?.producePartC()
}
}
/// It makes sense to use the Builder pattern only when your products are quite
/// complex and require extensive configuration.
///
/// Unlike in other creational patterns, different concrete builders can produce
/// unrelated products. In other words, results of various builders may not
/// always follow the same interface.
class Product1 {
private var parts = [String]()
func add(part: String) {
self.parts.append(part)
}
func listParts() -> String {
return "Product parts: " + parts.joined(separator: ", ") + "\n"
}
}
/// The client code creates a builder object, passes it to the director and then
/// initiates the construction process. The end result is retrieved from the
/// builder object.
class Client {
// ...
static func someClientCode(director: Director) {
let builder = ConcreteBuilder1()
director.update(builder: builder)
print("Standard basic product:")
director.buildMinimalViableProduct()
print(builder.retrieveProduct().listParts())
print("Standard full featured product:")
director.buildFullFeaturedProduct()
print(builder.retrieveProduct().listParts())
// Remember, the Builder pattern can be used without a Director class.
print("Custom product:")
builder.producePartA()
builder.producePartC()
print(builder.retrieveProduct().listParts())
}
// ...
}
/// Let's see how it all comes together.
class BuilderConceptual: XCTestCase {
func testBuilderConceptual() {
let director = Director()
Client.someClientCode(director: director)
}
}
Output.txt: Wynik działania
Standard basic product:
Product parts: PartA1
Standard full featured product:
Product parts: PartA1, PartB1, PartC1
Custom product:
Product parts: PartA1, PartC1
Przykład z prawdziwego życia
Example.swift: Przykład z prawdziwego życia
import Foundation
import XCTest
class BaseQueryBuilder<Model: DomainModel> {
typealias Predicate = (Model) -> (Bool)
func limit(_ limit: Int) -> BaseQueryBuilder<Model> {
return self
}
func filter(_ predicate: @escaping Predicate) -> BaseQueryBuilder<Model> {
return self
}
func fetch() -> [Model] {
preconditionFailure("Should be overridden in subclasses.")
}
}
class RealmQueryBuilder<Model: DomainModel>: BaseQueryBuilder<Model> {
enum Query {
case filter(Predicate)
case limit(Int)
/// ...
}
fileprivate var operations = [Query]()
@discardableResult
override func limit(_ limit: Int) -> RealmQueryBuilder<Model> {
operations.append(Query.limit(limit))
return self
}
@discardableResult
override func filter(_ predicate: @escaping Predicate) -> RealmQueryBuilder<Model> {
operations.append(Query.filter(predicate))
return self
}
override func fetch() -> [Model] {
print("RealmQueryBuilder: Initializing RealmDataProvider with \(operations.count) operations:")
return RealmProvider().fetch(operations)
}
}
class CoreDataQueryBuilder<Model: DomainModel>: BaseQueryBuilder<Model> {
enum Query {
case filter(Predicate)
case limit(Int)
case includesPropertyValues(Bool)
/// ...
}
fileprivate var operations = [Query]()
override func limit(_ limit: Int) -> CoreDataQueryBuilder<Model> {
operations.append(Query.limit(limit))
return self
}
override func filter(_ predicate: @escaping Predicate) -> CoreDataQueryBuilder<Model> {
operations.append(Query.filter(predicate))
return self
}
func includesPropertyValues(_ toggle: Bool) -> CoreDataQueryBuilder<Model> {
operations.append(Query.includesPropertyValues(toggle))
return self
}
override func fetch() -> [Model] {
print("CoreDataQueryBuilder: Initializing CoreDataProvider with \(operations.count) operations.")
return CoreDataProvider().fetch(operations)
}
}
/// Data Providers contain a logic how to fetch models. Builders accumulate
/// operations and then update providers to fetch the data.
class RealmProvider {
func fetch<Model: DomainModel>(_ operations: [RealmQueryBuilder<Model>.Query]) -> [Model] {
print("RealmProvider: Retrieving data from Realm...")
for item in operations {
switch item {
case .filter(_):
print("RealmProvider: executing the 'filter' operation.")
/// Use Realm instance to filter results.
break
case .limit(_):
print("RealmProvider: executing the 'limit' operation.")
/// Use Realm instance to limit results.
break
}
}
/// Return results from Realm
return []
}
}
class CoreDataProvider {
func fetch<Model: DomainModel>(_ operations: [CoreDataQueryBuilder<Model>.Query]) -> [Model] {
/// Create a NSFetchRequest
print("CoreDataProvider: Retrieving data from CoreData...")
for item in operations {
switch item {
case .filter(_):
print("CoreDataProvider: executing the 'filter' operation.")
/// Set a 'predicate' for a NSFetchRequest.
break
case .limit(_):
print("CoreDataProvider: executing the 'limit' operation.")
/// Set a 'fetchLimit' for a NSFetchRequest.
break
case .includesPropertyValues(_):
print("CoreDataProvider: executing the 'includesPropertyValues' operation.")
/// Set an 'includesPropertyValues' for a NSFetchRequest.
break
}
}
/// Execute a NSFetchRequest and return results.
return []
}
}
protocol DomainModel {
/// The protocol groups domain models to the common interface
}
private struct User: DomainModel {
let id: Int
let age: Int
let email: String
}
class BuilderRealWorld: XCTestCase {
func testBuilderRealWorld() {
print("Client: Start fetching data from Realm")
clientCode(builder: RealmQueryBuilder<User>())
print()
print("Client: Start fetching data from CoreData")
clientCode(builder: CoreDataQueryBuilder<User>())
}
fileprivate func clientCode(builder: BaseQueryBuilder<User>) {
let results = builder.filter({ $0.age < 20 })
.limit(1)
.fetch()
print("Client: I have fetched: " + String(results.count) + " records.")
}
}
Output.txt: Wynik działania
Client: Start fetching data from Realm
RealmQueryBuilder: Initializing RealmDataProvider with 2 operations:
RealmProvider: Retrieving data from Realm...
RealmProvider: executing the 'filter' operation.
RealmProvider: executing the 'limit' operation.
Client: I have fetched: 0 records.
Client: Start fetching data from CoreData
CoreDataQueryBuilder: Initializing CoreDataProvider with 2 operations.
CoreDataProvider: Retrieving data from CoreData...
CoreDataProvider: executing the 'filter' operation.
CoreDataProvider: executing the 'limit' operation.
Client: I have fetched: 0 records.