
Command を Swift で
Command は、 振る舞いに関するデザインパターンの一つで、 リクエストや簡単な操作をオブジェクトに変換します。
変換により、 コマンドの遅延実行や遠隔実行を可能にしたり、 コマンドの履歴の保存を可能にしたりできます。
複雑度:
人気度:
使用例: Command パターンは、 Swift コードではよく見かけます。 最もよく使われるのは、 UI 要素をアクションでパラメーター化する時のコールバックの代わりとしてです。 また、 タスクをキューに入れたり、 操作履歴の管理などでも使われます。
見つけ方: Command パターンは、 抽象またはインターフェース型 (送り手) 中の行動的メソッド (複数) が違う抽象またはインターフェース型 (受け手) 中のある一つのメソッドを起動することから識別できます。 受け手は、 生成時にコマンドの実装によりカプセル化されています。 コマンドのクラスは通常特定のアクションに限定されています。
以下の例は Swift Playgroundsで利用できます。
Playgroundバージョンを作成してくれた Alejandro Mohamadに感謝します。
概念的な例
この例は、 Command デザインパターンの構造を説明するためのものです。 以下の質問に答えることを目的としています:
- どういうクラスからできているか?
- それぞれのクラスの役割は?
- パターンの要素同士はどう関係しているのか?
ここでパターンの構造を学んだ後だと、 これに続く、 現実世界の Swift でのユースケースが理解しやすくなります。
Example.swift: 概念的な例
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: 実行結果
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