Рубріки: Подборки

Топ-5 шаблонов проектирования в Swift для разработки приложений на iOS

Роман Гармидер

Swift — выпущенный в 2014 году собственный язык программирования Apple — мощный инструмент, который позволяет разработчикам создавать различные приложения для нескольких операционных систем (хотя чаще всего, конечно, для iOS).

Текст предоставлен компанией Ruby Garage.

Swift — относительно новый язык программирования, и многие разработчики не знают, какие шаблоны проектирования использовать и как их применять. Без умения использовать релевантный шаблон проектирования сложно создавать функциональные, качественные и безопасные приложения.

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

Типы шаблонов проектирования ПО

Прежде чем перейти к самым распространенным шаблонам в Swift, расскажем о трех общих типах шаблонов проектирования программного обеспечения и о том, чем они отличаются:

  • Порождающие (Creational) шаблоны дизайна ПО работают с механизмами создания объектов.
  • Структурные (Structural) — направлены на упрощение проекта путем поиска легкого способа реализации взаимосвязей между классами и объектами.
  • Поведенческие (Behavioral) — определяют общие модели взаимодействия между объектами и реализуют эти модели

Общие шаблоны в свою очередь делятся на множество частных шаблонов проектирования, реализующих эти паттерны. Правда большинство из них используются редко, поэтому мы выбрали пять шаблонов проектирования, которые чаще всего используются в Swift для разработки на iOS и других операционных системах.

Часто используемые шаблоны проектирования в Swift

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

# 1 “Строитель” (Builder)

“Строитель” — это порождающий шаблон проектирования, который позволяет создавать сложные объекты из простых поэтапно. Этот шаблон помогает использовать один и тот же код для создания различных отражений объектов.

Представьте себе сложный объект, который требует постепенной инициализации нескольких полей и вложенных объектов. Как правило, код инициализации для таких объектов скрыт внутри монстрообразного конструктора с десятками параметров. Или еще хуже — он может быть рассеян по всему коду клиента.

Шаблон “Строитель” требует отделения конструкции объекта от его собственного класса. Зато построение этого объекта поручается специальным объектам, которые называются “строителями”, и разделено на несколько этапов. Для создания объекта вы последовательно вызываете методы “Строителя”, при этом вам не нужно проходить все этапы, а только те, которые необходимы для создания объекта с определенной конфигурацией.

Шаблон проектирования “Строитель” следует применять,

  • если вы хотите избежать использования “телескопического конструктора” (когда конструктор имеет слишком много параметров, становится трудно его читать и управлять им);
  • когда код должен создавать различные представления какого-то определенного объекта;
  • когда нужно собирать сложные объекты.

Пример

Допустим, вы разрабатываете 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

 

# 2 “Адаптер” (Adapter)

“Адаптер” — структурный шаблон проектирования, который позволяет объектам с несовместимыми интерфейсами работать вместе. Иными словами, он меняет интерфейс объекта, чтобы адаптировать его к другому объекту.

“Адаптер” “заворачивает” объект так, что почти полностью скрывает его от другого объекта. Например, объект, который работает в метрической системе измерения, можно “обернуть” адаптером, который преобразует данные в футы.

Шаблон проектирования “Адаптер” следует применять,

  • когда вы хотите использовать сторонний класс, но его интерфейс не соответствует остальному коду вашей программы;
  • когда вам нужно использовать несколько существующих подклассов, но они не имеют какой-то особой функциональности, и к тому же вы не можете расширить суперкласс.

Пример

Допустим, вы хотите применить функцию календаря и управления событиями в своем 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

 

# 3“Декоратор” (Decorator)

Декоратор — структурный шаблон проектирования, который позволяет динамически добавлять объектам новую функциональность, оборачивая их в полезные “обертки”.

Недаром этот шаблон называют также “Обертка” (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

 

# 4 “Фасад” (Facade)

Фасад — структурный шаблон проектирования, который обеспечивает простой интерфейс для библиотеки, фреймворка или сложной системы классов.

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

Шаблон “Фасад” обеспечивает простой интерфейс для работы со сложными подсистемами, содержащими множество классов. Он предлагает упрощенный интерфейс с ограниченными функциональными возможностями, который можно расширить, используя сложную подсистему направления. Этот упрощенный интерфейс предоставляет только те функции, которые нужны клиенту, скрывая все остальные.

Шаблон проектирования “Фасад” следует применять, 

  • когда вы хотите предоставить простой или унифицированный интерфейс сложной подсистеме;
  • когда нужно разложить подсистему на отдельные слои.

Пример

Многие современные мобильные приложения поддерживают запись и воспроизведение звука. Допустим, вам нужно применить эту функцию. Вы можете использовать шаблон “Фасад”, чтобы скрыть реализацию служб, ответственных за файловую систему (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

 

# 5 Шаблонный метод

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

Этот шаблон дизайна разбивает алгоритм на последовательность шагов, описывает эти шаги отдельными методами и вызывает их последовательно с помощью одного шаблона.

Шаблонный метод следует применять, 

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

Пример

Допустим, вы работаете над 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 позволяет создавать функциональные и безопасные приложения, которые легко поддерживать и модернизировать. В арсенале любого разработчика должно быть знание и навыки работы с шаблонами проектирования, поскольку они не только упрощают и оптимизируют процесс разработки, но и обеспечивают высокое качество кода.

Останні статті

Обучение Power BI – какие онлайн курсы аналитики выбрать

Сегодня мы поговорим о том, как выбрать лучшие курсы Power BI в Украине, особенно для…

13.01.2024

Work.ua назвал самые конкурентные вакансии в IТ за 2023 год

В 2023 году во всех крупнейших регионах конкуренция за вакансию выросла на 5–12%. Не исключением…

08.12.2023

Украинская IT-рекрутерка создала бесплатный трекер поиска работы

Unicorn Hunter/Talent Manager Лина Калиш создала бесплатный трекер поиска работы в Notion, систематизирующий все этапы…

07.12.2023

Mate academy отправит работников в 10-дневный оплачиваемый отпуск

Edtech-стартап Mate academy принял решение отправить своих работников в десятидневный отпуск – с 25 декабря…

07.12.2023

Переписки, фото, история браузера: киевский программист зарабатывал на шпионаже

Служба безопасности Украины задержала в Киеве 46-летнего программиста, который за деньги устанавливал шпионские программы и…

07.12.2023

Как вырасти до сеньйора? Девелопер создал популярную подборку на Github

IT-специалист Джордан Катлер создал и выложил на Github подборку разнообразных ресурсов, которые помогут достичь уровня…

07.12.2023