6.6. Command

  • EN: Command

  • PL: Polecenie

  • Type: object

The Command design pattern is a behavioral design pattern that turns a request into a stand-alone object that contains all information about the request. This transformation lets you pass requests as a method arguments, delay or queue a request's execution, and support undoable operations.

In Python, we can implement the Command pattern using classes. Here's a simple example:

First, we define a Command class that declares an abstract execute method:

>>> class Command:
...     def execute(self):
...         pass

Then, we define a ConcreteCommand class that implements the execute method:

>>> class ConcreteCommand(Command):
...     def __init__(self, receiver):
...         self._receiver = receiver
...
...     def execute(self):
...         self._receiver.action()

The Receiver class has the actual business logic that should be performed:

>>> class Receiver:
...     def action(self):
...         print("Receiver action is being performed.")

Finally, we have an Invoker class that calls the command:

>>> class Invoker:
...     def __init__(self, command):
...         self._command = command
...
...     def call(self):
...         self._command.execute()
...
>>> receiver = Receiver()
>>> command = ConcreteCommand(receiver)
>>> invoker = Invoker(command)
...
>>> invoker.call()
Receiver action is being performed.

In this example, the Invoker calls the ConcreteCommand, which performs an action on the Receiver.

6.6.1. Pattern

  • Receiver — The Object that will receive and execute the command

  • Invoker — Which will send the command to the receiver

  • Command Object — Itself, which implements an execute, or action method, and contains all required information

  • Client — The main application or module which is aware of the Receiver, Invoker and Commands

  • GUI Buttons, menus

  • Macro recording

  • Multi level undo/redo (See Tutorial)

  • Networking — send whole command objects across a network, even as a batch

  • Parallel processing or thread pools

  • Transactional behaviour — Rollback whole set of commands, or defer till later

  • Wizards

../../_images/designpatterns-command-pattern.png

6.6.2. Problem

design-patterns/behavioral/img/designpatterns-command-problem.png

class Button:
    label: str

    def set_label(self, name):
        self.label = name

    def get_label(self):
        return self.label

    def click(self):
        ...


if __name__ == '__main__':
    button = Button()
    button.set_label('My Button')
    button.click()

6.6.3. Solution

../../_images/designpatterns-command-solution.png

Command pattern:

from abc import ABC, abstractmethod
from dataclasses import dataclass


class Command(ABC):
    @abstractmethod
    def execute(self) -> None:
        pass


class Button:
    label: str
    command: Command

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

    def set_label(self, name):
        self.label = name

    def get_label(self):
        return self.label

    def click(self):
        self.command.execute()


class CustomerService:
    def add_customer(self) -> None:
        print('Add customer')


@dataclass
class AddCustomerCommand(Command):
    service: CustomerService

    def execute(self) -> None:
        self.service.add_customer()


if __name__ == '__main__':
    service = CustomerService()
    command = AddCustomerCommand(service)
    button = Button(command)
    button.click()
    # Add customer

Composite commands (Macros):

from abc import ABC, abstractmethod
from dataclasses import dataclass, field


class Command(ABC):
    @abstractmethod
    def execute(self) -> None:
        pass


class ResizeCommand(Command):
    def execute(self) -> None:
        print('Resize')

class BlackAndWhiteCommand(Command):
    def execute(self) -> None:
        print('Black And White')


@dataclass
class CompositeCommand(Command):
    commands: list[Command] = field(default_factory=list)

    def add(self, command: Command) -> None:
        self.commands.append(command)

    def execute(self) -> None:
        for command in self.commands:
            command.execute()


if __name__ == '__main__':
    composite = CompositeCommand()
    composite.add(ResizeCommand())
    composite.add(BlackAndWhiteCommand())
    composite.execute()
    # Resize
    # Black And White

Undoable commands:

from abc import ABC, abstractmethod
from dataclasses import dataclass, field


class Command(ABC):
    @abstractmethod
    def execute(self) -> None:
        pass

class UndoableCommand(Command):
    @abstractmethod
    def unexecute(self) -> None:
        pass


@dataclass
class History:
    commands: list[UndoableCommand] = field(default_factory=list)

    def push(self, command: UndoableCommand) -> None:
        self.commands.append(command)

    def pop(self):
        return self.commands.pop()

    def size(self) -> int:
        return len(self.commands)


@dataclass
class HtmlDocument:
    content: str = ''

    def set_content(self, content):
        self.content = content

    def get_content(self):
        return self.content


@dataclass
class BoldCommand(UndoableCommand):
    document: HtmlDocument
    history: History = History()
    previous_content: str | None = None

    def unexecute(self) -> None:
        self.document.set_content(self.previous_content)

    def apply(self, content):
        return f'<b>{content}</b>'

    def execute(self) -> None:
        current_content = self.document.get_content()
        self.previous_content = current_content
        self.document.set_content(self.apply(current_content))
        self.history.push(self)


@dataclass
class UndoCommand(Command):
    history: History

    def execute(self) -> None:
        if self.history.size() > 0:
            self.history.pop().unexecute()


if __name__ == '__main__':
    history = History()
    document = HtmlDocument('Hello World')

    # This should be onButtonClick or KeyboardShortcut
    BoldCommand(document, history).execute()
    print(document.get_content())
    # <b>Hello World</b>

    # This should be onButtonClick or KeyboardShortcut
    UndoCommand(history).execute()
    print(document.get_content())
    # Hello World

6.6.4. Further Reading

6.6.5. Assignments