
스위프트로 작성된 빌더
빌더는 복잡한 객체들을 단계별로 생성할 수 있도록 하는 생성 디자인 패턴입니다.
다른 생성 패턴과 달리 빌더 패턴은 제품들에 공통 인터페이스를 요구하지 않습니다. 이를 통해 같은 생성공정을 사용하여 다양한 제품을 생산할 수 있습니다.
복잡도:
인기도:
사용 예시들: 빌더 패턴은 스위프트 개발자들에게 잘 알려진 패턴이며, 가능한 설정 옵션이 많은 객체를 만들어야 할 때 특히 유용합니다.
식별법: 빌더 패턴은 하나의 생성 메서드와 결과 객체를 설정하기 위한 여러 메서드가 있는 클래스가 있습니다. 또 빌더 메서드들은 자주 사슬식 연결을 지원합니다 (예: 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.