Шаблоны в iOS

Сегодня мы будем закреплять/вспоминать/учить шаблоны в iOS. Некоторые будем рассматривать детально, некоторых слегка коснемся, про другие совсем ничего не скажем :)

Что такое шаблоны? Это многократно используемые решение часто встречающихся проблем при разработке. Они помогают писать код, который можно легко понять и использовать.

Requirements: XCode 8.2.1, knowledge of Swift 3, time and brain :)

Разделим все паттерны на категории: Creational-, Structural- и Behavioral-паттерны.

Singleton (Creational category)

Представьте ситуацию, когда вам нужно иметь только одну копию объекта. Например, это может быть реальный объект: принтер, сервер или что-то, чего не должно быть несколько копий. У Apple, например, есть объект UserDefaults, доступ к нему осуществляется через свойство standard. Пример реализации:

private override init(){}static let sharedInstance = EventManager()
    override func copy()-> Any {
        fatalError("You are not allowed to use copy method on singleton!")}
    override func mutableCopy()-> Any {
        fatalError("You are not allowed to use copy method on singleton!")}

Для класса EventManager я сделал инициализатор private, чтобы никто не мог создать новую копию объекта. Добавил статическую переменную sharedInstance и инициализировал ее объектом EventManager(). Далее, чтобы избежать копирования объекта, я переписал методы copy() и mutableCopy(). В прежних версиях swift можно было писать dispatch_once{}, но в swift 3.0 эта функция более недоступна.

Factory Method (Creational category)

Используется, когда необходимо сделать выбор между классами, которые реализуют общий протокол или разделяют общий базовый класс. Шаблон содержит в себе логику, которая решает, какой класс выбрать.

Вся логика, как предполагает название шаблона, находится в методе, который инкапсулирует решение.

У нас есть два варианта реализации: глобальный метод или использование базового класса.

Глобальный метод:

func createRentalCar(_ passengers:Int)-> RentalCar? {
        var carImp: RentalCar.Type?
        switch passengers {case0...3: carImp = Compact.self
        case4...8: carImp = SUV.self
        default: carImp =nil}return carImp?.createRentalCar(passengers)}

Использование базового класса предполагает перенос этой логики в базовый класс.

Abstract Factory (Creational category)

Этот шаблон очень похож на описанный выше, кроме того, что используется для создания группы объектов. Шаблон не знает о том, какая реализация будет использована, но он знает, как выбрать подходящий конкретный объект.

//heart of pattern
    final class func getFactory(car: Cars)-> CarFactory? {
        var factory: CarFactory?
        switch car {case .compact: factory = CompactCarFactory()case .sports: factory = SportsCarFactory()case .SUV: factory = SUVCarFactory()}return factory
    }

Этот метод находится в базовом абстрактном классе. Далее в структуре Car мы можем его использовать:

struct Car {
    var carType: Cars
    var floor: FloorPlan
    var suspension: Suspension
    var drive: Drivetrain
 
    init(carType: Cars){
        let concreteFactory = CarFactory.getFactory(car: carType)
        self.floor= concreteFactory!.createFloorplan()
        self.suspension = concreteFactory!.createSuspension()
        self.drive = concreteFactory!.createDrivetrain()
        self.carType = carType
    }
 
    func printDetails(){
        print("Car type: \(carType.rawValue)")
        print("Seats: \(floor.seats)")
        print("Engine: \(floor.enginePosition.rawValue)")
        print("Suspension: \(suspension.suspensionType.rawValue)")
        print("Drive: \(drive.driveType.rawValue)")}}

Builder (Creational category)

Этот шаблон используется для отделения конфигурации от создания объекта. Вызывающий объект содержит данные для конфигурации и передает их Builder-объекту, который отвечает за создание объекта. Предположим, что у нас есть ресторан, и нужно создать приложение для заказа бургеров:

let builder = BurgerBuilder()
 
//1 stage
let name ="Joe"
 
//2 stage
builder.setVeggie(choice:false)
 
//3 stage
builder.setMayo(choice:false)
builder.setCooked(choice: .welldone)
 
//4 stage
builder.addPatty(choice:true)
 
let order = builder.buildObject(name: name)
order.printDescription()

Для того, чтобы не опрашивать посетителя о каждом компоненте, который он хочет в свой бургер, создаем дефолтные значения в Builder-классе.

