Команда на Swift
Команда — это поведенческий паттерн, позволяющий заворачивать запросы или простые операции в отдельные объекты.
Это позволяет откладывать выполнение команд, выстраивать их в очереди, а также хранить историю и делать отмену.
Сложность:
Популярность:
Применимость: Паттерн можно часто встретить в Swift-коде, особенно когда нужно откладывать выполнение команд, выстраивать их в очереди, а также хранить историю и делать отмену.
Признаки применения паттерна: Классы команд построены вокруг одного действия и имеют очень узкий контекст. Объекты команд часто подаются в обработчики событий элементов GUI. Практически любая реализация отмены использует принципа команд.
Концептуальный пример
Этот пример показывает структуру паттерна Команда, а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом.
После ознакомления со структурой, вам будет легче воспринимать второй пример, который рассматривает реальный случай использования паттерна в мире Swift.
Example.swift: Пример структуры паттерна
import XCTest /// Интерфейс Команды объявляет метод для выполнения команд. protocol Command { func execute() } /// Некоторые команды способны выполнять простые операции самостоятельно. class SimpleCommand: Command { private var payload: String init(_ payload: String) { self.payload = payload } func execute() { print("SimpleCommand: See, I can do simple things like printing (" + payload + ")") } } /// Но есть и команды, которые делегируют более сложные операции другим /// объектам, называемым «получателями». class ComplexCommand: Command { private var receiver: Receiver /// Данные о контексте, необходимые для запуска методов получателя. private var a: String private var b: String /// Сложные команды могут принимать один или несколько объектов-получателей /// вместе с любыми данными о контексте через конструктор. init(_ receiver: Receiver, _ a: String, _ b: String) { self.receiver = receiver self.a = a self.b = b } /// Команды могут делегировать выполнение любым методам получателя. func execute() { print("ComplexCommand: Complex stuff should be done by a receiver object.\n") receiver.doSomething(a) receiver.doSomethingElse(b) } } /// Классы Получателей содержат некую важную бизнес-логику. Они умеют выполнять /// все виды операций, связанных с выполнением запроса. Фактически, любой класс /// может выступать Получателем. class Receiver { func doSomething(_ a: String) { print("Receiver: Working on (" + a + ")\n") } func doSomethingElse(_ b: String) { print("Receiver: Also working on (" + b + ")\n") } } /// Отпрвитель связан с одной или несколькими командами. Он отправляет запрос /// команде. class Invoker { private var onStart: Command? private var onFinish: Command? /// Инициализация команд. func setOnStart(_ command: Command) { onStart = command } func setOnFinish(_ command: Command) { onFinish = command } /// Отправитель не зависит от классов конкретных команд и получателей. /// Отправитель передаёт запрос получателю косвенно, выполняя команду. func doSomethingImportant() { print("Invoker: Does anybody want something done before I begin?") onStart?.execute() print("Invoker: ...doing something really important...") print("Invoker: Does anybody want something done after I finish?") onFinish?.execute() } } /// Давайте посмотрим как всё это будет работать. class CommandConceptual: XCTestCase { func test() { /// Клиентский код может параметризовать отправителя любыми командами. let invoker = Invoker() invoker.setOnStart(SimpleCommand("Say Hi!")) let receiver = Receiver() invoker.setOnFinish(ComplexCommand(receiver, "Send email", "Save report")) invoker.doSomethingImportant() } } Output.txt: Результат выполнения
Invoker: Does anybody want something done before I begin? SimpleCommand: See, I can do simple things like printing (Say Hi!) Invoker: ...doing something really important... Invoker: Does anybody want something done after I finish? ComplexCommand: Complex stuff should be done by a receiver object. Receiver: Working on (Send email) Receiver: Also working on (Save report) Пример из реальной жизни
Example.swift: Пример из реальной жизни
import Foundation import XCTest class DelayedOperation: Operation, @unchecked Sendable { private var delay: TimeInterval init(_ delay: TimeInterval = 0) { self.delay = delay } override var isExecuting : Bool { get { return _executing } set { willChangeValue(forKey: "isExecuting") _executing = newValue didChangeValue(forKey: "isExecuting") } } private var _executing : Bool = false override var isFinished : Bool { get { return _finished } set { willChangeValue(forKey: "isFinished") _finished = newValue didChangeValue(forKey: "isFinished") } } private var _finished : Bool = false override func start() { guard delay > 0 else { _start() return } let deadline = DispatchTime.now() + delay DispatchQueue(label: "").asyncAfter(deadline: deadline) { self._start() } } private func _start() { guard !self.isCancelled else { print("\(self): operation is canceled") self.isFinished = true return } self.isExecuting = true self.main() self.isExecuting = false self.isFinished = true } } class WindowOperation: DelayedOperation, @unchecked Sendable { override func main() { print("\(self): Windows are closed via HomeKit.") } override var description: String { return "WindowOperation" } } class DoorOperation: DelayedOperation, @unchecked Sendable { override func main() { print("\(self): Doors are closed via HomeKit.") } override var description: String { return "DoorOperation" } } class TaxiOperation: DelayedOperation, @unchecked Sendable { override func main() { print("\(self): Taxi is ordered via Uber") } override var description: String { return "TaxiOperation" } } class CommandRealWorld: XCTestCase { func testCommandRealWorld() { prepareTestEnvironment { let siri = SiriShortcuts.shared print("User: Hey Siri, I am leaving my home") siri.perform(.leaveHome) print("User: Hey Siri, I am leaving my work in 3 minutes") siri.perform(.leaveWork, delay: 3) /// for simplicity, we use seconds print("User: Hey Siri, I am still working") siri.cancel(.leaveWork) } } } extension CommandRealWorld { struct ExecutionTime { static let max: TimeInterval = 5 static let waiting: TimeInterval = 4 } func prepareTestEnvironment(_ execution: () -> ()) { /// This method tells Xcode to wait for async operations. Otherwise the /// main test is done immediately. let expectation = self.expectation(description: "Expectation for async operations") let deadline = DispatchTime.now() + ExecutionTime.waiting DispatchQueue.main.asyncAfter(deadline: deadline) { expectation.fulfill() } execution() wait(for: [expectation], timeout: ExecutionTime.max) } } class SiriShortcuts { static let shared = SiriShortcuts() private lazy var queue = OperationQueue() private init() {} enum Action: String { case leaveHome case leaveWork } func perform(_ action: Action, delay: TimeInterval = 0) { print("Siri: performing \(action)-action\n") switch action { case .leaveHome: add(operation: WindowOperation(delay)) add(operation: DoorOperation(delay)) case .leaveWork: add(operation: TaxiOperation(delay)) } } func cancel(_ action: Action) { print("Siri: canceling \(action)-action\n") switch action { case .leaveHome: cancelOperation(with: WindowOperation.self) cancelOperation(with: DoorOperation.self) case .leaveWork: cancelOperation(with: TaxiOperation.self) } } private func cancelOperation(with operationType: Operation.Type) { queue.operations.filter { operation in return type(of: operation) == operationType }.forEach({ $0.cancel() }) } private func add(operation: Operation) { queue.addOperation(operation) } } Output.txt: Результат выполнения
User: Hey Siri, I am leaving my home Siri: performing leaveHome-action User: Hey Siri, I am leaving my work in 3 minutes Siri: performing leaveWork-action User: Hey Siri, I am still working Siri: canceling leaveWork-action DoorOperation: Doors are closed via HomeKit. WindowOperation: Windows are closed via HomeKit. TaxiOperation: operation is canceled