Head First Design Patterns using Go — 6. Encapsulating Invocation: the Command Pattern

A comprehensive series exploring design patterns, inspired by the O’Reilly book of the same name, adapted from Java to Go.

Piyush Sinha
FAUN — Developer Community 🐾

--

Table of Contents

  1. Introduction
  2. Welcome to Design Patterns: the Strategy Pattern
  3. Keeping your Objects in the know: the Observer Pattern
  4. Decorating Objects: the Decorator Pattern
  5. Baking with OO goodness: the Factory Pattern
  6. Encapsulating Invocation: the Command Pattern
  7. Being Adaptive: the Adapter and Facade Patterns
  8. Encapsulating Algorithms: the Template Method Pattern
  9. Well-managed Collections: the Iterator and Composite Patterns
  10. The State of Things: the State Pattern
  11. Controlling Object Access: the Proxy Pattern
  12. Patterns of Patterns: Compound Patterns

This series of articles will run you by the design patterns the way it is explained in the Head First Design Patterns book using Go as the coding language.

Problem Statement 1:

  • Design the Home Automation Remote Control.
  • The remote consists of 7 programmable slots (each can be assigned to a different household device) along with corresponding on/off buttons for each.
  • Enclosed will be a set of APIs that is created by various vendors to control home automation devices such as lights, fans, audio equipments etc.
  • Note that it is important that we be able to control any future devices that the vendors may supply.
The Remote Control
Vendor Classes

Problem Statement 2:

  • Seems like not a lot of thought has been given to come up with a set of common interfaces by the vendors.
  • We don’t want the remote to have to know the specifics of the vendor classes.
  • We don’t either want the remote to consist of a set of if statements, like “if slot1 == Light, then light.on(), else if slot1 = Hottub then hottub.jetsOn()”

Solution:

  • Decouple the requester of an action from the object that actually performs the action.
  • Here the requester would be the remote control and the object that performs the action would be an instance of one of the vendor class.
  • We can decouple them by introducing “command” objects into our design. A command object encapsulates a request to do something (like turn on a light) on a specific object (say, the living room light object). So, if we store a command object for each button, when the button is pressed we ask the command object to do some work.
  • The remote doesn’t have any idea what the work is, it just has a command object that knows how to talk to the right object to get the work done. So, you see, the remote is decoupled from the light object!
  • This pattern is called Command Pattern
  • Wiki Definition: The Command Pattern encapsulates a request as an object, thereby letting you parameterise other objects with different requests, queue or log requests, and support undoable operations.
  • Let’s dig into some code to understand this
  1. Implementing the command interface
type iCommand interface {
execute()
}

2. Implementing a Command to turn a light on

Remember the Light class has two methods

type lightOnCommand struct {
light *light
}

/**
* The constructor is passed the specific
* light that this command is going to
* control - say the living room light
* - and stashes it in the light instance
* variable. When execute gets called, this
* is the light object that is going to be
* the Receiver of the request.
*/

func newLightOnCommand(l *light) *lightOnCommand {
return &lightOnCommand{
light: l,
}
}

/**
* The execute method calls the
* on() method on the receiving
* object, which is the light we
* are controlling
*/

func (l *lightOnCommand) execute() {
l.light.on()
}

3. Using the command object

Imagine we have a remote control with only one button and corresponding slot to hold a device to control

type simpleRemoteControl struct {
slot iCommand
}

/**
* We have a method for setting
* the command the slot is going
* to control. This could be called
* multiple times if the client of
* this code wanted to change the
* behaviour of the remote button.
*/

func (r *simpleRemoteControl) setCommand(command iCommand) {
r.slot = command
}

/**
* This method is called when the
* button is pressed. All we do is take
* the current command bound to the
* slot and call its execute() method.
*/

func (r *simpleRemoteControl) buttonWasPressed() {
r.slot.execute()
}

4. Testing the simple remote control

func main() {
/**
* The remote is our Invoker;
* it will be passed a
* command object that can
* be used to make requests.
*/

remoteControl := &simpleRemoteControl{}

/**
* Now we create a Light
* object, this will be the
* Receiver of the request
*/

light := &light{}

/**
* Create a command and
* pass the Receiver to it.
*/

lightOnCommand := newLightOnCommand(light)

/**
* Pass the command to the Invoker
*/

remoteControl.setCommand(lightOnCommand)

/**
* We simulate the button being pressed.
*/

remoteControl.buttonWasPressed()
}

5. Output

Light was turned on

6. Taking another command implementation: GarageDoorOpenCommand

The GarageDoor class looks like this

The GarageDoorOpenCommand means we need to up() the GarageDoor and turn the lights on (lightOn())

type garageDoorOpenCommand struct {
garage *garage
}

func newGarageDoorOpenCommand(g *garage) *garageDoorOpenCommand {
return &garageDoorOpenCommand{
garage: g,
}
}

func (g *garageDoorOpenCommand) execute() {
g.garage.up()
g.garage.lightOn()
}

