Порождающие паттерны проектирования в примерах на Swift для самых маленьких

от автора

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

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

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


ссылка на оригинал статьи https://habr.com/ru/articles/916564/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *