
Всем привет! Зачастую чтобы в чем то разобраться полезнее один раз увидеть конкретный пример чем несколько раз прочитать заумное описание.Решил написать ряд небольших статей для начинающих, в которых дать краткое описание основных паттернов проектирования и привести лаконичные примеры их использования.Данная статья, как можно догадаться из названия =), посвящена порождающим паттернам.
Фабричный метод /Factory Method
Фабричный метод — это порождающий паттерн проектирования, который определяет общий интерфейс для создания объектов в суперклассе, позволяя подклассам изменять тип создаваемых объектов.
Представим, что мы разрабатываем UI фреймворк и хотим, чтобы он мог использоваться и в iOS и в Windows. У каждой из платформ свои уникальные UI элементы. Нам нужен механизм, который создавал бы элементы в зависимости от платформы на которой мы запустили нашу программу. В этом нам может помочь паттерн фабричный метод.
Разберем на примере кнопки.
Для начала создадим протокол.
protocol IButton {
    func render()
    func onClick()
}Создадим кнопки для windows и для iOS.
class windowsButton: IButton {
    func render() {
        print("wiwindowsButton.windowsButton")
    }
    
    func onClick() {
        print("windowsButton.onClick")
    }
}
class iosButton: IButton {
    func render() {
        print("iosButton.windowsButton")
    }
    
    func onClick() {
        print("iosButton.onClick")
    }
}Определим протокол класса создателя. Это тот класс, который будет создавать кнопку и отображать ее на экране.
protocol iCreator {
    func render()
    func createButton() -> IButton
}Определим классы создатели для обеих платформ.
class windowsCreator: iCreator {
    func render() {
        let button = createButton()
        button.render()
        print("rendering windows button")
    }
    
    func createButton() -> IButton {
        print("creating windows button")
        return windowsButton()
    }
}
class iosCreator: iCreator {
    func render() {
        let button = createButton()
        button.render()
        print("rendering ios button")
    }
    
    func createButton() -> IButton {
        print("creating ios button")
        return iosButton()
    }
}Обратите внимание, что в метода createButton iosCreator возвращает iosButton, а windowsCreator соответственно windowsButton.
Теперь наш код может в зависимости от системы, в которой он запущен генерировать iOS или Windows UI элемент.
// Application
enum System {
    case windows
    case ios
}
class MyAplication {
    let creator: iCreator?
    
    init(system: System) {
        switch system {
        case .windows:
            creator = windowsCreator()
        case.ios:
            creator = iosCreator()
        }
    }
}В момент запуска системы определяется какой тип класса создателя будет использоваться.
let myApplication = MyAplication(system: .windows)
myApplication.creator?.render()В данном случае создастся и отобразиться кнопка для Windows.
Абстрактная фабрика / Abstract Factory
Абстрактная фабрика — это порождающий паттерн проектирования, который позволяет создавать семейства связанных объектов, не привязываясь к конкретным классам создаваемых объектов.
Логическим продолжением фабричного метода является паттерн абстрактная фабрика.
Что если нам нужно создавать не один UI элемент, а целое семейство элементов в зависимости от среды в которой запущена наша программа или иных условий.
Воспользуемся паттерном абстрактная фабрика.
Создадим протоколы элементов, которые мы хотим создавать. Для простоты возьмем два. Кнопку и чекбокс.
protocol IButton {
    func pressButton()
}
protocol ICheckmark {
    func chooseCheckMark()
}Создадим классы кнопок.
class WindowsButton: IButton {
    func pressButton() {
        print("Windows button pressed")
    }
}
class IosButton: IButton {
    func pressButton() {
        print("IOS button pressed")
    }
}
И классы чекбоксов.
class WindowsCheckmark: ICheckmark {
    func chooseCheckMark() {
        print("Windows checkmark choosen")
    }
}
class IosCheckmark: ICheckmark {
    func chooseCheckMark() {
        print("IOS checkmark choosen")
    }
}Определим протокол фабрики, которая будет поставлять нам элементы.
protocol AbstractUIElementsFactory {
    func makeButton() -> IButton
    func makeCheckmark() -> ICheckmark
}Определим Windows фабрику.
class WindowsUIElementsFactory: AbstractUIElementsFactory {
    func makeButton() -> IButton {
        print("Windows button is creating ...")
        return WindowsButton()
    }
    
    func makeCheckmark() -> ICheckmark {
        print("Windows checkmark is creating ...")
        return WindowsCheckmark()
    }
}И iOS фабрику.
class IosUIElementsFactory: AbstractUIElementsFactory {
    func makeButton() -> IButton {
        print("IOS button is creating ...")
        return IosButton()
    }
    
