Строитель — это порождающий паттерн проектирования, который позволяет создавать объекты пошагово.
В отличие от других порождающих паттернов, Строитель позволяет производить различные продукты, используя один и тот же процесс строительства.
Концептуальный пример
Этот пример показывает структуру паттерна Строитель , а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом.
После ознакомления со структурой, вам будет легче воспринимать второй пример, который рассматривает реальный случай использования паттерна в мире 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.