Инкапсуляция действий как объектов. Реализация отмены/повтора операций. Команды через callable-объекты и functools.partial.
Команда превращает действие в объект, который можно сохранить, отменить и повторить.
Command — поведенческий паттерн, инкапсулирующий запрос как объект, позволяя параметризовать клиентов с разными запросами и поддерживать undo/redo.
# ❌ ПЛОХО: Логика выполнения размазана
class TextEditor:
def insert_text(self, text: str):
print(f"Inserting: {text}")
def delete_text(self, start: int, end: int):
print(f"Deleting {start}:{end}")
# Клиент знает о всех методах
editor = TextEditor()
editor.insert_text("Hello")
editor.delete_text(0, 5)
# Невозможно отменить, сохранить, поставить в очередь# ✅ ХОРОШО: Каждое действие — объект
from abc import ABC, abstractmethod
class Command(ABC):
@abstractmethod
def execute(self):
pass
@abstractmethod
def undo(self):
pass
class TextEditor:
def __init__(self):
self._text = ""
def insert(self, text: str, position: int):
self._text = self._text[:position] + text + self._text[position:]
def delete(self, start: int, end: int):
self._text = self._text[:start] + self._text[end:]
@property
def text(self) -> str:
return self._text
class InsertCommand(Command):
def __init__(self, editor: TextEditor, text: str, position: int):
self._editor = editor
self._text = text
self._position = position
self._executed = False
def execute(self):
self._editor.insert(self._text, self._position)
self._executed = True
def undo(self):
if self._executed:
self._editor.delete(self._position, self._position + len(self._text))
self._executed = False
class DeleteCommand(Command):
def __init__(self, editor: TextEditor, start: int, end: int):
self._editor = editor
self._start = start
self._end = end
self._deleted_text = None
def execute(self):
self._deleted_text = self._editor.text[self._start:self._end]
self._editor.delete(self._start, self._end)
def undo(self):
if self._deleted_text:
self._editor.insert(self._deleted_text, self._start)
self._deleted_text = None
# Использование
editor = TextEditor()
history = []
# Выполнение команд
cmd1 = InsertCommand(editor, "Hello", 0)
cmd1.execute()
history.append(cmd1)
cmd2 = InsertCommand(editor, " World", 5)
cmd2.execute()
history.append(cmd2)
print(editor.text) # Hello World
# Undo
history.pop().undo() # Undo: InsertCommand
print(editor.text) # Hello
history.pop().undo() # Undo: InsertCommand
print(editor.text) # ""# ✅ Проще: функции как команды
from typing import Callable, List
class CommandHistory:
def __init__(self):
self._history: List[Callable] = []
self._undo_stack: List[Callable] = []
def execute(self, command: Callable, undo: Callable = None):
command()
self._history.append(command)
if undo:
self._undo_stack.append(undo)
self._redo_stack = [] # Сброс redo при новом действии
def undo(self):
if self._undo_stack:
undo_fn = self._undo_stack.pop()
undo_fn()
# Использование
editor = TextEditor()
history = CommandHistory()
def insert(): editor.insert("X", 0)
def undo_insert(): editor.delete(0, 1)
history.execute(insert, undo_insert)
history.undo()# ✅ Pythonic: partial для команд
from functools import partial
from typing import Protocol
class Command(Protocol):
def __call__(self): ...
def undo(self): ...
class TextEditor:
def __init__(self):
self._text = ""
self._history: List[Command] = []
def insert(self, text: str, position: int):
self._text = self._text[:position] + text + self._text[position:]
def delete(self, start: int, end: int):
self._text = self._text[:start] + self._text[end:]
@property
def text(self) -> str:
return self._text
def execute(self, command: Command):
command()
self._history.append(command)
def undo(self):
if self._history:
self._history.pop().undo()
class InsertCommand:
def __init__(self, editor: TextEditor, text: str, position: int):
self.editor = editor
self.text = text
self.position = position
def __call__(self):
self.editor.insert(self.text, self.position)
def undo(self):
self.editor.delete(self.position, self.position + len(self.text))
# Использование
editor = TextEditor()
editor.execute(InsertCommand(editor, "Hello", 0))
editor.execute(InsertCommand(editor, " World", 5))
print(editor.text) # Hello World
editor.undo()
print(editor.text) # Hellofrom typing import List, Optional
class CommandHistory:
def __init__(self):
self._undo_stack: List[Command] = []
self._redo_stack: List[Command] = []
def execute(self, command: Command):
command.execute()
self._undo_stack.append(command)
self._redo_stack.clear() # Сброс redo при новом действии
def undo(self) -> bool:
if not self._undo_stack:
return False
command = self._undo_stack.pop()
command.undo()
self._redo_stack.append(command)
return True
def redo(self) -> bool:
if not self._redo_stack:
return False
command = self._redo_stack.pop()
command.execute()
self._undo_stack.append(command)
return True
@property
def can_undo(self) -> bool:
return len(self._undo_stack) > 0
@property
def can_redo(self) -> bool:
return len(self._redo_stack) > 0
# Использование
history = CommandHistory()
history.execute(InsertCommand(editor, "A", 0))
history.execute(InsertCommand(editor, "B", 1))
history.execute(InsertCommand(editor, "C", 2))
# editor.text = "ABC"
history.undo() # "AB"
history.undo() # "A"
history.redo() # "AB"
history.redo() # "ABC"class MacroCommand(Command):
"""Команда из последовательности команд"""
def __init__(self, commands: List[Command] = None):
self._commands = commands or []
def add(self, command: Command):
self._commands.append(command)
def execute(self):
for cmd in self._commands:
cmd.execute()
def undo(self):
# Отменяем в обратном порядке
for cmd in reversed(self._commands):
cmd.undo()
# Использование
macro = MacroCommand()
macro.add(InsertCommand(editor, "Hello", 0))
macro.add(InsertCommand(editor, " ", 5))
macro.add(InsertCommand(editor, "World", 6))
history.execute(macro)
# editor.text = "Hello World"
history.undo()
# editor.text = ""from contextlib import contextmanager
class Transaction:
"""Транзакция с откатом"""
def __init__(self, history: CommandHistory):
self._history = history
self._commands: List[Command] = []
def execute(self, command: Command):
command.execute()
self._commands.append(command)
def commit(self):
for cmd in self._commands:
self._history._undo_stack.append(cmd)
self._history._redo_stack.clear()
def rollback(self):
for cmd in reversed(self._commands):
cmd.undo()
@contextmanager
def transaction(history: CommandHistory):
tx = Transaction(history)
try:
yield tx
tx.commit()
except Exception:
tx.rollback()
raise
# Использование
with transaction(history) as tx:
tx.execute(InsertCommand(editor, "A", 0))
tx.execute(InsertCommand(editor, "B", 1))
# При ошибке — автоматический rollbackimport queue
import threading
class CommandQueue:
def __init__(self):
self._queue = queue.Queue()
self._worker = threading.Thread(target=self._process, daemon=True)
self._worker.start()
def enqueue(self, command: Command):
self._queue.put(command)
def _process(self):
while True:
command = self._queue.get()
command.execute()
self._queue.task_done()
# Использование
cmd_queue = CommandQueue()
cmd_queue.enqueue(InsertCommand(editor, "A", 0))
cmd_queue.enqueue(InsertCommand(editor, "B", 1))
# Команды выполняются в фоновом потокеfrom typing import Callable
class Button:
def __init__(self, label: str, command: Callable = None):
self.label = label
self.command = command
def click(self):
if self.command:
self.command()
class CopyCommand(Command):
def __init__(self, text_editor: TextEditor):
self._editor = text_editor
self._clipboard = ""
def execute(self):
self._clipboard = self._editor.text
print(f"Copied: {self._clipboard}")
def undo(self):
self._clipboard = ""
print("Clipboard cleared")
class PasteCommand(Command):
def __init__(self, text_editor: TextEditor, clipboard: str):
self._editor = text_editor
self._clipboard = clipboard
def execute(self):
self._editor.insert(self._clipboard, len(self._editor.text))
def undo(self):
self._editor.delete(len(self._editor.text) - len(self._clipboard), len(self._editor.text))
# Использование
editor = TextEditor()
copy_btn = Button("Copy", CopyCommand(editor).execute)
paste_btn = Button("Paste", PasteCommand(editor, "text").execute)
copy_btn.click() # Copied: ...from abc import ABC, abstractmethod
from datetime import datetime
from typing import Optional
class Task(ABC):
@abstractmethod
def execute(self):
pass
class SendEmailTask(Task):
def __init__(self, to: str, subject: str, body: str):
self.to = to
self.subject = subject
self.body = body
self.executed_at: Optional[datetime] = None
def execute(self):
print(f"Sending email to {self.to}: {self.subject}")
self.executed_at = datetime.now()
class TaskQueue:
def __init__(self):
self._tasks: List[Task] = []
def add(self, task: Task):
self._tasks.append(task)
def execute_all(self):
for task in self._tasks:
task.execute()
self._tasks.clear()
# Использование
queue = TaskQueue()
queue.add(SendEmailTask("user@example.com", "Welcome", "Hello!"))
queue.add(SendEmailTask("admin@example.com", "New user", "Registered"))
queue.execute_all()| Паттерн | Когда использовать | Pythonic-реализация |
|---|---|---|
| Command (класс) | Undo/redo, макросы, очереди | Класс с execute/undo |
| Command (callable) | Простые действия | Функции + partial |
| MacroCommand | Группировка команд | Список команд |
| Transaction | Атомарность операций | Контекстный менеджер |
Главный принцип: Команда инкапсулирует действие как объект, позволяя управлять его выполнением.
Изучите тему Iterator и Generator patterns для работы с последовательностями.
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.