Command es un patrón de diseño de comportamiento que convierte solicitudes u operaciones simples en objetos.
La conversión permite la ejecución diferida de comandos, el almacenamiento del historial de comandos, etc.
Ejemplo conceptual
Este ejemplo ilustra la estructura del patrón de diseño Command y se centra en las siguientes preguntas:
¿De qué clases se compone?
¿Qué papeles juegan esas clases?
¿De qué forma se relacionan los elementos del patrón?
Después de conocer la estructura del patrón, será más fácil comprender el siguiente ejemplo basado en un caso de uso real de Swift.
Example.swift: Ejemplo conceptual
import XCTest
/// The Command interface declares a method for executing a command.
protocol Command {
func execute()
}
/// Some commands can implement simple operations on their own.
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 + ")")
}
}
/// However, some commands can delegate more complex operations to other
/// objects, called "receivers."
class ComplexCommand: Command {
private var receiver: Receiver
/// Context data, required for launching the receiver's methods.
private var a: String
private var b: String
/// Complex commands can accept one or several receiver objects along with
/// any context data via the constructor.
init(_ receiver: Receiver, _ a: String, _ b: String) {
self.receiver = receiver
self.a = a
self.b = b
}
/// Commands can delegate to any methods of a receiver.
func execute() {
print("ComplexCommand: Complex stuff should be done by a receiver object.\n")
receiver.doSomething(a)
receiver.doSomethingElse(b)
}
}
/// The Receiver classes contain some important business logic. They know how to
/// perform all kinds of operations, associated with carrying out a request. In
/// fact, any class may serve as a Receiver.
class Receiver {
func doSomething(_ a: String) {
print("Receiver: Working on (" + a + ")\n")
}
func doSomethingElse(_ b: String) {
print("Receiver: Also working on (" + b + ")\n")
}
}
/// The Invoker is associated with one or several commands. It sends a request
/// to the command.
class Invoker {
private var onStart: Command?
private var onFinish: Command?
/// Initialize commands.
func setOnStart(_ command: Command) {
onStart = command
}
func setOnFinish(_ command: Command) {
onFinish = command
}
/// The Invoker does not depend on concrete command or receiver classes. The
/// Invoker passes a request to a receiver indirectly, by executing a
/// 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()
}
}
/// Let's see how it all comes together.
class CommandConceptual: XCTestCase {
func test() {
/// The client code can parameterize an invoker with any commands.
let invoker = Invoker()
invoker.setOnStart(SimpleCommand("Say Hi!"))
let receiver = Receiver()
invoker.setOnFinish(ComplexCommand(receiver, "Send email", "Save report"))
invoker.doSomethingImportant()
}
}
Output.txt: Resultado de la ejecución
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)
Ejemplo del mundo real
Example.swift: Ejemplo del mundo real
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: Resultado de la ejecución
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