diff --git a/patterns/behavioral/command.py b/patterns/behavioral/command.py index e2b88394..c989d2a9 100644 --- a/patterns/behavioral/command.py +++ b/patterns/behavioral/command.py @@ -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): + 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 """