本文大部分内容翻译至《Pro Design Pattern In Swift》By Adam Freeman,一些地方做了些许修改,并将代码升级到了Swift2.0,翻译不当之处望多包涵。
命令模式(The Command Pattern)
“行为请求者”与“行为实现者”通常呈现一种“紧耦合”。但在某些场合,比如要对行为进行“记录、撤销/重做、事务”等处理,这种无法抵御变化的紧耦合是不合适的。在这种情况下,如何将“行为请求者”与“行为实现者”解耦,将一组行为抽象为对象,实现二者之间的松耦合。
示例工程
Xcode OS X Command Line Tool工程:
Calculator.swift
class Calculator {
private(set) var total = 0
func add(amount:Int) {
total += amount
}
func subtract(amount:Int) {
total -= amount
}
func multiply(amount:Int) {
total = total * amount
}
func divide(amount:Int) {
total = total / amount
}
}
Calculator类定义了一个存储属性total,以及改变total值的一些方法。
main.swift
let calc = Calculator()
calc.add(10)
calc.multiply(4)
calc.subtract(2)
print("Total: \(calc.total)")
运行程序,输出:
Total: 38
理解命令模式解决的问题
假如两个请求组件同时操作同一个 Calculator 对象,每个请求组件都去调用方法去修改total的值。
现在想象一下你需要支持撤销前面的操作。一个解决方法是每一个请求组件都追踪自己的操作以便未来能撤销。但是这么做的问题是每一个组件都不知道其他组件做了什么,这样很可能导致撤销出现混乱。
理解命令模式
命令模式用一个对象来提供方法调用所有可以解决撤销等一系列问题。
命令模式的关键就是这个 命令对象。在它的私有实现里,命令对象有接收对象的引用和如何让接收者调用方法的说明。
图中,receiver和invocation instructions都是私有的所有请求组件并不能访问。唯一公有的是execution method。
实现命令模式
定义命令协议
实现命令模式的第一步是定义一个拥有execution 方法的协议。
Commands.swift
protocol Command {
func execute()
}
行为实现者允许行为请求者执行方法但隐藏了receiver对象和invocation instructions。
定义命令实现类
实现类对于swift来说可以很简单,因为instructions可以用闭包来实现。
Commands.swift
protocol Command {
func execute()
}
class GenericCommand<T> : Command {
private var receiver: T
private var instructions: T -> Void
init(receiver:T, instructions: T -> Void) {
self.receiver = receiver
self.instructions = instructions
}
func execute() {
instructions(receiver)
}
class func createCommand(receiver:T, instuctions: T -> Void) -> Command {
return GenericCommand(receiver: receiver, instructions: instuctions)
}
}
我们定义了一个泛型实现类GenericCommand ,它有私有属性receiver 和instructions。execute方法调用了instructions并将receiver作为参数传递了进去。
应用命令模式
Calculator.swift
class Calculator {
private(set) var total = 0
private var history = [Command]()
func add(amount:Int) {
addUndoCommand(Calculator.add, amount: amount)
total += amount
}
func subtract(amount:Int) {
addUndoCommand(Calculator.subtract, amount: amount)
total -= amount
}
func multiply(amount:Int) {
addUndoCommand(Calculator.multiply, amount: amount)
total = total * amount
}
func divide(amount:Int) {
addUndoCommand(Calculator.divide, amount: amount)
total = total / amount
}
private func addUndoCommand(method:Calculator -> Int -> Void, amount:Int) {
self.history.append(GenericCommand<Calculator>.createCommand(self){calc in
method(calc)(amount)
})
}
func undo() {
if self.history.count > 0 {
self.history.removeLast().execute()
// temporary measure - executing the command adds to the history
self.history.removeLast()
}
}
}
并发保护
Calculator.swift
import Foundation
class Calculator {
private(set) var total = 0
private var history = [Command]()
private var queue = dispatch_queue_create("arrayQ", DISPATCH_QUEUE_SERIAL)
private var performingUndo = false
func add(amount:Int) {
addUndoCommand(Calculator.subtract, amount: amount)
total += amount
}
func subtract(amount:Int) {
addUndoCommand(Calculator.add, amount: amount)
total -= amount
}
func multiply(amount:Int) {
addUndoCommand(Calculator.divide, amount: amount)
total = total * amount
}
func divide(amount:Int) {
addUndoCommand(Calculator.multiply, amount: amount)
total = total / amount
}
private func addUndoCommand(method:Calculator -> Int -> Void, amount:Int) {
if (!performingUndo){
dispatch_sync(queue){[weak self] in
self!.history.append(GenericCommand<Calculator>.createCommand(self!){calc in
//Currying
method(calc)(amount)
})
}
}
}
func undo() {
dispatch_sync(self.queue){[weak self] in
if self!.history.count > 0 {
self!.performingUndo = true
self!.history.removeLast().execute()
self!.performingUndo = false
}
}
}
}
我们创建了一个串行队列,用同步方法dispatch_sync提交操作命令给数组来保证 history数组不会被并发修改。同时我们定义了一个performingUndo布尔变量,用来当我们执行撤销命令时阻止addUndoCommand 方法调用。
使用撤销功能
main.swift
let calc = Calculator()
calc.add(10)
calc.multiply(4)
calc.subtract(2)
print("Total: \(calc.total)")
for _ in 0 ..< 3 {
calc.undo()
print("Undo called. Total: \(calc.total)")
}
运行程序,可以看到值最后为0:
Undo called. Total: 40
Undo called. Total: 10
Undo called. Total: 0
命令模式的变形
下面将一次介绍三种变形:
1.创建复合命令
前面的例子中每一次只能撤销一个命令,但是利用command协议来创建一个外层类实现复数命令也是很简单的。
Commands.swift
.....
class CommandWrapper : Command {
private let commands:[Command]
init(commands:[Command]) {
self.commands = commands
}
func execute() {
for command in commands {
command.execute()
}
}
}
.....
CommandWrapper类创建了一个存储command对象的常量数组。接下来我们在 Calculator类中增加一个方法。
Calculator.swift
......
func getHistorySnaphot() -> Command? {
var command:Command?
dispatch_sync(queue){[weak self] in
command = CommandWrapper(commands: self!.history.reverse())
}
return command
}
.....
最后修改main.swit:
let calc = Calculator()
calc.add(10)
calc.multiply(4)
calc.subtract(2)
let snapshot = calc.getHistorySnaphot()
print("Pre-Snapshot Total: \(calc.total)")
snapshot?.execute()
print("Post-Snapshot Total: \(calc.total)")
运行程序:
Pre-Snapshot Total: 38
Post-Snapshot Total: 0
2.宏命令
命令通畅可以做成宏,这样允许一系列的针对不同对象的操作。将命令当做对象使用的话,receiver就必须传递给excute方法而不是初始化方法。
Commands.swift
protocol Command {
func execute(receiver:Any)
}
class CommandWrapper : Command {
private let commands:[Command]
init(commands:[Command]) {
self.commands = commands;
}
func execute(receiver:Any) {
for command in commands {
command.execute(receiver)
}
}
}
class GenericCommand<T> : Command {
private var instructions: T -> Void
init(instructions: T -> Void) {
self.instructions = instructions
}
func execute(receiver:Any) {
if let safeReceiver = receiver as? T {
instructions(safeReceiver)
} else {
fatalError("Receiver is not expected type")
}
}
class func createCommand(instuctions: T -> Void) -> Command {
return GenericCommand(instructions: instuctions)
}
}
Calculator.swift
import Foundation
class Calculator {
private(set) var total = 0
private var history = [Command]()
private var queue = dispatch_queue_create("arrayQ", DISPATCH_QUEUE_SERIAL)
func add(amount:Int) {
addMacro(Calculator.add, amount: amount)
total += amount
}
func subtract(amount:Int) {
addMacro(Calculator.subtract, amount: amount)
total -= amount
}
func multiply(amount:Int) {
addMacro(Calculator.multiply, amount: amount)
total = total * amount
}
func divide(amount:Int) {
addMacro(Calculator.divide, amount: amount)
total = total / amount
}
private func addMacro(method:Calculator -> Int -> Void, amount:Int) {
dispatch_sync(self.queue){[weak self] in
self!.history.append(GenericCommand<Calculator>.createCommand(
{ calc in method(calc)(amount) }))
}
}
func getMacroCommand() -> Command? {
var command:Command?
dispatch_sync(queue){[weak self] in
command = CommandWrapper(commands: self!.history)
}
return command
}
}
最后我们看main.swift:
let calc = Calculator()
calc.add(10)
calc.multiply(4)
calc.subtract(2)
print("Calc 1 Total: \(calc.total)")
let macro = calc.getMacroCommand()
let calc2 = Calculator()
macro?.execute(calc2)
print("Calc 2 Total: \(calc2.total)")
运行程序,得到下面结果:
Calc 1 Total: 38
Calc 2 Total: 38
3.闭包代替命令
Calculator.swift
import Foundation;
class Calculator {
private(set) var total = 0
typealias CommandClosure = (Calculator -> Void)
private var history = [CommandClosure]()
private var queue = dispatch_queue_create("arrayQ", DISPATCH_QUEUE_SERIAL)
func add(amount:Int) {
addMacro(Calculator.add, amount: amount)
total += amount
}
func subtract(amount:Int) {
addMacro(Calculator.subtract, amount: amount)
total -= amount
}
func multiply(amount:Int) {
addMacro(Calculator.multiply, amount: amount);
total = total * amount
}
func divide(amount:Int) {
addMacro(Calculator.divide, amount: amount)
total = total / amount
}
private func addMacro(method:Calculator -> Int -> Void, amount:Int) {
dispatch_sync(self.queue, {() in
self.history.append({ calc in method(calc)(amount)})
})
}
func getMacroCommand() -> (Calculator -> Void) {
var commands = [CommandClosure]();
dispatch_sync(queue, {() in
commands = self.history
})
return { calc in
if (commands.count > 0) {
for index in 0 ..< commands.count {
commands[index](calc)
}
}
}
}
}
接着我们看main.swift:
let calc = Calculator()
calc.add(10)
calc.multiply(4)
calc.subtract(2)
print("Calc 1 Total: \(calc.total)")
let macro = calc.getMacroCommand()
let calc2 = Calculator()
macro(calc2)
print("Calc 2 Total: \(calc2.total)")
运行程序:
Calc 1 Total: 38
Calc 2 Total: 38