
Всем привет! Зачастую чтобы в чем то разобраться полезнее один раз увидеть конкретный пример чем несколько раз прочитать заумное описание.Решил написать ряд небольших статей для начинающих, в которых дать краткое описание основных паттернов проектирования и привести лаконичные примеры их использования.Данная статья, как можно догадаться из названия =), посвящена порождающим паттернам.
Фабричный метод /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 :)