    func makeCheckmark() -> ICheckmark {
        print("IOS checkmark is creating ...")
        return IosCheckmark()
    }
}В клиентском коде мы создаем нужные элементы, вызывая методы фабрики.
class  Client {
    static func createUIElements(factory: AbstractUIElementsFactory) {
        let button = factory.makeButton()
        let checkmark = factory.makeCheckmark()
        
        button.pressButton()
        checkmark.chooseCheckMark()
    }
}А тип элементов, которые мы получаем зависит от типа переданной фабрики.
Client.createUIElements(factory: WindowsUIElementsFactory())
print("")
Client.createUIElements(factory: IosUIElementsFactory())Строитель / Builder
Строитель — это порождающий паттерн проектирования, который позволяет создавать сложные объекты пошагово. Строитель даёт возможность использовать один и тот же код строительства для получения разных представлений объектов.
Как следует из определения строитель даёт возможность использовать один и тот же код строительства для получения разных представлений объектов.
Представьте что у нас есть сложный класс с множеством настроек или несколько взаимосвязанных классов, зависящий друг от друга. Настраивая их каждый раз в ручную легко ошибиться. Тут нам на помощь может прийти паттерн строитель.
Предположим у нас есть класс автомобиль.
final class Car {
    var seats = 0
    var engine = ""
    var tripComputer = ""
    var gps = false
    
    init() {
        print("Car is creating ...")
    }
    
    func printDescription() {
        print("seats: \(seats)\nengine: \(engine)\ntripComputer: \(tripComputer)\ngps: \(gps)\n")
    }
}Так как автомобиль технически сложное изделие, нам нужна инструкция к нему, описывающая все характеристики и свойства автомобиля.
final class Manual {
    var seats = ""
    var engine = ""
    var tripComputer = ""
    var gps = ""
    
    init() {
        print("Manual is creating ...")
    }
    
    func printDescription() {
        print("seats: \(seats)\nengine: \(engine)\ntripComputer: \(tripComputer)\ngps: \(gps)\n")
    }
}Для настройки классов автомобиль и инструкция мы можем воспользоваться дополнительным классом строителя, который знает как их настроить, не пропустив какие то необходимые важные действия.
Определим протокол строителя.
protocol Builder {
    func reset()
    func setSeats(_ : Int)
    func setEngine(_ : String)
    func setTripComputer(_ : String)
    func setGPS(_ : Bool)
}Определим конкретный класс строителя автомобиля.
final class CarBuilder: Builder {
    private var car = Car()
    
    // MARK: - Protocol methods
    func reset() {
        car = Car()
    }
    
    func setSeats(_ seats: Int) {
        car.seats = seats
    }
    
    func setEngine(_ engine: String) {
        car.engine = engine
    }
    
    func setTripComputer(_ computer: String) {
        car.tripComputer = computer
    }
    
    func setGPS(_ isSet: Bool) {
        car.gps = isSet ? true : false
    }
    
    // MARK: - getResult method
    func getResult() -> Car {
        return car
    }
}А так же класс строителя инструкции.
final class ManualBuilder: Builder {
    private var manual = Manual()
    
    // MARK: - Protocol methods
    func reset() {
        manual = Manual()
    }
    
    func setSeats(_ seats: Int) {
        let ending = seats == 1 ? "seat" : "seats"
        manual.seats = "Car has \(seats) \(ending)"
    }
    
    func setEngine(_ engine: String) {
        manual.engine = "Car has \(engine) engine"
    }
    
    func setTripComputer(_ computer: String) {
        manual.tripComputer = "Car has \(computer) computer"
    }
    
    func setGPS(_ isSet: Bool) {
        manual.gps = isSet ? "GPS is set" : "There is no GPS"
    }
    
    // MARK: - getResult method
    func getResult() -> Manual {
        return manual
    }
}Так же мы можем создать не обязательный класс директора и предать ему управление строителями.
final class Director {
    func constructSportsCar(builder: Builder) {
        builder.reset()
        builder.setSeats(2)
        builder.setEngine("Honda")
        builder.setTripComputer("Apple")
        builder.setGPS(true)
    }
}Директор вызывает все необходимые методы и задает необходимые значения свойств. Класс директор в паттерне строитель не обязателен, мы можем напрямую вызывать методы строителя в нашем коде. Но директор структурирует вызовы строители и тем самым может быть нам полезен.
Далее мы можем создать автомобиль и инструкцию к нему воспользовавшись соответствующими строителями.
// Создаем директора и строителей
let director = Director()
let carBuilder = CarBuilder()
let manualBuilder = ManualBuilder()
// Передаем в метод директора стрителя авто
director.constructSportsCar(builder: carBuilder)
let car = carBuilder.getResult()
car.printDescription()
// Передаем в метод директора стрителя инструкции
director.constructSportsCar(builder: manualBuilder)
let manual = manualBuilder.getResult()
manual.printDescription()Важно, что результат строительства мы получаем не от директора, а от строителя так как директор чаще всего не знает и не зависит от конкретных классов строителей и продуктов.
let car = carBuilder.getResult()Прототип / Prototype
Прототип — это порождающий паттерн проектирования, который позволяет копировать объекты, не вдаваясь в подробности их реализации.
Представим, что у нас есть объект, который нужно скопировать. Проблема в том, что у объекта могут быть приватные поля, доступа к которым из вызывающего кода у нас нет.
Как быть в таком случае? Давайте передадим обязанность копировать себя самому объекту, ведь он про себя знает все и имеет доступ ко всем своим свойствам, включая приватные.
В Swift у нас есть встроенная поддержка копирования. Чтобы сделать класс, который может копировать сам себя, нам нужно реализовать в нём протокол NSCopying, а именно методcopy.
Продолжим автомобильную тему. Создадим класс автомобиль.
class Car: NSCopying, Equatable {
    var model: String
    var color: String
    var numberOfSeats: Int
    
