Топ-5 шаблонов проектирования в Swift для разработки приложений на iOS
Swift — выпущенный в 2014 году собственный язык программирования Apple — мощный инструмент, который позволяет разработчикам создавать различные приложения для нескольких операционных систем (хотя чаще всего, конечно, для iOS).
Текст предоставлен компанией Ruby Garage.
Swift — относительно новый язык программирования, и многие разработчики не знают, какие шаблоны проектирования использовать и как их применять. Без умения использовать релевантный шаблон проектирования сложно создавать функциональные, качественные и безопасные приложения.
Мы решили проанализировать шаблоны проектирования, которые чаще всего используются в Swift, и продемонстрировать различные подходы к их применению при решении типичных проблем мобильной разработки.
Прежде чем перейти к самым распространенным шаблонам в Swift, расскажем о трех общих типах шаблонов проектирования программного обеспечения и о том, чем они отличаются:
Общие шаблоны в свою очередь делятся на множество частных шаблонов проектирования, реализующих эти паттерны. Правда большинство из них используются редко, поэтому мы выбрали пять шаблонов проектирования, которые чаще всего используются в Swift для разработки на iOS и других операционных системах.
Мы дадим только самую важную информацию о каждом шаблоне, а именно, как они работают с технической точки зрения и когда их следует применять. А еще приведем наглядные примеры для языка Swift.
“Строитель” — это порождающий шаблон проектирования, который позволяет создавать сложные объекты из простых поэтапно. Этот шаблон помогает использовать один и тот же код для создания различных отражений объектов.
Представьте себе сложный объект, который требует постепенной инициализации нескольких полей и вложенных объектов. Как правило, код инициализации для таких объектов скрыт внутри монстрообразного конструктора с десятками параметров. Или еще хуже — он может быть рассеян по всему коду клиента.
Шаблон “Строитель” требует отделения конструкции объекта от его собственного класса. Зато построение этого объекта поручается специальным объектам, которые называются “строителями”, и разделено на несколько этапов. Для создания объекта вы последовательно вызываете методы “Строителя”, при этом вам не нужно проходить все этапы, а только те, которые необходимы для создания объекта с определенной конфигурацией.
Шаблон проектирования “Строитель” следует применять,
Допустим, вы разрабатываете iOS-приложение для ресторана, и вам нужно применить функцию заказа. Вы можете представить две структуры, Dish и Order, а с помощью объекта OrderBuilder можно составлять заказ с различными наборами блюд.
// Design Patterns: Builder import Foundation // Models enum DishCategory: Int { case firstCourses, mainCourses, garnishes, drinks } struct Dish { var name: String var price: Float } struct OrderItem { var dish: Dish var count: Int } struct Order { var firstCourses: [OrderItem] = [] var mainCourses: [OrderItem] = [] var garnishes: [OrderItem] = [] var drinks: [OrderItem] = [] var price: Float { let items = firstCourses + mainCourses + garnishes + drinks return items.reduce(Float(0), { $0 + $1.dish.price * Float($1.count) }) } } // Builder class OrderBuilder { private var order: Order? func reset() { order = Order() } func setFirstCourse(_ dish: Dish) { set(dish, at: order?.firstCourses, withCategory: .firstCourses) } func setMainCourse(_ dish: Dish) { set(dish, at: order?.mainCourses, withCategory: .mainCourses) } func setGarnish(_ dish: Dish) { set(dish, at: order?.garnishes, withCategory: .garnishes) } func setDrink(_ dish: Dish) { set(dish, at: order?.drinks, withCategory: .drinks) } func getResult() -> Order? { return order ?? nil } private func set(_ dish: Dish, at orderCategory: [OrderItem]?, withCategory dishCategory: DishCategory) { guard let orderCategory = orderCategory else { return } var item: OrderItem! = orderCategory.filter( { $0.dish.name == dish.name } ).first guard item == nil else { item.count += 1 return } item = OrderItem(dish: dish, count: 1) switch dishCategory { case .firstCourses: order?.firstCourses.append(item) case .mainCourses: order?.mainCourses.append(item) case .garnishes: order?.garnishes.append(item) case .drinks: order?.drinks.append(item) } } } // Usage let steak = Dish(name: "Steak", price: 2.30) let chips = Dish(name: "Chips", price: 1.20) let coffee = Dish(name: "Coffee", price: 0.80) let builder = OrderBuilder() builder.reset() builder.setMainCourse(steak) builder.setGarnish(chips) builder.setDrink(coffee) let order = builder.getResult() order?.price // Result: // 4.30
“Адаптер” — структурный шаблон проектирования, который позволяет объектам с несовместимыми интерфейсами работать вместе. Иными словами, он меняет интерфейс объекта, чтобы адаптировать его к другому объекту.
“Адаптер” “заворачивает” объект так, что почти полностью скрывает его от другого объекта. Например, объект, который работает в метрической системе измерения, можно “обернуть” адаптером, который преобразует данные в футы.
Шаблон проектирования “Адаптер” следует применять,
Допустим, вы хотите применить функцию календаря и управления событиями в своем iOS-приложении. Для этого следует интегрировать фреймворк EventKit и адаптировать модель Event из фреймворка к модели в вашем приложении. “Адаптер” может “охватить” модель фреймворка и сделать ее совместимой с вашей.
// Design Patterns: Adapter import EventKit // Models protocol Event: class { var title: String { get } var startDate: String { get } var endDate: String { get } } extension Event { var description: String { return "Name: \(title)\nEvent start: \(startDate)\nEvent end: \(endDate)" } } class LocalEvent: Event { var title: String var startDate: String var endDate: String init(title: String, startDate: String, endDate: String) { self.title = title self.startDate = startDate self.endDate = endDate } } // Adapter class EKEventAdapter: Event { private var event: EKEvent private lazy var dateFormatter: DateFormatter = { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "MM-dd-yyyy HH:mm" return dateFormatter }() var title: String { return event.title } var startDate: String { return dateFormatter.string(from: event.startDate) } var endDate: String { return dateFormatter.string(from: event.endDate) } init(event: EKEvent) { self.event = event } } // Usage let dateFormatter = DateFormatter() dateFormatter.dateFormat = "MM/dd/yyyy HH:mm" let eventStore = EKEventStore() let event = EKEvent(eventStore: eventStore) event.title = "Design Pattern Meetup" event.startDate = dateFormatter.date(from: "06/29/2018 18:00") event.endDate = dateFormatter.date(from: "06/29/2018 19:30") let adapter = EKEventAdapter(event: event) adapter.description // Result: // Name: Design Pattern Meetup // Event start: 06-29-2018 18:00 // Event end: 06-29-2018 19:30
Декоратор — структурный шаблон проектирования, который позволяет динамически добавлять объектам новую функциональность, оборачивая их в полезные “обертки”.
Недаром этот шаблон называют также “Обертка” (Wrapper). Это название более точно описывает его основную идею: вы размещаете целевой объект внутри другого объекта-обертки, который инициирует основное поведение целевого объекта и добавляет свой результат к конечному.
Оба объекта имеют один и тот же интерфейс, поэтому для пользователя не имеет значения, с каким объектом он взаимодействует — “чистым” или “обернутым”. Разработчик же может использовать несколько “оберток” одновременно и получить их комбинированное поведение.
Шаблон проектирования “Декоратор” следует применять,
Представьте, что вам нужно внедрить управление данными в своем iOS-приложении. Вы можете создать два “декоратора”: EncryptionDecorator для шифрования и дешифрования данных и EncodingDecorator для кодирования и декодирования.
// Design Patterns: Decorator import Foundation // Helpers (may be not include in blog post) func encryptString(_ string: String, with encryptionKey: String) -> String { let stringBytes = [UInt8](string.utf8) let keyBytes = [UInt8](encryptionKey.utf8) var encryptedBytes: [UInt8] = [] for stringByte in stringBytes.enumerated() { encryptedBytes.append(stringByte.element ^ keyBytes[stringByte.offset % encryptionKey.count]) } return String(bytes: encryptedBytes, encoding: .utf8)! } func decryptString(_ string: String, with encryptionKey: String) -> String { let stringBytes = [UInt8](string.utf8) let keyBytes = [UInt8](encryptionKey.utf8) var decryptedBytes: [UInt8] = [] for stringByte in stringBytes.enumerated() { decryptedBytes.append(stringByte.element ^ keyBytes[stringByte.offset % encryptionKey.count]) } return String(bytes: decryptedBytes, encoding: .utf8)! } // Services protocol DataSource: class { func writeData(_ data: Any) func readData() -> Any } class UserDefaultsDataSource: DataSource { private let userDefaultsKey: String init(userDefaultsKey: String) { self.userDefaultsKey = userDefaultsKey } func writeData(_ data: Any) { UserDefaults.standard.set(data, forKey: userDefaultsKey) } func readData() -> Any { return UserDefaults.standard.value(forKey: userDefaultsKey)! } } // Decorators class DataSourceDecorator: DataSource { let wrappee: DataSource init(wrappee: DataSource) { self.wrappee = wrappee } func writeData(_ data: Any) { wrappee.writeData(data) } func readData() -> Any { return wrappee.readData() } } class EncodingDecorator: DataSourceDecorator { private let encoding: String.Encoding init(wrappee: DataSource, encoding: String.Encoding) { self.encoding = encoding super.init(wrappee: wrappee) } override func writeData(_ data: Any) { let stringData = (data as! String).data(using: encoding)! wrappee.writeData(stringData) } override func readData() -> Any { let data = wrappee.readData() as! Data return String(data: data, encoding: encoding)! } } class EncryptionDecorator: DataSourceDecorator { private let encryptionKey: String init(wrappee: DataSource, encryptionKey: String) { self.encryptionKey = encryptionKey super.init(wrappee: wrappee) } override func writeData(_ data: Any) { let encryptedString = encryptString(data as! String, with: encryptionKey) wrappee.writeData(encryptedString) } override func readData() -> Any { let encryptedString = wrappee.readData() as! String return decryptString(encryptedString, with: encryptionKey) } } // Usage var source: DataSource = UserDefaultsDataSource(userDefaultsKey: "decorator") source = EncodingDecorator(wrappee: source, encoding: .utf8) source = EncryptionDecorator(wrappee: source, encryptionKey: "secret") source.writeData("Design Patterns") source.readData() as! String // Result: // Design Patterns
Фасад — структурный шаблон проектирования, который обеспечивает простой интерфейс для библиотеки, фреймворка или сложной системы классов.
Представьте, что ваш код должен иметь дело с несколькими объектами сложной библиотеки или фреймворка. Вам нужно инициализировать все эти объекты, отслеживать правильный порядок зависимостей и т.д. Как следствие, бизнес-логика одних ваших классов переплетается с деталями реализации других классов. Такой код трудно читать и поддерживать.
Шаблон “Фасад” обеспечивает простой интерфейс для работы со сложными подсистемами, содержащими множество классов. Он предлагает упрощенный интерфейс с ограниченными функциональными возможностями, который можно расширить, используя сложную подсистему направления. Этот упрощенный интерфейс предоставляет только те функции, которые нужны клиенту, скрывая все остальные.
Шаблон проектирования “Фасад” следует применять,
Многие современные мобильные приложения поддерживают запись и воспроизведение звука. Допустим, вам нужно применить эту функцию. Вы можете использовать шаблон “Фасад”, чтобы скрыть реализацию служб, ответственных за файловую систему (FileService), аудиосеансы (AudioSessionService), аудиозапись (RecorderService) и воспроизведение звука (PlayerService). “Фасад” обеспечивает упрощенный интерфейс для этой довольно сложной системы классов.
// Design Patterns: Facade import AVFoundation // Services (may be not include in blog post) struct FileService { private var documentDirectory: URL { return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! } var contentsOfDocumentDirectory: [URL] { return try! FileManager.default.contentsOfDirectory(at: documentDirectory, includingPropertiesForKeys: nil) } func path(withPathComponent component: String) -> URL { return documentDirectory.appendingPathComponent(component) } func removeItem(at index: Int) { let url = contentsOfDocumentDirectory[index] try! FileManager.default.removeItem(at: url) } } protocol AudioSessionServiceDelegate: class { func audioSessionService(_ audioSessionService: AudioSessionService, recordPermissionDidAllow allowed: Bool) } class AudioSessionService { weak var delegate: AudioSessionServiceDelegate? func setupSession() { try! AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayAndRecord, with: [.defaultToSpeaker]) try! AVAudioSession.sharedInstance().setActive(true) AVAudioSession.sharedInstance().requestRecordPermission { [weak self] allowed in DispatchQueue.main.async { guard let strongSelf = self, let delegate = strongSelf.delegate else { return } delegate.audioSessionService(strongSelf, recordPermissionDidAllow: allowed) } } } func deactivateSession() { try! AVAudioSession.sharedInstance().setActive(false) } } struct RecorderService { private var isRecording = false private var recorder: AVAudioRecorder! private var url: URL init(url: URL) { self.url = url } mutating func startRecord() { guard !isRecording else { return } isRecording = !isRecording recorder = try! AVAudioRecorder(url: url, settings: [AVFormatIDKey: kAudioFormatMPEG4AAC]) recorder.record() } mutating func stopRecord() { guard isRecording else { return } isRecording = !isRecording recorder.stop() } } protocol PlayerServiceDelegate: class { func playerService(_ playerService: PlayerService, playingDidFinish success: Bool) } class PlayerService: NSObject, AVAudioPlayerDelegate { private var player: AVAudioPlayer! private var url: URL weak var delegate: PlayerServiceDelegate? init(url: URL) { self.url = url } func startPlay() { player = try! AVAudioPlayer(contentsOf: url) player.delegate = self player.play() } func stopPlay() { player.stop() } func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { delegate?.playerService(self, playingDidFinish: flag) } } // Facade protocol AudioFacadeDelegate: class { func audioFacadePlayingDidFinish(_ audioFacade: AudioFacade) } class AudioFacade: PlayerServiceDelegate { private let audioSessionService = AudioSessionService() private let fileService = FileService() private let fileFormat = ".m4a" private var playerService: PlayerService! private var recorderService: RecorderService! weak var delegate: AudioFacadeDelegate? private lazy var dateFormatter: DateFormatter = { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd_HH:mm:ss" return dateFormatter }() init() { audioSessionService.setupSession() } deinit { audioSessionService.deactivateSession() } func startRecord() { let fileName = dateFormatter.string(from: Date()).appending(fileFormat) let url = fileService.path(withPathComponent: fileName) recorderService = RecorderService(url: url) recorderService.startRecord() } func stopRecord() { recorderService.stopRecord() } func numberOfRecords() -> Int { return fileService.contentsOfDocumentDirectory.count } func nameOfRecord(at index: Int) -> String { let url = fileService.contentsOfDocumentDirectory[index] return url.lastPathComponent } func removeRecord(at index: Int) { fileService.removeItem(at: index) } func playRecord(at index: Int) { let url = fileService.contentsOfDocumentDirectory[index] playerService = PlayerService(url: url) playerService.delegate = self playerService.startPlay() } func stopPlayRecord() { playerService.stopPlay() } func playerService(_ playerService: PlayerService, playingDidFinish success: Bool) { if success { delegate?.audioFacadePlayingDidFinish(self) } } } // Usage let audioFacade = AudioFacade() audioFacade.numberOfRecords() // Result: // 0
Шаблонный метод — это поведенческий шаблон проектирования, который определяет скелет алгоритма и делегирует ответственность за некоторые шаги подклассам. Этот шаблон позволяет подклассам переопределять отдельные шаги алгоритма, не меняя его общей структуры.
Этот шаблон дизайна разбивает алгоритм на последовательность шагов, описывает эти шаги отдельными методами и вызывает их последовательно с помощью одного шаблона.
Шаблонный метод следует применять,
Допустим, вы работаете над iOS-приложением, которое должно делать и хранить фотографии. Ваша программа должна получить разрешения на использование камеры iPhone и галереи изображений. Для этого вы можете использовать базовый класс PermissionService, который имеет определенный алгоритм. Чтобы получить разрешение на использование камеры и галереи, вы можете создать два подкласса, CameraPermissionService и PhotoPermissionService, которые переопределяют определенные шаги алгоритма, оставляя неизменными другие.
// Design Patterns: Template Method import AVFoundation import Photos // Services typealias AuthorizationCompletion = (status: Bool, message: String) class PermissionService: NSObject { private var message: String = "" func authorize(_ completion: @escaping (AuthorizationCompletion) -> Void) { let status = checkStatus() guard !status else { complete(with: status, completion) return } requestAuthorization { [weak self] status in self?.complete(with: status, completion) } } func checkStatus() -> Bool { return false } func requestAuthorization(_ completion: @escaping (Bool) -> Void) { completion(false) } func formMessage(with status: Bool) { let messagePrefix = status ? "You have access to " : "You haven't access to " let nameOfCurrentPermissionService = String(describing: type(of: self)) let nameOfBasePermissionService = String(describing: type(of: PermissionService.self)) let messageSuffix = nameOfCurrentPermissionService.components(separatedBy: nameOfBasePermissionService).first! message = messagePrefix + messageSuffix } private func complete(with status: Bool, _ completion: @escaping (AuthorizationCompletion) -> Void) { formMessage(with: status) let result = (status: status, message: message) completion(result) } } class CameraPermissionService: PermissionService { override func checkStatus() -> Bool { let status = AVCaptureDevice.authorizationStatus(for: .video).rawValue return status == AVAuthorizationStatus.authorized.rawValue } override func requestAuthorization(_ completion: @escaping (Bool) -> Void) { AVCaptureDevice.requestAccess(for: .video) { status in completion(status) } } } class PhotoPermissionService: PermissionService { override func checkStatus() -> Bool { let status = PHPhotoLibrary.authorizationStatus().rawValue return status == PHAuthorizationStatus.authorized.rawValue } override func requestAuthorization(_ completion: @escaping (Bool) -> Void) { PHPhotoLibrary.requestAuthorization { status in completion(status.rawValue == PHAuthorizationStatus.authorized.rawValue) } } } // Usage let permissionServices = [CameraPermissionService(), PhotoPermissionService()] for permissionService in permissionServices { permissionService.authorize { (_, message) in print(message) } } // Result: // You have access to Camera // You have access to Photo
Мы подробно рассмотрели пять шаблонов проектирования, которые чаще всего используются в Swift. В этом репозитории можно найти примеры того, как реализовать другие шаблоны архитектуры программного обеспечения на случай, если они вам нужны.
Возможность выбрать шаблон проектирования в Swift позволяет создавать функциональные и безопасные приложения, которые легко поддерживать и модернизировать. В арсенале любого разработчика должно быть знание и навыки работы с шаблонами проектирования, поскольку они не только упрощают и оптимизируют процесс разработки, но и обеспечивают высокое качество кода.
Прокси (proxy), или прокси-сервер — это программа-посредник, которая обеспечивает соединение между пользователем и интернет-ресурсом. Принцип…
Согласитесь, было бы неплохо соединить в одно сайт и приложение для смартфона. Если вы еще…
Повсеместное распространение смартфонов привело к огромному спросу на мобильные игры и приложения. Миллиарды пользователей гаджетов…
В перечне популярных чат-ботов с искусственным интеллектом Google Bard (Gemini) еще не пользуется такой популярностью…
Скрипт (англ. — сценарий), — это небольшая программа, как правило, для веб-интерфейса, выполняющая определенную задачу.…
Дедлайн (от англ. deadline — «крайний срок») — это конечная дата стачи проекта или задачи…