The concept behind this pattern is to transform a request into an object in order to facilitate some actions, such as undo/redo, insertion into a queue, or tracking of the request.
The command pattern creates distance between the client that requests an operation and the object that can perform it. The request is encapsulated into an object. This object contains a reference to the receiver who will effectively execute the operation.
The real operation is managed by the receiver and the command is like an order; it only contains a reference to the invoker, the object that will perform the action and an execute function will call the real operation on the worker.
The classes participating in this pattern are as follows:
Command
: This declares the interface for executing an operation.ConcreteCommand
: This implements the Command
interface with the execute
method by invoking the corresponding operations on Receiver
. It defines a link between the Receiver
class and the action.Client
: This creates a ConcreteCommand
object and sets its receiver.Invoker
: This asks the command to carry out the request.Receiver
: This knows how to perform the operations.The following sequence diagram defines the collaboration between all objects participating in the command pattern:
Let's discuss the preceding diagram in detail:
Imagine that your company is working on a new universal controller that can manage up to four commands. This controller has four slots where we can add two commands to each of them. Near each slot, we have two buttons: the "On" and "Off" buttons.
Your team has already two objects and their specifications that allow a remote control to manipulate them:
Your job is to conceptualize commands that will be stored in the universal remote controller.
When we press the on or off button of a slot, the appropriate command should be sent to the appropriate device (the audio player or the light).
Open the Xcode project called CommandPattern.xcodeproj
. Here is the organization of our project:
The structure of our project reflects the class diagram that we have seen in the Design section of the command pattern:
Invoker
folder that contains our UniversalRemoteController
objectReceiver
folder contains two devices that will be able to receive commands to execute the appropriate operationInterface
folder contains the definition of a commandConcreteCommand
folder contains all the commands that we want to use with our universal remote controllermain.swift
file contains the code that will allow us to see the demoTo implement this example, let's begin with defining our Command
interface.
We only need an execute()
method to execute the command:
protocol ICommand { func execute() }
Before we write our concrete command objects, let's see how our Light
and AudioPlayer
objects are implemented:
class Light { func on() { print("Light is On") } func off() { print("Light is Off") } }
It is quite simple; the on()
function will turn on the light and the off()
function will turn it off.
Now, let's define the AudioPlayer
class:
class AudioPlayer { enum AudioPlayerState { case On case Off case Playing } private var state = AudioPlayerState.Off func on() { state = AudioPlayerState.On print("Audio Player is On") } func off() { state = AudioPlayerState.Off print("Audio Player is Off") } func playCD(){ if state == AudioPlayerState.Off { print("doesn't work : the audio player is currently off") } else { state = AudioPlayerState.Playing print("AudioPlayer is playing") } } func stopCD(){ if state == AudioPlayerState.Off { print("doesn't work : the audio player is currently off") } if state == AudioPlayerState.On { print("doesn't work : the audio player currently doesn't play music") } else { state = AudioPlayerState.On print("AudioPlayer has stopped to play music") } } }
This object is more complex. We have the same on()
and off()
methods, but we also have the playCD()
and StopCD()
methods.
We see that this object has an internal state. The state changes depending on the function called and the state is also used to control if the asked function is possible.
Now that we have all the necessary information, we can start writing our commands.
Let's begin with the light. What we want is to be able to use our universal remote controller to turn a light on or off, depending on the button pushed near the slot.
Therefore, we can first write our LightOnCommand
concrete command object:
class LightOnCommand: ICommand { var light:Light init(light: Light) { self.light = light } func execute() { self.light.on() } }
Here, we created an object called LightOnCommand
that implements the ICommand
interface.
The command needs to know what the receiver object is, so we pass an argument to it during the initialization of the object:
init(light: Light) {
self.light = light
}
Then, the execute
method encapsulates the call to the on()
function of the Light
object to effectively process the command.
That's all; your LightOnCommand
object is now ready.
We do the same with the LightOffCommand
class and make changes wherever it is appropriate in order to use the off()
function of the Light
object instead of on()
:
class LightOffCommand: ICommand { var light:Light init(light: Light) { self.light = light } func execute() { self.light.off() } }
Our commands to control lights are both ready. Let's now see what we will do for the audio player. What we want is to be able to turn on or off the audio player, play, or stop music. These commands are similar to what we have already done with the light.
The
AudioPlayerOnCommand
class is written as follows:
class AudioPlayerOnCommand: ICommand { var audioPlayer:AudioPlayer init(audioPlayer:AudioPlayer) { self.audioPlayer = audioPlayer } func execute() { audioPlayer.on() } }
The
AudioPlayerOffCommand
class is written as follows:
class AudioPlayerOffCommand: ICommand { var audioPlayer:AudioPlayer init(audioPlayer:AudioPlayer) { self.audioPlayer = audioPlayer } func execute() { audioPlayer.off() } }
The
AudioPlayerPlayCdCommand
class is written as follows:
class AudioPlayerPlayCDCommand: ICommand { var audioPlayer:AudioPlayer init(audioPlayer:AudioPlayer) { self.audioPlayer = audioPlayer } func execute() { audioPlayer.playCD() } }
The
AudioPlayerStopCDCommand
class is written as follows:
class AudioPlayerStopCDCommand: ICommand { var audioPlayer:AudioPlayer init(audioPlayer:AudioPlayer) { self.audioPlayer = audioPlayer } func execute() { audioPlayer.stopCD() } }
At this point, all the commands needed are written.
We want to use our remote controller that has only four slots, as shown in the following diagram. With the remote controller, we want to be able to manipulate two lights: one in the bedroom, one in the hall, and an audio player to play and stop music:
Why not create a command that will turn on the audio player and play the music in the same command object? Indeed, using our remote controller to execute only the on or off command is useless. What we want is to play or stop the music.
Imagine that you want to be able to turn on the audio player and play the CD only by pressing one button; in the same way, you want to be able to stop the CD player and turn off the audio player only by pressing one button.
To implement this, we only have to encapsulate appropriate functions of the audio player object on the execute function of the command. When our remote controller will invoke the execute
method, we will first call the on
function of the audioPlayer
class and then the playCD()
function:
class AudioPlayerSetOnAndPlayCommand: ICommand { var audioPlayer:AudioPlayer init(audioPlayer:AudioPlayer) { self.audioPlayer = audioPlayer } func execute() { audioPlayer.on() audioPlayer.playCD() } }
Similarly, we proceed with our StopMusicAndSetOff
command:
class AudioPlayerStopMusicAndSetOff: ICommand { var audioPlayer:AudioPlayer init(audioPlayer:AudioPlayer) { self.audioPlayer = audioPlayer } func execute() { audioPlayer.stopCD() audioPlayer.off() } }
Our devices are ready to accept commands and command objects are ready. Before we start writing our demo code, let's see how the remote controller works:
class UniversalRemoteControl { var onCommands = [ICommand]() var offCommands = [ICommand]() init() { for _ in 1...4 { onCommands.append(NoCommand()) offCommands.append(NoCommand()) } } func addCommandToSlot(slot:Int, onCommand:ICommand, offCommand:ICommand) { onCommands[slot] = onCommand offCommands[slot] = offCommand } func buttonOnIsPushedOnSlot(slot:Int) { onCommands[slot].execute() } func buttonOffIsPushedOnSlot(slot:Int) { offCommands[slot].execute() } }
When the remote controller is initialized, the four slots have a NoCommand
object that is assigned. This object is as follows:
class NoCommand: ICommand { func execute() { print("No command associated to this") } }
So, if we do not use the addCommandToSlot(…)
function, each button will call the execute
function of the NoCommand
object, which means that there is nothing to do.
The remote controller has two buttons near each slot. Depending on the button and slot, buttonOnIsPushedOnSlot(…)
or buttonOffIsPushedOnSlot
is called.
As commands are stored in the onCommands
and offCommands
arrays, when addCommandToSlot
is called, we call the execute
command of the appropriate object. To execute the on command of a slot, we will run the following code:
onCommands[slot].execute()
To execute the off command of the same slot we will also run the following code:
offCommands[slot].execute()
Here, slot
is the index of the button slot. Now, it is time to implement our demo code.
First, we initialize our remote controller, create our audioPlayer
, and create our two lights: the bedroom light and hall light:
let uRemoteControl = UniversalRemoteControl() let audioPlayerLivingRoom = AudioPlayer() let lightBedroom = Light() let lightHall = Light()
Then, we create all our command objects:
// MARK: Definition of our commands let bedroomLightOnCommand = LightOnCommand(light: lightBedroom) let bedroomLightOffCommand = LightOffCommand(light: lightBedroom) let hallLightOnCommand = LightOnCommand(light: lightHall) let hallLightOffCommand = LightOffCommand(light: lightHall) let audioPlayerLivingRoomOnCommand = AudioPlayerOnCommand(audioPlayer: audioPlayerLivingRoom) let audioPlayerLivingRoomOffCommand = AudioPlayerOffCommand(audioPlayer: audioPlayerLivingRoom) let audioPlayerOnAndPlayLivingRoom = AudioPlayerSetOnAndPlayCommand(audioPlayer: audioPlayerLivingRoom) let audioPlayerStopAndOffLivingRoom = AudioPlayerStopMusicAndSetOff(audioPlayer: audioPlayerLivingRoom)
Once our commands are ready, we can assign them to the remote controller using the addCommandToSlot
function:
// Mark: Assign commands to the remote controller uRemoteControl.addCommandToSlot(0, onCommand: bedroomLightOnCommand, offCommand: bedroomLightOffCommand) uRemoteControl.addCommandToSlot(1, onCommand: hallLightOnCommand, offCommand: hallLightOffCommand) uRemoteControl.addCommandToSlot(2, onCommand: audioPlayerLivingRoomOnCommand, offCommand: audioPlayerLivingRoomOffCommand) uRemoteControl.addCommandToSlot(3, onCommand: audioPlayerOnAndPlayLivingRoom, offCommand: audioPlayerStopAndOffLivingRoom)
The last thing that is needed for the demo is to simulate the press on each button:
// Mark: Usage of the remote controller uRemoteControl.buttonOnIsPushedOnSlot(0) uRemoteControl.buttonOffIsPushedOnSlot(0) uRemoteControl.buttonOnIsPushedOnSlot(1) uRemoteControl.buttonOffIsPushedOnSlot(1) uRemoteControl.buttonOnIsPushedOnSlot(2) uRemoteControl.buttonOffIsPushedOnSlot(2) uRemoteControl.buttonOnIsPushedOnSlot(3) uRemoteControl.buttonOffIsPushedOnSlot(3)
Here, note that we have not added the concurrency protection. If the command is used by several components, we should make sure that the concurrency protection is added.
For this, we need to create a queue that will receive all the commands, execute them in a synchronous way, and have the first command receive in the queue, being the first command executed (first in first out). To see how to implement concurrency protection, you can check the implementation of the mediator pattern in Chapter 7, Behavioral Patterns – Iterator, Mediator, and Observer and the note about concurrency protection available in the same chapter.
Click on build and run the demo.
You will now see the following result on the console, corresponding to each button pressed on the universal remote controller: