Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 82 additions & 42 deletions patterns/behavioral/command.py
Original file line number Diff line number Diff line change
@@ -1,61 +1,101 @@
"""
Command pattern decouples the object invoking a job from the one who knows
how to do it. As mentioned in the GoF book, a good example is in menu items.
You have a menu that has lots of items. Each item is responsible for doing a
special thing and you want your menu item just call the execute method when
it is pressed. To achieve this you implement a command object with the execute
method for each menu item and pass to it.

*About the example
We have a menu containing two items. Each item accepts a file name, one hides the file
and the other deletes it. Both items have an undo option.
Each item is a MenuItem class that accepts the corresponding command as input and executes
it's execute method when it is pressed.

*TL;DR
Encapsulates all information needed to perform an action or trigger an event.
Object oriented implementation of callback functions.

*Examples in Python ecosystem:
Django HttpRequest (without `execute` method):
https://docs.djangoproject.com/en/2.1/ref/request-response/#httprequest-objects
Django HttpRequest (without execute method):
https://docs.djangoproject.com/en/2.1/ref/request-response/#httprequest-objects
"""

import os

class HideFileCommand:
"""
A command to hide a file given its name
"""

class MoveFileCommand:
def __init__(self, src, dest):
self.src = src
self.dest = dest
def __init__(self):
# an array of files hidden, to undo them as needed
self._hidden_files = []

def execute(self):
self.rename(self.src, self.dest)
def execute(self, filename):
print(f'hiding {filename}')
self._hidden_files.append(filename)

def undo(self):
self.rename(self.dest, self.src)
filename = self._hidden_files.pop()
print(f'un-hiding {filename}')


class DeleteFileCommand:
"""
A command to delete a file given its name
"""

def __init__(self):
# an array of deleted files, to undo them as needed
self._deleted_files = []

def execute(self, filename):
print(f'deleting {filename}')
self._deleted_files.append(filename)

def rename(self, src, dest):
print("renaming {} to {}".format(src, dest))
os.rename(src, dest)
def undo(self):
filename = self._deleted_files.pop()
print(f'restoring {filename}')


class MenuItem:
"""
The invoker class. Here it is items in a menu.
"""

def __init__(self, command):
self._command = command

def on_do_press(self, filename):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think names like on_do_press and on_undo_press are harder to read then rename, delete or whatever else

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right, but the invoker (here is the menu item) does not know what is going to do.

Am I right?

self._command.execute(filename)

def on_undo_press(self):
self._command.undo()


def main():
"""
>>> from os.path import lexists

>>> command_stack = [
... MoveFileCommand('foo.txt', 'bar.txt'),
... MoveFileCommand('bar.txt', 'baz.txt')
... ]

# Verify that none of the target files exist
>>> assert not lexists("foo.txt")
>>> assert not lexists("bar.txt")
>>> assert not lexists("baz.txt")

# Create empty file
>>> open("foo.txt", "w").close()

# Commands can be executed later on
>>> for cmd in command_stack:
... cmd.execute()
renaming foo.txt to bar.txt
renaming bar.txt to baz.txt

# And can also be undone at will
>>> for cmd in reversed(command_stack):
... cmd.undo()
renaming baz.txt to bar.txt
renaming bar.txt to foo.txt

>>> os.unlink("foo.txt")
>>> item1 = MenuItem(DeleteFileCommand())

>>> item2 = MenuItem(HideFileCommand())

# create a file named `test-file` to work with
>>> test_file_name = 'test-file'

# deleting `test-file`
>>> item1.on_do_press(test_file_name)
deleting test-file

# restoring `test-file`
>>> item1.on_undo_press()
restoring test-file

# hiding `test-file`
>>> item2.on_do_press(test_file_name)
hiding test-file

# un-hiding `test-file`
>>> item2.on_undo_press()
un-hiding test-file
"""


Expand Down