The Command Pattern

The command pattern is used to capture an operation, and any parameters required to perform the operation, in a command object.

In addition to the command, there are 2 other objects that play a role in the pattern:

the receiver: the receiver object contains the logic that is executed by the command

the invoker: the invoker object stores a cache of actions, knows the interface for executing the actions, and ultimately executes the actions by calling methods on the receiver

In the apps I write, I use the command pattern to manage network request bookkeeping and cancellation. Typically I’ll start with a protocol to establish a common interface for my request-management-related command objects:

protocol NetworkCommand {
  func execute()
}

I’ll create two command objects. One for handling a new request, and another for cancelling all outstanding requests:

struct AddCommand: NetworkCommand {
  func execute() {}
}

struct CancelCommand: NetworkCommand {
  func execute() {}
}

Then I’ll add an invoker and a receiver. The invoker, which I’ll call BaseService, knows about the command objects, but only about their interfaces. It does not know anything about the internals of the command objects:

struct BaseService: NetworkService {
  let addCommand: AddCommand
  let cancelCommand: CancelCommand
  
  init() {
    self.addCommand = AddCommand()
    self.cancelCommand = CancelCommand()  
  }
}

The receiver object, which I’ll call RequestManager, does the real work. When called on by the invoker, it either adds a request to its cache or cancels all cached requests.

struct RequestManager {
    private let pendingRequests: NSHashTable

    init() {
        self.pendingRequests = NSHashTable.weakObjectsHashTable()
    }

    func addRequest(request: NSURLSessionDataTask) {
        pendingRequests.addObject(request)
    }
    
    func cancelPendingRequests() {
        let requests = pendingRequests.allObjects
        for request in requests {
            request.cancel()
        }
    }
}

To make all the pieces fit together, the NetworkCommand protocol must be modified a little. The execute method needs to accommodate the structure of the receiver object by taking additional parameters:

protocol NetworkCommand {
    func execute(receiver: RequestManager, request: NSURLRequest?)
}

Now I can fill out the internal logic of my two commands:

struct CancelCommand: NetworkCommand {
    func execute(receiver: RequestManager, request: NSURLRequest?) {
        receiver.cancelPendingRequests()
    }
}

struct AddCommand: NetworkCommand {
    func execute(receiver: RequestManager, request: NSURLSessionDataTask?) {
        if let request = request {
            receiver.addRequest(request)
        }
    }
}

With this logic in place, I can complete the invoker object. When all is said and done, I have:

protocol NetworkCommand {
    func execute(receiver: RequestManager, request: NSURLSessionDataTask?)
}

// Command for cancelling pending requests
struct CancelCommand: NetworkCommand {
    func execute(receiver: RequestManager, request: NSURLSessionDataTask?) {
        receiver.cancelPendingRequests()
    }
}

// Command for adding a request
struct AddCommand: NetworkCommand {
    func execute(receiver: RequestManager, request: NSURLSessionDataTask?) {
        if let request = request {
            receiver.addRequest(request)
        }
    }
}

// Invoker object - stores pending requests and executes commands
struct NetworkService {

    private var cancelCommand: CancelCommand
    private var addCommand: AddCommand
    private var requestManager: RequestManager

    init(requestManager: RequestManager) {
        self.cancelCommand = CancelCommand()
        self.addCommand = AddCommand()
        self.requestManager = requestManager
    }
    
    func cancelPendingRequests() {
        cancelCommand.execute(requestManager, request: nil)
    }

    func addRequest(request: NSURLSessionDataTask) {
        addCommand.execute(requestManager, request: request)
    }
}

// Receiver object - the invoker executes actions on the receiver
struct RequestManager {
    private let pendingRequests: NSHashTable

    init() {
        self.pendingRequests = NSHashTable.weakObjectsHashTable()
    }

    func addRequest(request: NSURLSessionDataTask) {
        pendingRequests.addObject(request)
    }
    
    func cancelPendingRequests() {
        let requests = pendingRequests.allObjects
        for request in requests {
            request.cancel()
        }
    }
}

Usage


let requestManager = RequestManager()
let networkService = NetworkService(requestManager: requestManager)

let URL = NSURL(string: "https://www.google.com/")!
let req = NSURLRequest(URL: URL)
let task = NSURLSession.sharedSession().dataTaskWithRequest(req)
task.resume()

networkService.addRequest(task)
networkService.cancelPendingRequests()