7.6. Command

7.6.1. Rationale

  • EN: Command

  • PL: Polecenie

  • Type: object

7.6.2. Use Cases

  • 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

  • Source 1

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

7.6.4. Design

  • 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

../../_images/designpatterns-command-gof.png

7.6.5. Implementation

../../_images/designpatterns-command-usecase.png

Command pattern:

from abc import ABCMeta, abstractmethod
from dataclasses import dataclass


class Command(metaclass=ABCMeta):
    @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 ABCMeta, abstractmethod
from dataclasses import dataclass, field


class Command(metaclass=ABCMeta):
    @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 ABCMeta, abstractmethod
from dataclasses import dataclass, field
from typing import Optional


class Command(metaclass=ABCMeta):
    @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: Optional[str] = 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

7.6.6. References

1

https://medium.com/design-patterns-in-python/command-design-pattern-in-python-2f15b09f3774

7.6.7. Assignments