21.
Command Pattern
Written by Joshua Greene
The command pattern is a behavioral pattern that encapsulates information to perform an action into a command object. It involves three types:
- The invoker stores and executes commands.
- The command encapsulates the action as an object.
- The receiver is the object that’s acted upon by the command.
Hence, this pattern allows you to model the concept of executing an action.
When should you use it?
Use this pattern whenever you want to create actions that can be executed on receivers at a later point in time. For example, you can create and store commands to be performed by a computer AI, and then execute these over time.
Playground example
Open AdvancedDesignPatterns.xcworkspace in the Starter directory, and then open the Command page.
For this playground example, you’ll create a simple guessing game: a Doorman will open and close a Door a random number of times, and you’ll guess in advance whether the door will be open or closed in the end.
Add the following after Code Example:
import Foundation
// MARK: - Receiver
public class Door {
public var isOpen = false
}
Door is a simple model that will act as the receiver. It will be opened and closed by setting its isOpen property.
Add the following code next:
// MARK: - Command
// 1
public class DoorCommand {
public let door: Door
public init(_ door: Door) {
self.door = door
}
public func execute() { }
}
// 2
public class OpenCommand: DoorCommand {
public override func execute() {
print("opening the door...")
door.isOpen = true
}
}
// 3
public class CloseCommand: DoorCommand {
public override func execute() {
print("closing the door...")
door.isOpen = false
}
}
Here’s what this does:
-
You first define a class called
DoorCommand, which acts at the command. This class is intended to be an abstract base class, meaning you won’t instantiate it directly. Rather, you will instantiate and use its subclasses.This class has one property,
door, which you set within its initializer. It also has a single method,execute(), which you override within its subclasses. -
You next define a class called
OpenCommandas a subclass ofDoorCommand. This overridesexecute(), wherein it prints a message and setsdoor.isOpentotrue. -
You lastly define
CloseCommandas a subclass ofDoorCommand. This likewise overridesexecute()to print a message and setsdoor.isOpentofalse.
Next, add the following to the end of the playground:
// MARK: - Invoker
// 1
public class Doorman {
// 2
public let commands: [DoorCommand]
public let door: Door
// 3
public init(door: Door) {
let commandCount = arc4random_uniform(10) + 1
self.commands = (0 ..< commandCount).map { index in
return index % 2 == 0 ?
OpenCommand(door) : CloseCommand(door)
}
self.door = door
}
// 4
public func execute() {
print("Doorman is...")
commands.forEach { $0.execute() }
}
}
Here’s what this does in detail:
- You define a class called
Doorman, which will act as the invoker. - You define two properties on
Doorman:commandsanddoor. - Within
init(door:), you generate a random number,commandCount, to determine how many times thedoorshould be opened and closed. You setcommandsby iterating from0tocommandCountand returning either anOpenCommandorCloseCommandbased on whether or not theindexis even. - You lastly define
execute(), wherein you callexecute()on each of thecommands.
Great! You’re ready to try out these classes. Enter the following at the end of the playground:
// MARK: - Example
public let isOpen = true
print("You predict the door will be " +
"\(isOpen ? "open" : "closed").")
print("")
You make a prediction for whether the Door will ultimately be open or closed, as determined by isOpen. You should see this printed to the console:
You predict the door will be open.
If you don’t think it will be open, change isOpen to false instead.
Add the following to the end of the playground::
let door = Door()
let doorman = Doorman(door: door)
doorman.execute()
print("")
You create a door and doorman, and then call doorman.execute(). You should see something like this printed to the console. The number of opening and closing statements will depend on whatever random number is chosen!
Doorman is...
opening the door...
closing the door...
opening the door...
To complete the game, you should also print out whether your guess was right or wrong.
To do so, add the following to the end of the playground:
if door.isOpen == isOpen {
print("You were right! :]")
} else {
print("You were wrong :[")
}
print("The door is \(door.isOpen ? "open" : "closed").")
If you guessed right, you’ll see this printed to the console:
You were right!
The door is open.
To repeat the game, press the “Stop Playground” button, and then press the “Play” button that appears.
What should you be careful about?
The command pattern can result in many command objects. Consequently, this can lead to code that’s harder to read and maintain. If you don’t need to perform actions later, you may be better off simply calling the receiver’s methods directly.
Tutorial project
You’ll build a game app called RayWenToe in this chapter. This is a variation on TicTacToe. Here are the rules:
-
Like TicTacToe, players place Xs and Os on a 3x3 gameboard. The first player is X, and the second player is O.
-
Unlike TicTacToe, each player secretly makes five selections at the beginning of the game, which may not be changed. Players then alternate placing Xs and Os on the gameboard in their preselected order.
-
If a player places his mark on a spot that’s already taken, his mark overwrites the existing mark.
-
A player may select the same spot multiple times, and he may even select the same spot for all of his selections.
-
After all of the players’ selections have been played, a winner is decided.
-
Like TicTacToe, if only one player has three marks in a row — vertically, horizontally or diagonally — that player is the winner.
-
If both players have three marks in a row, or neither player has, the first player (X) is the winner.
-
Thereby, it’s a reasonable strategy for the first player to try to get three Xs in a row or to prevent his opponent from getting three Os in a row.
-
The only way for the second player (O) to win is to get three Os in a row without his opponent having three Xs in a row as well.
Can you guess which pattern you’ll use? The command pattern, of course!
Building your game
Open Finder and navigate to where you downloaded the resources for this chapter. Then, open starter\RayWenToe\RayWenToe.xcodeproj in Xcode.
Build and run, and you’ll be presented with a Select Gameplay Mode screen:
Select One Player Mode, and you’ll see the gameboard:
If you tap on a spot, however, nothing happens. You need to implement this logic.
Open GameManager.swift, and scroll to onePlayerMode(); this method is a class constructor to create a GameManager for one-player mode.
RayWenToe uses the state pattern — see Chapter 15, “State Pattern,” if you’re not familiar with it — to support both one-player and two-player modes. Specifically, it uses three states:
-
PlayerInputStateallows the user to select spots on the gameboard. -
ComputerInputStategenerates spots on the gameboard for the computer AI. -
PlayGameStatealternates placingplayer1andplayer2positions on the board.
Open PlayerInputState.swift, and you’ll see there are a few methods containing TODO: - comments. Likewise, if you open ComputerInputState.swift and PlayGameState.swift, you’ll see a few other methods with similar comments.
These methods all require a command object to complete them!
Creating and storing command objects
Add a new Swift file called MoveCommand.swift to the GameManager group, which is a subgroup within the Controllers group, and replace its contents with the following:
// 1
public struct MoveCommand {
// 2
public var gameboard: Gameboard
// 3
public var gameboardView: GameboardView
// 4
public var player: Player
// 5
public var position: GameboardPosition
}
Here’s what you’ve done:
-
You first defined a new
structcalledMoveCommand. Ultimately, this will place a player’s move onto thegameboardand thegameboardView. -
The
Gameboardis a model that represents the TicTacToe board. It contains a 2D array ofpositions, which holds onto thePlayerthat has played at a given spot on the board. -
The
GameboardViewis a view for the RayWenToe board. It already contains logic to draw the board and to draw aMarkView, representing either an X or an O, at a givenposition. It also has the logic to notify itsdelegatein response to touches, which has been set toGameplayViewController. -
The
Playerrepresents the user that performed this move. It contains amarkViewPrototype, which uses the prototype pattern — see Chapter 14, “Prototype Pattern,” if you’re not familiar with it — to allow a newMarkViewto be created by copying it. -
The
GameboardPositionis a model for the gameboard position at which this move should be performed.
In order to be useful, you also need to declare a means to execute this command. Add the following method next, right before the closing curly brace:
public func execute(completion: (() -> Void)? = nil) {
// 1
gameboard.setPlayer(player, at: position)
// 2
gameboardView.placeMarkView(
player.markViewPrototype.copy(), at: position,
animated: true, completion: completion)
}
Here’s what this does:
-
You first set the
playerat thepositionon thegameboard. This doesn’t affect how the view looks but, rather, it’s used to determine the game’s winner at the end. -
You then create a copy of the player’s
markViewPrototypeand set this at the givenpositionon thegameboardView. This method has already been implemented for you, including animation and calling thecompletionclosure when its finished. If you’re curious how it works, seeGameboardView.swiftfor its implementation.
Since gameboard and gameboardView are acted upon by this command, they are both receivers.
With this done, you’re now ready to put the command into use! Open GameManager.swift and add the following, right after the gameboard property:
internal lazy var movesForPlayer =
[player1: [MoveCommand](), player2: [MoveCommand]()]
You’ll use this to hold onto the MoveCommand objects for a given Player.
Next, open GameState.swift and add the following, right after the gameplayView property:
public var movesForPlayer: [Player: [MoveCommand]] {
get { return gameManager.movesForPlayer }
set { gameManager.movesForPlayer = newValue }
}
Here, you declare a computed property for movesForPlayer, which sets and returns gameManager.movesForPlayer.
You’ll use this property a lot in both PlayerInputState and ComputerInputState, so this computed property will make your code a bit shorter and easier to read.
This handles storing the command objects! You next need to actually create them.
Open PlayerInputState.swift and replace addMove(at:) with the following:
// 1
public override func addMove(at position: GameboardPosition) {
// 2
let moveCount = movesForPlayer[player]!.count
guard moveCount < turnsPerPlayer else { return }
// 3
displayMarkView(at: position, turnNumber: moveCount + 1)
// 4
enqueueMoveCommand(at: position)
updateMoveCountLabel()
}
Here’s what this does:
-
addMove(at:)is called byGameManager, which in turn is called byGamePlayViewControllerin response to the user selecting a spot on theGameboardView. This method is where you need to display aMarkViewfor the selection and enqueue aMoveCommandto be executed later. -
Next, you create a variable for
moveCountby getting thecountofmovesForPlayerfor the givenPlayer. IfmoveCountisn’t less thanturnsPerPlayer, then the user has already picked all of her spots, and you return early. -
Next, you call
displayMarkView(at:turnNumber:), passing the selectedpositionandmoveCount + 1. SincemoveCountis zero-indexed, you increment this by1to show the first turn as “1” instead of “0”.displayMarkView(at:turnNumber:)has already been implemented for you. -
Finally, you call
enqueueMoveCommand(at:)andupdateMoveCountLabel(). Both of these require you to useMoveCommand, so you’ll need to implement these next.
Implementing move commands
Replace the contents of enqueueMoveCommand(at:) with the following:
let newMove = MoveCommand(gameboard: gameboard,
gameboardView: gameboardView,
player: player,
position: position)
movesForPlayer[player]!.append(newMove)
You here create a new MoveCommand and append this to the existing array at movesForPlayer[player].
Next, replace the contents of updateMoveCountLabel() with the following:
let turnsRemaining = turnsPerPlayer - movesForPlayer[player]!.count
gameplayView.moveCountLabel.text =
"\(turnsRemaining) Moves Left"
You calculate the turnsRemaining by subtracting the number of moves already added, given by movesForPlayer[player]!.count, from the turnsPerPlayer, which is the total number of moves allowed per player. You then use this to set moveCountLabel.text.
Build and run, select One Player Mode and tap a spot on the gameboard. You should now see that an X appears! You can even tap on the same spot multiple times, and this is handled correctly, too.
If you press Play or Undo, nothing happens. You need to implement handleActionPressed() and handleUndoPressed() for these.
Still in PlayerInputState.swift, replace the contents of handleActionPressed() with the following:
guard movesForPlayer[player]!.count == turnsPerPlayer
else { return }
gameManager.transitionToNextState()
You first verify the player has made all of her selections. If not, you return early. Otherwise, you call gameManager.transitionToNextState(). Said method simply moves to the next GameState: in one-player mode, this transitions to ComputerInputState, and, in two-player mode, this goes to another PlayerInputState for the other player.
Next, replace the contents of handleUndoPressed() with the following:
// 1
var moves = movesForPlayer[player]!
guard let position = moves.popLast()?.position else { return }
// 2
movesForPlayer[player] = moves
updateMoveCountLabel()
// 3
let markView = gameboardView.markViewForPosition[position]!
_ = markView.turnNumbers.popLast()
// 4
guard markView.turnNumbers.count == 0 else { return }
gameboardView.removeMarkView(at: position, animated: false)
There’s a lot happening here:
-
First, you get the
movesfor the givenplayerfrommovesForPlayer, and you callpopLast()to remove and return the last object. If there aren’t any commands to pop, this will returnnil, and you return early. If there is a command that’s popped, you get itsposition. -
Next, you update
movesForPlayer[player]with the new array ofmovesand callupdateMoveCountLabel()to show the new number of turns remaining. -
Next, you get the
markViewfrom thegameboardViewand callturnNumbers.popLast()on it.MarkViewusesturnNumbersin order to display the order that it was selected. This is an array since the player can select the same spot more than once. -
Finally, you check if
markView.turnNumbers.countequals zero, and, if so, this means all of the moves for theMarkViewhave been popped. In which case, you remove it from thegameboardViewby callingremoveMarkView(at:animated:).
Build and run, select One Player Mode and tap a spot to add a move. Then, press Undo, and your move will be removed.
If you press Play, however, still nothing happens. What gives?
Remember how PlayGameState.swift and ComputerInputState.swift also had stubbed out methods? Yep, you have to implement these to play the game!
Open PlayGameState.swift and add the following method after begin():
private func combinePlayerMoves() -> [MoveCommand] {
var result: [MoveCommand] = []
let player1Moves = movesForPlayer[player1]!
let player2Moves = movesForPlayer[player2]!
assert(player1Moves.count == player2Moves.count)
for i in 0 ..< player1Moves.count {
result.append(player1Moves[i])
result.append(player2Moves[i])
}
return result
}
As its name implies, this method combines the MoveCommand objects for Player1 and Player2 into a single array. You’ll use this to alternate performing each moves for each player.
Next, add the following method right after combinePlayerMoves():
private func performMove(at index: Int,
with moves: [MoveCommand]) {
// 1
guard index < moves.count else {
displayWinner()
return
}
// 2
let move = moves[index]
move.execute(completion: { [weak self] in
self?.performMove(at: index + 1, with: moves)
})
}
Here’s what this does:
-
You check that the passed-in
indexis less thanmoves.count. If it isn’t, then all of the moves have been played, and you calldisplayWinner()to calculate and display the winner. -
You get the
movefor the givenindexand thenexecuteit. Within thecompletionclosure, you recursively callperformMove(at: with:)again, incrementing theindexby1. In this manner, you will execute each of themovesin order.
You also need to call these methods. Replace the TODO comment within begin() with the following:
let gameMoves = combinePlayerMoves()
performMove(at: 0, with: gameMoves)
Here, you simply use the methods you just created.
Awesome! You’re ready to try out the game. Build and run, but this time select Two Player Mode.
Select gameboard spots for the first player and press Ready. Then, select spots for the second player and press Play. You’ll then see each of the MoveCommands executed in order and animated onscreen.
If you press New Game, however, you’ll notice there’s an issue - the “moves left” label shows as 0! This is because you don’t currently reset movesForPlayer whenever a new game is started. Fortunately, this is easy to fix.
Open GameManager.swift and replace the TODO comment within newGame() with the following:
movesForPlayer = [player1: [], player2: []]
You can now play as many games as you’d like in Two Player Mode!
In case you don’t have a friend around, you also need to complete One Player Mode. To do so, you’ll need to complete ComputerInputState.swift. Instead of accepting spot selections from a user as PlayerInputState does, ComputerInputState will generate these automatically.
Open ComputerInputState.swift and replace the TODO comment within begin() with this:
movesForPlayer[player] = positions.map {
MoveCommand(gameboard: gameboard,
gameboardView: gameboardView,
player: player,
position: $0)
}
gameManager.transitionToNextState()
The logic to generate positions to play on has already been implemented for you, via generateRandomWinningCombination(). Here, you map those positions to create an array of MoveCommand objects, which you set on movesForPlayer. You then immediately called gameManager.transitionToNextState(), which will ultimately transition to PlayGameState and begin the game.
Build and run, and select One Player Mode. Pick your spots, press Play, and watch the game play out!
Key points
You learned about the command pattern in this chapter. Here are its key points:
-
The command pattern encapsulates information to perform an action into a command object. It involves three types: an invoker, command and receiver.
-
The invoker stores and executes commands; the command encapsulates an action as an object; and the receiver is the object that’s acted upon.
-
This pattern works best for actions that need to be stored and executed later. If you always intend to execute actions immediately, consider calling the methods directly on the receiver instead.
Where to go from here?
You created a fun variant of TicTacToe where players select their moves in advance. There’s still a lot of functionality and changes you can make to RayWenToe:
-
You can use a larger board size, instead of the vanilla size of 3x3. Both
GameboardViewandGameboardhave been written generically to support arbitrary board sizes of 3x3 or larger, so you can easily change this and see how it affects the game. -
Instead of just showing a text label for who won, you can create a new
GameStateto draw a line connecting the winning views. -
You can add a three-person variation and a new mark entirely, instead of just
XandO.
Each of these is possible using the existing patterns you’ve already learned from this book. Feel free to continue experimenting with RayWenToe as much as you like.
When you’re ready, continue onto the next chapter to learn about the chain-of-responsibility pattern.