Now showing the use of setCommand() method wherein we can change the command implementation at runtime

func main() {

remoteControl := &remoteControl{}

light := &light{}

lightOnCommand := newLightOnCommand(light)

remoteControl.setCommand(lightOnCommand)

remoteControl.buttonWasPressed()

garage := &garage{}

garageDoorOpenCommand := newGarageDoorOpenCommand(garage)

/**
* Pass the new command to the invoker
*/

remoteControl.setCommand(garageDoorOpenCommand)

remoteControl.buttonWasPressed()
}

Output:

Light was turned on
The Garage is open
The Garage Lights are on
  • In terms of class diagram
Final UML Diagram

Problem Statement 3:

  • We did Simple Remote which had just one slot. We need to assign commands to the slots in the actual remote, each having an “on” and “off” button.

Solution:

  • Assign commands to the remote something like this:
onCommands[0] = onCommand 
offCommands[0] = offCommand
  • Implementing the Remote Control
/**
* the remote is going to
* handle seven On and Off commands, which
* we’ll hold in corresponding arrays.
*/

type remoteControl struct {
onCommands, offCommands []iCommand
}

func newRemoteControl() *remoteControl {
return &remoteControl{
onCommands: make([]iCommand, 7),
offCommands: make([]iCommand, 7),
}
}

/**
* The setCommand() method takes a slot position
* and an On and Off command to be stored in
* that slot. It puts these commands in the on and
* off arrays for later use
*/

func (r *remoteControl) setCommand(slot int, onCommand, offCommand iCommand) {
r.onCommands[slot] = onCommand
r.offCommands[slot] = offCommand
}

/**
* When an On or Off button is
* pressed, the hardware takes
* care of calling the corresponding
* methods onButtonWasPushed() or
* offButtonWasPushed().
*/
func (r *remoteControl) onButtonWasPushed(slot int) {
fmt.Println("*****")
r.onCommands[slot].execute()
fmt.Println("*****")
}

func (r *remoteControl) offButtonWasPushed(slot int) {
fmt.Println("*****")
r.offCommands[slot].execute()
fmt.Println("*****")
}

/**
* Implementing String() to print out each slot and its
* corresponding command.
*/

func (r *remoteControl) String() string {
s := fmt.Sprintf("\n------ Remote Control -------\n")

for i := range r.onCommands {
if r.onCommands[i] == nil {
continue
}

onClass := r.getClassName(r.onCommands[i])
offClass := r.getClassName(r.offCommands[i])
s += fmt.Sprintf("[slot %d] %s %s\n", i, onClass, offClass)
}
s += fmt.Sprintf("-----------------------------\n")

return s
}

func (r *remoteControl) getClassName(myVar interface{}) string {
if t := reflect.TypeOf(myVar); t.Kind() == reflect.Ptr {
return t.Elem().Name()
} else {
return t.Name()
}
}
  • Implementing few Commands
/**
* The LightOffCommand works exactly
* the same way as the LightOnCommand,
* except that we are binding the receiver
* to a different action: the off() method.
*/

type lightOffCommand struct {
light *light
}

func newLightOffCommand(l *light) *lightOffCommand {
return &lightOffCommand{
light: l,
}
}

func (l *lightOffCommand) execute() {
l.light.off()
}
/**
* Just like the LightOnCommand, we get
* passed the instance of the stereo we
* are going to be controlling and we store
* it in a local instance variable.
*/

type stereoOnCommand struct {
stereo *stereo
}

func newStereoOnCommand(s *stereo) *stereoOnCommand {
return &stereoOnCommand{
stereo: s,
}
}

/**
* To carry out this request, we need to call three
* methods on the stereo: first, turn it on, then set
* it to play the CD, and finally set the volume to 11.
*/

func (s *stereoOnCommand) execute() {
s.stereo.on()
s.stereo.setCD()
s.stereo.setVolume(11)
}
  • Putting the Remote Control to test
func main() {
remoteControl := newRemoteControl()

// Instantiate all the devices
livingRoomLight := &light{"Living Room"}
kitchenLight := &light{"Kitchen"}
ceilingFan := &ceilingFan{"Living Room"}
garage := &garage{}
stereo := &stereo{"Living Room"}

// Instantiate all the Command Objects
livingRoomLightOn := newLightOnCommand(livingRoomLight)
livingRoomLightOff := newLightOffCommand(livingRoomLight)
kitchenLightOn := newLightOnCommand(kitchenLight)
kitchenLightOff := newLightOffCommand(kitchenLight)

ceilingFanOn := newCeilingFanOnCommand(ceilingFan)
ceilingFanOff := newCeilingFanOffCommand(ceilingFan)

stereoOnWithCD := newStereoOnCommand(stereo)
stereoOff := newStereoOffCommand(stereo)

garageDoorOpen := newGarageDoorOpenCommand(garage)
garageDoorClose := newGarageDoorCloseCommand(garage)

// Load commands into the remote slots
remoteControl.setCommand(0, livingRoomLightOn, livingRoomLightOff)
remoteControl.setCommand(1, kitchenLightOn, kitchenLightOff)
remoteControl.setCommand(2, ceilingFanOn, ceilingFanOff)
remoteControl.setCommand(3, stereoOnWithCD, stereoOff)
remoteControl.setCommand(4, garageDoorOpen, garageDoorClose)

fmt.Println(remoteControl)

// We step through each slot and push its On and Off button.
remoteControl.onButtonWasPushed(0)
remoteControl.offButtonWasPushed(0)
remoteControl.onButtonWasPushed(1)
remoteControl.offButtonWasPushed(1)
remoteControl.onButtonWasPushed(2)
remoteControl.offButtonWasPushed(2)
remoteControl.onButtonWasPushed(3)
remoteControl.offButtonWasPushed(3)
remoteControl.onButtonWasPushed(4)
remoteControl.offButtonWasPushed(4)

}
  • Run the code!
