Skip to content

Command

The purpose of the Command behavioral pattern is to separate objects that send requests from objects that accept their results. It does this by entering an object representing the command that executes such a process. Optionally, the command may also be able to undo the result of such a process.

By wrapping the process execution into a separate object, we keep the SOLID rules, removing the potential strong link between the sender and the recipient of the request result.

Design and example

The main element we need to create is a common object representing the process execution. Optionally, it may be possible to undo the execution of the command.

The objects needed to execute the command most often come in the implementation constructor.

The example below uses the Command interface, along with two implementations: ChangeFileNameCommand i RemoveEmptyLinesCommand. It is possible to undo the invocation of the ChangeFileNameCommand command with the cancel method.

class Command:
    def apply(self):
        pass

    def cancel(self):
        pass
class PythonFile:
    def __init__(self, file_name, lines_content):
        self.file_name = file_name
        self.lines_content = lines_content

    def add_line(self, line):
        self.lines_content.append(line)

    def __str__(self):
        return f"file_name={self.file_name}\n{self.lines_content}"
class ChangeFileNameCommand(Command):
    def __init__(self, python_file, new_name):
        self._python_file = python_file
        self._new_name = new_name
        self._previous_name = None

    def apply(self):
        self._previous_name = self._python_file.file_name
        self._python_file.file_name = self._new_name
        print(f"File name changed to: {self._new_name}")

    def cancel(self):
        if self._previous_name:
            self._python_file.file_name = self._previous_name
            self._previous_name = None
class RemoveEmptyLinesCommand(Command):
    def __init__(self, python_file):
        self._python_file = python_file

    def apply(self):
        self._python_file.lines_content = [line for line in self._python_file.lines_content if line.strip()]

    def cancel(self):
        print('Operation not supported!')

The client that uses the result of executing the commands:

def main():
    python_file = PythonFile('test.py', ["import this", "    ", "print('That's all folks!)", ""])

    change_file_name_command = ChangeFileNameCommand(python_file, 'zen.py')
    remove_empty_lines_command = RemoveEmptyLinesCommand(python_file)

    change_file_name_command.apply()
    remove_empty_lines_command.apply()

    print(python_file)

    change_file_name_command.cancel()
    remove_empty_lines_command.cancel()

    print(python_file)


if __name__ == '__main__':
    main()

The result of the program will be:

File name changed to: zen.py
file_name=zen.py
['import this', "print('That's all folks!)"]
Operation not supported!
file_name=test.py
['import this', "print('That's all folks!)"]