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
- The Invoker and the actions can be decoupled. The invoker doesn’t need to know how the operations are implemented.
- It is easy to change an operation and add new operations.
- 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:
- Home is the class on which the actions are performed.
- SmartRemote is the class that allows performing actions on the Home.
- FanOnCommand, FanoffCommand, FanSlowCommand, FanFastCommand are 4 commands that are supported.
- 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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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