Сегодня мы будем закреплять/вспоминать/учить шаблоны в 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.