Всем привет! Зачастую чтобы в чем то разобраться полезнее один раз увидеть конкретный пример чем несколько раз прочитать заумное описание.Решил написать ряд небольших статей для начинающих, в которых дать краткое описание основных паттернов проектирования и привести лаконичные примеры их использования.Данная статья, как можно догадаться из названия =), посвящена порождающим паттернам.

Фабричный метод /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.")
}

На этом про порождающие паттерны все. Надеюсь, что данная статья поможет начинающим разработчикам разобраться в такой не самой простой теме как паттерну проектирования

Комментарии (1)


  1. FreeNickname
    08.06.2025 03:55

    Спасибо за статью.

    Я бы только, особенно учитывая, что статья для начинающих, упомянул, что для платформеннозависимого кода лучше использовать не фабрику, а что-нибудь полходящее из if-директив. Полагаю, или #if os, или if #available. Но не знаю точно, как будет выглядить запись для Windows, не пробовал собирать Swift под Windows.

    Ну и вместо NSCopying лучше, полагаю, сразу использовать Copyable. К тому же, зачем синглтону определять протокол для копирования, если его можно просто не объявлять? Даже если его объявят позже в extension-е, то всё равно не смогут создать другой экземпляр, т.к. доступа к конструктору нет.

    А, ну и у синглтона лучше всё-таки let shared, а не var shared, полагаю :) Конечно, ничего другого присвоить не смогут (хотя я вот не помню: у нас класс не финальный, получится ли создать наследника и присвоить его? Может и получится), но просто зачем? К то у же, в режиме Swift 6 Xcode будет орать про shared state :)