Команда — это поведенческий паттерн, позволяющий заворачивать запросы или простые операции в отдельные объекты.
Это позволяет откладывать выполнение команд, выстраивать их в очереди, а также хранить историю и делать отмену.
Концептуальный пример
Этот пример показывает структуру паттерна Команда , а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом.
После ознакомления со структурой, вам будет легче воспринимать второй пример, который рассматривает реальный случай использования паттерна в мире 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 {
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 {
override func main() {
print("\(self): Windows are closed via HomeKit.")
}
override var description: String { return "WindowOperation" }
}
class DoorOperation: DelayedOperation {
override func main() {
print("\(self): Doors are closed via HomeKit.")
}
override var description: String { return "DoorOperation" }
}
class TaxiOperation: DelayedOperation {
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