Шаблоны в iOS

Шаблоны в 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 {
        case 0...3: carImp = Compact.self
        case 4...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.