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:
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