------ Remote Control -------
[slot 0] lightOnCommand lightOffCommand
[slot 1] lightOnCommand lightOffCommand
[slot 2] ceilingFanOnCommand ceilingFanOffCommand
[slot 3] stereoOnCommand stereoOffCommand
[slot 4] garageDoorOpenCommand garageDoorCloseCommand
-----------------------------
*****
Living Room light is turned on
*****
*****
Living Room light is turned off
*****
*****
Kitchen light is turned on
*****
*****
Kitchen light is turned off
*****
*****
Living Room ceiling fan is turned on
*****
*****
Living Room ceiling fan is turned off
*****
*****
Living Room stereo is on
Living Room stereo is set for CD input
Living Room stereo volume set to 11
*****
*****
Living Room stereo is off
*****
*****
The Garage is open
The Garage Lights are on
*****
*****
The Garage Lights are off
The Garage is down
*****

Problem Statement 4:

  • Implementing a combo : What if I want to push one button to turn the living room lights and ceiling fan on and the stereo fired up? Let’s call this LivingRoomOnCombo

Solution:

  • We could implement a LivingRoomOnComboCommand and put the devices to execute their corresponding methods in the LivingRoomOnComboCommand’s execute() method.
  • Bu with this, we are essentially “hardcoding” the combo mode.
  • Let’s make a MacroCommand, wherein we can decide dynamically which Commands we want to go into the LivingRoomOnComboCommand/LivingRoomOffComboCommand.
  • To illustrate, make a new kind of Command that takes an array of commands and execute them.
type macroCommand struct {
commands []iCommand
}

func newMacroCommand(c []iCommand) *macroCommand {
return &macroCommand{
commands: c,
}
}
func (m * macroCommand) addCommand(c iCommand) {
m.commands = append(m.commands, c)
}
func (m *macroCommand) execute() {
for _, command := range m.commands {
command.execute()
}
}
  • We create two arrays, one for the On commands and one for the Off commands, and load them with the corresponding commands:
livingRoomOnCommands := []iCommand{livingRoomLightOn, ceilingFanOn, stereoOnWithCD}
livingRoomOffCommands := []iCommand{livingRoomLightOff, ceilingFanOff, stereoOff}
livingRoomMacroOnCommand := newMacroCommand(livingRoomOnCommands)
livingRoomMacroOffCommand := newMacroCommand(livingRoomOffCommands)
  • Then we assign MacroCommand to a slot:
remoteControl.setCommand(5, livingRoomMacroOnCommand, livingRoomMacroOffCommand)fmt.Println("---Pushing Macro On---")
remoteControl.onButtonWasPushed(5)

fmt.Println("---Pushing Macro Off---")
remoteControl.offButtonWasPushed(5)
  • Run the Code!
---Pushing Macro On---
*****
Living Room light is turned on
Living Room ceiling fan is turned on
Living Room stereo is on
Living Room stereo is set for CD input
Living Room stereo volume set to 11
*****
---Pushing Macro Off---
*****
Living Room light is turned off
Living Room ceiling fan is turned off
Living Room stereo is off
*****

BIG PICTURE

  • Use the Command pattern when you want to execute queue operations, schedule their execution, or execute them remotely.

Imagine a job queue: you add commands to the queue on one end, and on the other end sit a group of threads. Threads run the following script: they remove a command from the queue, call its execute() method, wait for the call to finish, then discard the command object and retrieve a new one.

  • Use the Command pattern when you want to parameterise objects with operations and you don’t have a common interface.

The Command pattern can turn a specific method call into a stand-alone object. This change opens up a lot of interesting uses: you can pass commands as method arguments, store them inside other objects, switch linked commands at runtime, etc.

Join FAUN: Website 💻|Podcast 🎙️|Twitter 🐦|Facebook 👥|Instagram 📷|Facebook Group 🗣️|Linkedin Group 💬| Slack 📱|Cloud Native News 📰|More.

If this post was helpful, please click the clap 👏 button below a few times to show your support for the author 👇

--

--