    required init(model: String = "", color: String = "", numberOfSeats: Int = 2) {
        self.model = model
        self.color = color
        self.numberOfSeats = numberOfSeats
    }
    // Реализуем возможность копирования
    // MARK: - NSCopying
    func copy(with zone: NSZone? = nil) -> Any {
        let protorype = type(of: self).init()
        
        protorype.model = model
        protorype.color = color
        protorype.numberOfSeats = numberOfSeats
        
        return protorype
    }
    // Дополнительно реализуем возможность сравнения, чтобы иметь возможность            сравнить скопированный авто с оригиналом.
    // MARK: - Equatable
    static func == (lhs: Car, rhs: Car) -> Bool {
        return lhs.model == rhs.model &&
        lhs.color == rhs.color &&
        lhs.numberOfSeats == rhs.numberOfSeats
    }
}В вызывающем коде создаем машину, клонившем ее и сравниваем два экземпляра.
let teslaCar = Car(model: "Tesla", color: "Red", numberOfSeats: 2)
teslaCar.engine = "TeslaElectric"
let teslaCarCopy = teslaCar.copy() as? Car
print(teslaCar == teslaCar)Одиночка / Singleton
Иногда нам нужно, чтобы в приложении был только один экземпляр како-то класса. Это может быть, например, хранилище данных или API менеджер.
Мы можем реализовать данное поведение с помощью паттерну одиночка.
class Singleton {
    // Статическое поле, управляющие доступом к экземпляру одиночки.
    // Эта реализация позволяет сохранять только один экземпляр класса.
    static var shared: Singleton = {
        let instance = Singleton()
        // ... настройка объекта
        // ...
        return instance
    }()
    // Инициализатор Одиночки всегда должен быть скрытым, чтобы предотвратить
    // прямое создание объекта через инициализатор.
    private init() {}
    // Любой одиночка должен содержать некоторую бизнес-логику,
    // которая может быть выполнена на его экземпляре.
    func someBusinessLogic() -> String {
        // ...
        return "Result of the 'someBusinessLogic' call"
    }
}Одиночка не должен быть копируемым. Для этого реализуем метод copy(with zone:) так, чтобы одиночка возвращал самого себя.
extension Singleton: NSCopying {
    func copy(with zone: NSZone? = nil) -> Any {
        return self
    }
}В клиентском коде мы можем убедиться, что экземпляр класса одиночки у на один.
let instance1 = Singleton.shared
let instance2 = Singleton.shared
if (instance1 === instance2) {
    print("Singleton works, both variables contain the same instance.")
} else {
    print("Singleton failed, variables contain different instances.")
}На этом про порождающие паттерны все. Надеюсь, что данная статья поможет начинающим разработчикам разобраться в такой не самой простой теме как паттерну проектирования
 
           
 
FreeNickname
Спасибо за статью.
Я бы только, особенно учитывая, что статья для начинающих, упомянул, что для платформеннозависимого кода лучше использовать не фабрику, а что-нибудь полходящее из if-директив. Полагаю, или #if os, или if #available. Но не знаю точно, как будет выглядить запись для Windows, не пробовал собирать Swift под Windows.
Ну и вместо NSCopying лучше, полагаю, сразу использовать Copyable. К тому же, зачем синглтону определять протокол для копирования, если его можно просто не объявлять? Даже если его объявят позже в extension-е, то всё равно не смогут создать другой экземпляр, т.к. доступа к конструктору нет.
А, ну и у синглтона лучше всё-таки let shared, а не var shared, полагаю :) Конечно, ничего другого присвоить не смогут (хотя я вот не помню: у нас класс не финальный, получится ли создать наследника и присвоить его? Может и получится), но просто зачем? К то у же, в режиме Swift 6 Xcode будет орать про shared state :)