…
 private var veggie =false
    private var pickles =false
    private var mayo =true
    private var ketchup =true
    private var lettuce =true
    private var cooked = Burger.Cooked.normal
    private var patties =2
    private var bacon =true

Если в будущем, проведя опрос, окажется, что клиенты хотят все-таки добавлять майонез, то в Builder-классе меняем дефолтное значение на true, а время опроса клиентов при этом остается прежним :)

MVC (Structural category)

Тут все просто: model работает только с данными (моделью); view работает со всем, что касается прорисовкой элементов интерфейса и анимацией разных кнопочек (хотя для анимации лучше использовать отдельный класс: вдруг захотите что-то потом переделать); controller «собирает» все это вместе.

Со временем, конечно, контроллер «раздуется» от логики, связанной с моделью, и вам придется перейти на более продвинутый паттерн MVVM (model — view — viewModel). Более подробно о шаблоне MVVM я рассказывал тут.

Facade (Structural category)

Этот шаблон предлагает один интерфейс (упрощенный) для сложных систем. Вместо того, чтобы показывать пользователю целую кучу методов с разными интерфейсами, мы создаем свой класс, инкапсулируя в нем другие объекты и показываем более упрощенный интерфейс для пользователя.

Полезно в том случае, когда вам приходится заменять, например, Alamofire на NSURLSession. Вы делаете изменение только в вашем Facade-классе, не трогая его интерфейс.

Пример интерфейса — мой класс SocketManager:

final internal class SocketManager :NSObject{
 
    static internal let sharedInstance: SocketManager.SocketManager
 
    override internal func copy()-> Any
 
    override internal func mutableCopy()-> Any
 
    internal var connectionWasOpened: Bool
 
    internal var lastUserId: String
 
    internal func openNewConnection(userId: String)
 
    internal func closeConnection()
 
    internal func logoutFromWebSocket()
 
    internal func reconnect()}

Конечный пользователь может не знать, что я использую SocketRocket. А через какое-то время я могу заменить его на что-то другое, и мне не надо будет вносить изменения во всех местах, где он был использован: достаточно будет поправить один класс.

Decorator (Structural category)

Добавляет поведение и обязанности к объекту, не модифицируя его код, например, когда используем 3d party libraries и не имеем доступа к исходному коду.

В swift есть две очень распространенные реализации этого шаблона: extensions и delegation.

Adapter (Structural category)

Адаптер позволяет классам с несовместимыми интерфейсами работать вместе. Apple реализует этот шаблон с помощью protocols. Адаптер используется, когда нужно интегрировать компонент, код которого нельзя менять. Возможно, это старый legacy product (мы не знаем, как это работает, и боимся трогать).

Bridge (Structural category)

Этот шаблон очень похож на Адаптер, но с несколькими отличиями. Мы можем менять исходный код (у нас к нему есть доступ). Шаблон отделяет абстракцию от реализации, так что они могут быть изменены без соответствующий изменений в другом классе. Пример:

protocol Switch {
    var appliance: Appliance {get set}
    func turnOn()}
 
protocol Appliance {
    func run()}
 
class RemoteControl: Switch {
    var appliance: Appliance
 
    func turnOn(){
        self.appliance.run()}
 
    init(appliance: Appliance){
        self.appliance = appliance
    }}
 
class TV: Appliance {
    func run(){
        print("tv turned on");
    }}
 
class VacuumCleaner: Appliance {
    func run(){
        print("vacuum cleaner turned on")}}
 
//===main.swift====
 
var vacCleaner = VacuumCleaner()
var control = RemoteControl(appliance: vacCleaner)
 
control.turnOn()

Теперь можно менять методы run(), не внося правок в main файл.

Observer (Behavioral category)

Один объект оповещает другие об изменении своего состояния. Обычно один объект «подписывается» на изменения другого. Push Notifications — глобальный пример. Более локальный: Notifications, Key-Value Observing (KVO).

Momento (Behavioral category)

Сохраняет ваши объекты. Это и UserDefaults, и Archiving с помощью NSCoding protocol, и, в принципе, тот же CoreData.

Command (Behavioral category)

Когда мы связываем метод с действием (action touch) для какого-то элемента интерфейса (кнопки, например) — это и есть Command-шаблон.

Итоги

Советую почитать следующие материалы:

https://www.raywenderlich.com/46988/ios-design-patterns

https://github.com/ochococo/Design-Patterns-In-Swift

И книгу:

Pro Design Patterns in Swift by Adam Freeman.