 
                스위프트로 작성된 빌더
빌더는 복잡한 객체들을 단계별로 생성할 수 있도록 하는 생성 디자인 패턴입니다.
다른 생성 패턴과 달리 빌더 패턴은 제품들에 공통 인터페이스를 요구하지 않습니다. 이를 통해 같은 생성공정을 사용하여 다양한 제품을 생산할 수 있습니다.
복잡도:
인기도:
사용 예시들: 빌더 패턴은 스위프트 개발자들에게 잘 알려진 패턴이며, 가능한 설정 옵션이 많은 객체를 만들어야 할 때 특히 유용합니다.
  식별법: 빌더 패턴은 하나의 생성 메서드와 결과 객체를 설정하기 위한 여러 메서드가 있는 클래스가 있습니다. 또 빌더 메서드들은 자주 사슬식 연결을 지원합니다 (예: someBuilder.setValueA(1).setValueB(2).create()).
개념적인 예시
이 예시는 빌더 디자인 패턴의 구조를 보여주고 다음 질문에 중점을 둡니다:
- 패턴은 어떤 클래스들로 구성되어 있나요?
- 이 클래스들은 어떤 역할을 하나요?
- 패턴의 요소들은 어떻게 서로 연관되어 있나요?
이 패턴의 구조를 배우면 실제 스위프트 사용 사례를 기반으로 하는 다음 예시를 더욱 쉽게 이해할 수 있을 것입니다.
Example.swift: 개념적인 예시
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: 실행 결과
Standard basic product:
Product parts: PartA1
Standard full featured product:
Product parts: PartA1, PartB1, PartC1
Custom product:
Product parts: PartA1, PartC1
실제 사례 예시
Example.swift: 실제 사례 예시
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: 실행 결과
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.