Using Python types for fun and profit: the Command Pattern

Introduction

The command pattern is a behavioral design pattern. It is used by an Invoker to perform one action or a series of actions on objects, called Receivers. The Invoker is unaware of the nature of the receiving objects, it just knows about the command. That way we achieve a loose coupling between Invoker and Receiver.

And it looks like this:

A short breakdown of this diagram:

  • The Invoker is the object want to execute a command or a series of commands on some unknown objects, that is, the Invoker might be unaware of them.
  • The Command interface. Many classes might implement this interface.
  • In this example there is just one class implementing the Command interface, CommandA, which works on ReceiverA. As you can see, the Invoker and the Receiver are not coupled, and the only communication between the two goes via the Command.

Note: I am using Python3.12 for this example. This means that some or all code might not work on earlier versions

Implementation in Python

We will start by defining the Command class. This class has one method with no implementation, therefore it could be called an interface:

class Command:
    def execute(self):
        pass

Next we define the first concrete class, the PrintCommand class:

class PrintCommand(Command):
    _text: str = None

    def __init__(self, text: str):
        self._text = text

    def execute(self):
        print(self._text)

All the execute() does here is print out some pre-defined text

We also implement a second class, the MultiplyCommand which doubles a given number and prints it out:

class MultiplyCommand(Command):
    _num: int = None

    def __init__(self, num: int):
        self._num = num

    def execute(self):
        result = self._num * 2
        print(f"Result is {result}")

Some notes here:

  • Both classes extend from the Command class and implement the execute() method.
  • The execute() method does not return anything, which would be difficult in this example. The PrintCommand really is a void, but the MultiplyCommand could return a number if needed. Whether you want a return value from execute() method depends on the situation and the use-case.

To show the strength of this pattern, we will implement an Invoker class which can execute a series of commands:

class Invoker:
    def __init__(self):
        self._history: list[Command] = []

    def add_command(self, command: Command):
        self._history.append(command)

    def execute_commands(self):
        for command in self._history:
            command.execute()

Line by line:

  1. In the constructor, we set the _history to an empty list. The _history field is of type list[Command]
  2. The add_command() simply adds a command to the history.
  3. In the execute_commands() methods, we iterate over every command in the _history, and executes it.

Here the type-annotations really come into their own, since they really make the intent of the code much clearer.

Time to test

Now we can test our implementation:

if __name__ == "__main__":
    invoker: Invoker = Invoker()
    invoker.add_command(PrintCommand('Hello'))
    invoker.add_command(PrintCommand('World'))
    invoker.add_command(MultiplyCommand(2))
    invoker.execute_commands()

A short description:

  1. We instantiat an Invoker object.
  2. Next we add two PrintCommand objects and one MultiplyCommand object to it. Since they all derive from the Command class, we can add them to the _history of the Invoker class.
  3. Then we simply execute all these commands.

Conclusion

As I experienced with many design patterns, implementing this pattern in Python is very easy, and with the type-annotation this is not at the expense of clarity or readability.

Leave a Reply

Your email address will not be published. Required fields are marked *