
Строитель на Swift
Строитель — это порождающий паттерн проектирования, который позволяет создавать объекты пошагово.
В отличие от других порождающих паттернов, Строитель позволяет производить различные продукты, используя один и тот же процесс строительства.
Сложность:
Популярность:
Применимость: Паттерн можно часто встретить в Swift-коде, особенно там, где требуется пошаговое создание продуктов или конфигурация сложных объектов.
Признаки применения паттерна: Строителя можно узнать в классе, который имеет один создающий метод и несколько методов настройки создаваемого продукта. Обычно, методы настройки вызывают для удобства цепочкой (например, someBuilder.setValueA(1).setValueB(2).create()
).
Концептуальный пример
Этот пример показывает структуру паттерна Строитель, а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом.
После ознакомления со структурой, вам будет легче воспринимать второй пример, который рассматривает реальный случай использования паттерна в мире Swift.
Example.swift: Пример структуры паттерна
import XCTest /// Интерфейс Строителя объявляет создающие методы для различных частей объектов /// Продуктов. protocol Builder { func producePartA() func producePartB() func producePartC() } /// Классы Конкретного Строителя следуют интерфейсу Строителя и предоставляют /// конкретные реализации шагов построения. Ваша программа может иметь несколько /// вариантов Строителей, реализованных по-разному. class ConcreteBuilder1: Builder { /// Новый экземпляр строителя должен содержать пустой объект продукта, /// который используется в дальнейшей сборке. private var product = Product1() func reset() { product = Product1() } /// Все этапы производства работают с одним и тем же экземпляром продукта. func producePartA() { product.add(part: "PartA1") } func producePartB() { product.add(part: "PartB1") } func producePartC() { product.add(part: "PartC1") } /// Конкретные Строители должны предоставить свои собственные методы /// получения результатов. Это связано с тем, что различные типы строителей /// могут создавать совершенно разные продукты с разными интерфейсами. /// Поэтому такие методы не могут быть объявлены в базовом интерфейсе /// Строителя (по крайней мере, в статически типизированном языке /// программирования). /// /// Как правило, после возвращения конечного результата клиенту, экземпляр /// строителя должен быть готов к началу производства следующего продукта. /// Поэтому обычной практикой является вызов метода сброса в конце тела /// метода getProduct. Однако такое поведение не является обязательным, вы /// можете заставить своих строителей ждать явного запроса на сброс из кода /// клиента, прежде чем избавиться от предыдущего результата. func retrieveProduct() -> Product1 { let result = self.product reset() return result } } /// Директор отвечает только за выполнение шагов построения в определённой /// последовательности. Это полезно при производстве продуктов в определённом /// порядке или особой конфигурации. Строго говоря, класс Директор необязателен, /// так как клиент может напрямую управлять строителями. class Director { private var builder: Builder? /// Директор работает с любым экземпляром строителя, который передаётся ему /// клиентским кодом. Таким образом, клиентский код может изменить конечный /// тип вновь собираемого продукта. func update(builder: Builder) { self.builder = builder } /// Директор может строить несколько вариаций продукта, используя одинаковые /// шаги построения. func buildMinimalViableProduct() { builder?.producePartA() } func buildFullFeaturedProduct() { builder?.producePartA() builder?.producePartB() builder?.producePartC() } } /// Имеет смысл использовать паттерн Строитель только тогда, когда ваши продукты /// достаточно сложны и требуют обширной конфигурации. /// /// В отличие от других порождающих паттернов, различные конкретные строители /// могут производить несвязанные продукты. Другими словами, результаты /// различных строителей могут не всегда следовать одному и тому же интерфейсу. class Product1 { private var parts = [String]() func add(part: String) { self.parts.append(part) } func listParts() -> String { return "Product parts: " + parts.joined(separator: ", ") + "\n" } } /// Клиентский код создаёт объект-строитель, передаёт его директору, а затем /// инициирует процесс построения. Конечный результат извлекается из объекта- /// строителя. 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()) // Помните, что паттерн Строитель можно использовать без класса // Директор. print("Custom product:") builder.producePartA() builder.producePartC() print(builder.retrieveProduct().listParts()) } // ... } /// Давайте посмотрим как всё это будет работать. 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.