The command pattern

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.

Roles

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.

This pattern allows the following features:

  • Sending requests to different receivers
  • Queuing, logging, and rejecting requests
  • Undoable actions (the execute method can memorize the state and allows you to go back to that state)
  • Encapsulate a request in an object
  • Allows the parameterization of clients with different requests

Design

The generic diagram class is represented as follows:

Design

Participants

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.

Collaboration

The following sequence diagram defines the collaboration between all objects participating in the command pattern:

Collaboration

Let's discuss the preceding diagram in detail:

  • The client asks for a command to be executed and specifies its receivers
  • The client then sends the command to the invoker that stores it (or places it in a queue system if some actions needs to be performed before execution of the command) in order to execute it later
  • The invoker is then called to launch the command by invoking the execute function on the appropriate command object
  • The concrete command asks the receiver to execute the appropriate operation

Illustration

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:

  • The objects that we will work with are a light and audio player
  • The light can only be turned on and off
  • The audio player can be turned on or off, and we can play or stop the music

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).

Note

In this example, we will not implement the undo/redo action. We will show you another pattern that is dedicated to this situation in the next chapter.

Implementation

Open the Xcode project called CommandPattern.xcodeproj. Here is the organization of our project:

Implementation

The structure of our project reflects the class diagram that we have seen in the Design section of the command pattern:

  • We have the Invoker folder that contains our UniversalRemoteController object
  • The Receiver folder contains two devices that will be able to receive commands to execute the appropriate operation
  • The Interface folder contains the definition of a command
  • The ConcreteCommand folder contains all the commands that we want to use with our universal remote controller
  • Lastly, the main.swift file contains the code that will allow us to see the demo

To 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:

Implementation

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)

Note

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:

Implementation
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset