8.8. 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.

8.8.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

8.8.2. Problem

>>> class Light:
...     def action(self, action: str):
...         if action == 'on':
...             print('Lights on')
...         elif action == 'off':
...             print('Lights off')
>>>
>>>
>>> light = Light()
>>>
>>> light.action('on')
Lights on
>>>
>>> light.action('off')
Lights off

8.8.3. Solution

>>> from typing import Protocol
>>>
>>>
>>> class Command(Protocol):
...     def execute(self): ...
>>>
>>>
>>> class Tasks:
...     commands: list[Command]
...
...     def __init__(self):
...         self.commands = []
...
...     def add(self, command: Command):
...         self.commands.append(command)
...
...     def run(self):
...         for command in self.commands:
...             command.execute()
>>>
>>>
>>> class LightsOnCommand:
...     def execute(self):
...         print('Lights on')
>>>
>>> class LightsOffCommand:
...     def execute(self):
...         print('Lights off')
>>>
>>>
>>> task = Tasks()
>>> task.add(LightsOnCommand())
>>> task.add(LightsOffCommand())
>>> task.add(LightsOnCommand())
>>> task.add(LightsOffCommand())
>>> task.add(LightsOnCommand())
>>> task.add(LightsOffCommand())
>>> task.run()
Lights on
Lights off
Lights on
Lights off
Lights on
Lights off

8.8.4. Case Study

Problem:

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()

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 = field(default_factory=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

8.8.5. Further Reading

8.8.6. Use Case - 1

>>> from typing import Protocol
>>>
>>>
>>> class Command(Protocol):
...     def execute(self): """Execute command"""
>>>
>>>
>>> class MorseCode:
...     commands: list[Command]
...
...     def __init__(self):
...         self.commands = []
...
...     def add(self, command: Command):
...         self.commands.append(command)
...
...     def send(self):
...         print('Sending message:')
...         for command in self.commands:
...             command.execute()
...         print('STOP')
>>> class A:
...     def execute(self):
...         print('.-', end=' ')
>>>
>>> class B:
...     def execute(self):
...         print('-...', end=' ')
>>>
>>> class C:
...     def execute(self):
...         print('-.-.', end=' ')
>>>
>>> class D:
...     def execute(self):
...         print('-..', end=' ')
>>>
>>> class E:
...     def execute(self):
...         print('.', end=' ')
>>>
>>> class F:
...     def execute(self):
...         print('..-.', end=' ')
>>>
>>> class G:
...     def execute(self):
...         print('--.', end=' ')
>>>
>>> class H:
...     def execute(self):
...         print('....', end=' ')
>>>
>>> class I:
...     def execute(self):
...         print('..', end=' ')
>>>
>>> class J:
...     def execute(self):
...         print('.---', end=' ')
>>>
>>> class K:
...     def execute(self):
...         print('-.-', end=' ')
>>>
>>> class L:
...     def execute(self):
...         print('.-..', end=' ')
>>>
>>> class M:
...     def execute(self):
...         print('--', end=' ')
>>>
>>> class N:
...     def execute(self):
...         print('-.', end=' ')
>>>
>>> class O:
...     def execute(self):
...         print('---', end=' ')
>>>
>>> class P:
...     def execute(self):
...         print('.--.', end=' ')
>>>
>>> class Q:
...     def execute(self):
...         print('--.-', end=' ')
>>>
>>> class R:
...     def execute(self):
...         print('.-.', end=' ')
>>>
>>> class S:
...     def execute(self):
...         print('...', end=' ')
>>>
>>> class T:
...     def execute(self):
...         print('-', end=' ')
>>>
>>> class U:
...     def execute(self):
...         print('..-', end=' ')
>>>
>>> class V:
...     def execute(self):
...         print('...-', end=' ')
>>>
>>> class W:
...     def execute(self):
...         print('.--', end=' ')
>>>
>>> class X:
...     def execute(self):
...         print('-..-', end=' ')
>>>
>>> class Y:
...     def execute(self):
...         print('-.--', end=' ')
>>>
>>> class Z:
...     def execute(self):
...         print('--..', end=' ')
>>> message = MorseCode()
>>>
>>> message.add(S())
>>> message.add(O())
>>> message.add(S())
>>>
>>> message.send()
Sending message:
... --- ... STOP

8.8.7. Assignments