Practical Design Patterns Part 1: The Command Pattern

nature-3616194_960_720


Introduction

Most software these days support undoing/redoing actions. For example, text editors support undoing what we have written, file managers support undoing file creation/deletion etc. It’s a good UX practice to make software forgiving and allow users to undo their actions.

I always wondered how was this implemented?  On a critical analysis, it seems as if the object on which the actions are taking place receives them from somewhere else, like a command. The object that invokes the actions, the object on which the actions are performed and the actions themselves are decoupled. This is exactly why the Command Pattern exists.


The Command Pattern

The Command Pattern is a behavioral design pattern, i.e it tries to simplify communication among objects and improve flexibility, in which an operation is encapsulated with all the information it needs to perform the action in an object.

What the above statement simply means is that we create a class that represents an operation and its instance holds all the data and methods that are needed to perform the action.

Advantages of The Command Pattern

  1. The Invoker and the actions can be decoupled. The invoker doesn’t need to know how the operations are implemented.
  2. It is easy to change an operation and add new operations.
  3. Multiple commands can be grouped together to execute them in order. (This is especially helpful in designing systems that support multi-level undo.)


Implementation and Usage

We are going to implement a simple home control system that supports a certain set of actions that the user can perform and an undo feature to allow a user to undo her actions.

Overview of the system looks as follows:

  1. Home is the class on which the actions are performed.
  2. SmartRemote is the class that allows performing actions on the Home.
  3. FanOnCommand, FanoffCommand, FanSlowCommand, FanFastCommand are 4 commands that are supported.
  4. A user can undo her actions.

Following is the implementation of the system explained above and showcases the power of the command pattern.

The example is complex enough to properly showcase the functionality and advantages of the design pattern.


class Home(object):
def __init__(self):
self._fan_state = "OFF"
self._fan_speed = 0
def turn_fan_on(self):
self._fan_state = "ON"
def turn_fan_off(self):
self._fan_state = "OFF"
def increase_fan_speed(self, offset=1):
self._fan_speed = self._fan_speed + offset
def decrease_fan_speed(self, offset=1):
self._fan_speed = self._fan_speed – offset
def get_fan_state(self):
return self._fan_state
def get_fan_speed(self):
return self._fan_speed
class SmartRemote(object):
def __init__(self, home_instance):
# Our smart home
self.home = home_instance
# stores a log of all the actions that we have performed
self.command_stack = []
# Decoupled commands
# Notice how the remote is not aware of how the commands are actually implemented.
# It is just responsible for invoking the right command.
self.fan_on_command_class = FanOnCommand
self.fan_off_command_class = FanOffCommand
self.fan_slow_command_class = FanSlowCommand
self.fan_fast_command_class = None
"""
Since commands are completely decoupled from the invoker, we can easily update the commands without
changing the invoker.
"""
def set_fan_fast_command(self, fan_fast_command):
self.fan_fast_command_class = fan_fast_command
"""
Invokes the fan on command and adds the command to the stack
"""
def fan_on(self):
if not self.fan_on_command_class:
raise NotImplementedError("This operation is not supported")
fan_on_command = self.fan_on_command_class()
fan_on_command.execute(self.home)
self.command_stack.append(fan_on_command)
"""
Invokes the fan off command and adds the command to the stack
"""
def fan_off(self):
if not self.fan_off_command_class:
raise NotImplementedError("This operation is not supported")
fan_off_command = self.fan_off_command_class()
fan_off_command.execute(self.home)
self.command_stack.append(fan_off_command)
"""
Invokes the fan slow command and adds the command to the stack
"""
def fan_slow(self):
if not self.fan_slow_command_class:
raise NotImplementedError("This operation is not supported")
fan_slow_command = self.fan_slow_command_class()
fan_slow_command.execute(self.home)
self.command_stack.append(fan_slow_command)
"""
Invokes the fan fast command and adds the command to the stack
"""
def fan_fast(self):
if not self.fan_fast_command_class:
raise NotImplementedError("This operation is not supported")
fan_fast_command = self.fan_fast_command_class()
fan_fast_command.execute(self.home)
self.command_stack.append(fan_fast_command)
"""
Undo the last action that was performed.
A major benefit of command pattern is that logging actions/commands becomes easy.
This can be easily extended to create something like a undo/redo feature.
"""
def undo_last_action(self):
last_command = self.command_stack.pop()
last_command.undo(home)
class BaseCommand(object):
def execute(self, home_instance):
raise NotImplementedError
def undo(self, home_instance):
raise NotImplementedError
class FanOnCommand(BaseCommand):
def __init__(self):
self._prev_fan_state = None
def execute(self, home_instance):
home_instance.turn_fan_on()
print ("Fan state is now {0}".format(home_instance.get_fan_state()))
def undo(self, home_instance):
home_instance.turn_fan_off()
print("UNDO: Fan state is now {0}".format(home_instance.get_fan_state()))
class FanOffCommand(BaseCommand):
def __init__(self):
self._prev_fan_state = None
def execute(self, home_instance):
home_instance.turn_fan_off()
print("Fan state is now {0}".format(home.get_fan_state()))
def undo(self, home_instance):
home_instance.turn_fan_on()
print("UNDO: Fan state is now {0}".format(home_instance.get_fan_state()))
class FanFastCommand(BaseCommand):
def __init__(self):
self._prev_fan_speed = None
def execute(self, home_instance):
home_instance.increase_fan_speed(offset=1)
print("Fan speed is now {0}".format(home_instance.get_fan_speed()))
def undo(self, home_instance):
home_instance.decrease_fan_speed(offset=1)
print("UNDO: Fan speed is now {0}".format(home_instance.get_fan_speed()))
class FanSlowCommand(BaseCommand):
def execute(self, home_instance):
home_instance.decrease_fan_speed(offset=1)
print("Fan speed is now {0}".format(home_instance.get_fan_speed()))
def undo(self, home_instance):
home_instance.increase_fan_speed(offset=1)
print("UNDO: Fan speed is now {0}".format(home_instance.get_fan_speed()))
if __name__ == '__main__':
home = Home()
smart_remote = SmartRemote(home)
# Since the commands and the invoker are decoupled, we can easily
# change the commands.
smart_remote.fan_fast_command_class = FanFastCommand
smart_remote.fan_on()
smart_remote.fan_fast()
smart_remote.fan_fast()
smart_remote.fan_slow()
# Notice how easy it is to undo actions.
# This is also useful in keeping a log of actions that have taken place.
smart_remote.undo_last_action()
smart_remote.undo_last_action()
# Like a responsible citizen always turn the fan off when leaving the room.
smart_remote.fan_off()

Conclusion

The Command Pattern is a behavioral design pattern that allows encapsulating actions in an object that has all the information available with it to perform the action.

This makes it a good candidate to implement systems that have GUI driven actions, transactional behaviors, multi-level undo etc.

Practical Design Patterns is an attempt to cover the most common design patterns with strong and practical explanation and implementation. Stay tuned for the next post in the series.

That’s all, folks!

 

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.