Перейти к содержанию

Команда

Также известен как: Действие, Транзакция, Action, Command

Команда — это поведенческий паттерн проектирования, который превращает запросы в объекты, позволяя передавать их как аргументы при вызове методов, ставить запросы в очередь, логировать их, а также поддерживать отмену операций.

Паттерн Команда

Проблема

Представьте, что вы работаете над программой текстового редактора. Дело как раз подошло к разработке панели управления. Вы создали класс красивых Кнопок и хотите использовать его для всех кнопок приложения, начиная от панели управления, заканчивая простыми кнопками в диалогах.

Проблема, которую решает Команда

Все кнопки приложения унаследованы от одного класса.

Все эти кнопки, хоть и выглядят схоже, но делают разные вещи. Поэтому возникает вопрос: куда поместить код обработчиков кликов по этим кнопкам? Самым простым решением было бы создать подклассы для каждой кнопки и переопределить в них метод действия под разные задачи.

Множество подклассов кнопок

Множество подклассов кнопок.

Но скоро стало понятно, что такой подход никуда не годится. Во-первых, получается очень много подклассов. Во-вторых, код кнопок, относящийся к графическому интерфейсу, начинает зависеть от классов бизнес-логики, которая довольно часто меняется.

Несколько классов дублируют одну и ту же функциональность

Несколько классов дублируют одну и ту же функциональность.

Но самое обидное ещё впереди. Ведь некоторые операции, например, «сохранить», можно вызывать из нескольких мест: нажав кнопку на панели управления, вызвав контекстное меню или просто нажав клавиши Ctrl + S. Когда в программе были только кнопки, код сохранения имелся только в подклассе SaveButton. Но теперь его придётся продублировать ещё в два класса.

Решение

Хорошие программы обычно структурированы в виде слоёв. Самый распространённый пример — слои пользовательского интерфейса и бизнес-логики. Первый всего лишь рисует красивую картинку для пользователя. Но когда нужно сделать что-то важное, интерфейс «просит» слой бизнес-логики заняться этим.

В реальности это выглядит так: один из объектов интерфейса напрямую вызывает метод одного из объектов бизнес-логики, передавая в него какие-то параметры.

Прямой доступ из UI в бизнес-логику

Прямой доступ из UI в бизнес-логику.

Паттерн Команда предлагает больше не отправлять такие вызовы напрямую. Вместо этого каждый вызов, отличающийся от других, следует завернуть в собственный класс с единственным методом, который и будет осуществлять вызов. Такие объекты называют командами.

К объекту интерфейса можно будет привязать объект команды, который знает, кому и в каком виде следует отправлять запросы. Когда объект интерфейса будет готов передать запрос, он вызовет метод команды, а та — позаботится обо всём остальном.

Доступ из UI в бизнес-логику через команду

Доступ из UI в бизнес-логику через команду.

Классы команд можно объединить под общим интерфейсом c единственным методом запуска. После этого одни и те же отправители смогут работать с различными командами, не привязываясь к их классам. Даже больше: команды можно будет взаимозаменять на лету, изменяя итоговое поведение отправителей.

Параметры, с которыми должен быть вызван метод объекта получателя, можно загодя сохранить в полях объекта-команды. Благодаря этому, объекты, отправляющие запросы, могут не беспокоиться о том, чтобы собрать необходимые для получателя данные. Более того, они теперь вообще не знают, кто будет получателем запроса. Вся эта информация скрыта внутри команды.

Классы UI делегируют работу командам

Классы UI делегируют работу командам.

После применения Команды в нашем примере с текстовым редактором вам больше не потребуется создавать уйму подклассов кнопок под разные действия. Будет достаточно единственного класса с полем для хранения объекта команды.

Используя общий интерфейс команд, объекты кнопок будут ссылаться на объекты команд различных типов. При нажатии кнопки будут делегировать работу связанным командам, а команды — перенаправлять вызовы тем или иным объектам бизнес-логики.

Так же можно поступить и с контекстным меню, и с горячими клавишами. Они будут привязаны к тем же объектам команд, что и кнопки, избавляя классы от дублирования.

Таким образом, команды станут гибкой прослойкой между пользовательским интерфейсом и бизнес-логикой. И это лишь малая доля пользы, которую может принести паттерн Команда!

Аналогия из жизни

Пример заказа в ресторане

Пример заказа в ресторане.

Вы заходите в ресторан и садитесь у окна. К вам подходит вежливый официант и принимает заказ, записывая все пожелания в блокнот. Откланявшись, он уходит на кухню, где вырывает лист из блокнота и клеит на стену. Далее лист оказывается в руках повара, который читает содержание заказа и готовит заказанные блюда.

В этом примере вы являетесь отправителем, официант с блокнотом — командой, а повар — получателем. Как и в паттерне, вы не соприкасаетесь напрямую с поваром. Вместо этого вы отправляете заказ с официантом, который самостоятельно «настраивает» повара на работу. С другой стороны, повар не знает, кто конкретно послал ему заказ. Но это ему безразлично, так как вся необходимая информация есть в листе заказа.

Структура

Структура классов паттерна Команда

  1. Отправитель хранит ссылку на объект команды и обращается к нему, когда нужно выполнить какое-то действие. Отправитель работает с командами только через их общий интерфейс. Он не знает, какую конкретно команду использует, так как получает готовый объект команды от клиента.
  2. Команда описывает общий для всех конкретных команд интерфейс. Обычно здесь описан всего один метод для запуска команды.
  3. Конкретные команды реализуют различные запросы, следуя общему интерфейсу команд. Обычно команда не делает всю работу самостоятельно, а лишь передаёт вызов получателю, которым является один из объектов бизнес-логики.
    Параметры, с которыми команда обращается к получателю, следует хранить в виде полей. В большинстве случаев объекты команд можно сделать неизменяемыми, передавая в них все необходимые параметры только через конструктор.
  4. Получатель содержит бизнес-логику программы. В этой роли может выступать практически любой объект. Обычно команды перенаправляют вызовы получателям. Но иногда, чтобы упростить программу, вы можете избавиться от получателей, «слив» их код в классы команд.
  5. Клиент создаёт объекты конкретных команд, передавая в них все необходимые параметры, среди которых могут быть и ссылки на объекты получателей. После этого клиент связывает объекты отправителей с созданными командами.

Псевдокод

В этом примере паттерн Команда служит для ведения истории выполненных операций, позволяя отменять их, если потребуется.

Структура классов примера паттерна Команда

Пример реализации отмены в текстовом редакторе.

Команды, которые меняют состояние редактора (например, команда вставки текста из буфера обмена), сохраняют копию состояния редактора перед выполнением действия. Копии выполненных команд помещаются в историю команд, откуда они могут быть получены, если нужно будет сделать отмену операции.

Классы элементов интерфейса, истории команд и прочие не зависят от конкретных классов команд, так как работают с ними через общий интерфейс. Это позволяет добавлять в приложение новые команды, не изменяя существующий код.

// Абстрактная команда задаёт общий интерфейс для конкретных
// классов команд и содержит базовое поведение отмены операции.
abstract class Command is
    protected field app: Application
    protected field editor: Editor
    protected field backup: text

    constructor Command(app: Application, editor: Editor) is
        this.app = app
        this.editor = editor

    // Сохраняем состояние редактора.
    method saveBackup() is
        backup = editor.text

    // Восстанавливаем состояние редактора.
    method undo() is
        editor.text = backup

    // Главный метод команды остаётся абстрактным, чтобы каждая
    // конкретная команда определила его по-своему. Метод должен
    // возвратить true или false в зависимости о того, изменила
    // ли команда состояние редактора, а значит, нужно ли её
    // сохранить в истории.
    abstract method execute()


// Конкретные команды.
class CopyCommand extends Command is
    // Команда копирования не записывается в историю, так как
    // она не меняет состояние редактора.
    method execute() is
        app.clipboard = editor.getSelection()
        return false

class CutCommand extends Command is
    // Команды, меняющие состояние редактора, сохраняют
    // состояние редактора перед своим действием и сигнализируют
    // об изменении, возвращая true.
    method execute() is
        saveBackup()
        app.clipboard = editor.getSelection()
        editor.deleteSelection()
        return true

class PasteCommand extends Command is
    method execute() is
        saveBackup()
        editor.replaceSelection(app.clipboard)
        return true

// Отмена — это тоже команда.
class UndoCommand extends Command is
    method execute() is
        app.undo()
        return false


// Глобальная история команд — это стек.
class CommandHistory is
    private field history: array of Command

    // Последний зашедший...
    method push(c: Command) is
        // Добавить команду в конец массива-истории.

    // ...выходит первым.
    method pop():Command is
        // Достать последнюю команду из массива-истории.


// Класс редактора содержит непосредственные операции над
// текстом. Он отыгрывает роль получателя — команды делегируют
// ему свои действия.
class Editor is
    field text: string

    method getSelection() is
        // Вернуть выбранный текст.

    method deleteSelection() is
        // Удалить выбранный текст.

    method replaceSelection(text) is
        // Вставить текст из буфера обмена в текущей позиции.


// Класс приложения настраивает объекты для совместной работы.
// Он выступает в роли отправителя — создаёт команды, чтобы
// выполнить какие-то действия.
class Application is
    field clipboard: string
    field editors: array of Editors
    field activeEditor: Editor
    field history: CommandHistory

    // Код, привязывающий команды к элементам интерфейса, может
    // выглядеть примерно так.
    method createUI() is
        // ...
        copy = function() {executeCommand(
            new CopyCommand(this, activeEditor)) }
        copyButton.setCommand(copy)
        shortcuts.onKeyPress("Ctrl+C", copy)

        cut = function() { executeCommand(
            new CutCommand(this, activeEditor)) }
        cutButton.setCommand(cut)
        shortcuts.onKeyPress("Ctrl+X", cut)

        paste = function() { executeCommand(
            new PasteCommand(this, activeEditor)) }
        pasteButton.setCommand(paste)
        shortcuts.onKeyPress("Ctrl+V", paste)

        undo = function() { executeCommand(
            new UndoCommand(this, activeEditor)) }
        undoButton.setCommand(undo)
        shortcuts.onKeyPress("Ctrl+Z", undo)

    // Запускаем команду и проверяем, надо ли добавить её в
    // историю.
    method executeCommand(command) is
        if (command.execute())
            history.push(command)

    // Берём последнюю команду из истории и заставляем её все
    // отменить. Мы не знаем конкретный тип команды, но это и не
    // важно, так как каждая команда знает, как отменить своё
    // действие.
    method undo() is
        command = history.pop()
        if (command != null)
            command.undo()

Применимость

Когда вы хотите параметризовать объекты выполняемым действием.

Команда превращает операции в объекты. А объекты можно передавать, хранить и взаимозаменять внутри других объектов.

Скажем, вы разрабатываете библиотеку графического меню и хотите, чтобы пользователи могли использовать меню в разных приложениях, не меняя каждый раз код ваших классов. Применив паттерн, пользователям не придётся изменять классы меню, вместо этого они будут конфигурировать объекты меню различными командами.

Когда вы хотите ставить операции в очередь, выполнять их по расписанию или передавать по сети.

Как и любые другие объекты, команды можно сериализовать, то есть превратить в строку, чтобы потом сохранить в файл или базу данных. Затем в любой удобный момент её можно достать обратно, снова превратить в объект команды и выполнить. Таким же образом команды можно передавать по сети, логировать или выполнять на удалённом сервере.

Когда вам нужна операция отмены.

Главная вещь, которая вам нужна, чтобы иметь возможность отмены операций, — это хранение истории. Среди многих способов, которыми можно это сделать, паттерн Команда является, пожалуй, самым популярным.

История команд выглядит как стек, в который попадают все выполненные объекты команд. Каждая команда перед выполнением операции сохраняет текущее состояние объекта, с которым она будет работать. После выполнения операции копия команды попадает в стек истории, все ещё неся в себе сохранённое состояние объекта. Если потребуется отмена, программа возьмёт последнюю команду из истории и возобновит сохранённое в ней состояние.

Этот способ имеет две особенности. Во-первых, точное состояние объектов не так-то просто сохранить, ведь часть его может быть приватным. Но с этим может помочь справиться паттерн Снимок.

Во-вторых, копии состояния могут занимать довольно много оперативной памяти. Поэтому иногда можно прибегнуть к альтернативной реализации, когда вместо восстановления старого состояния команда выполняет обратное действие. Недостаток этого способа в сложности (а иногда и невозможности) реализации обратного действия.

Шаги реализации

  1. Создайте общий интерфейс команд и определите в нём метод запуска.
  2. Один за другим создайте классы конкретных команд. В каждом классе должно быть поле для хранения ссылки на один или несколько объектов-получателей, которым команда будет перенаправлять основную работу.
    Кроме этого, команда должна иметь поля для хранения параметров, которые нужны при вызове методов получателя. Значения всех этих полей команда должна получать через конструктор.
    И, наконец, реализуйте основной метод команды, вызывая в нём те или иные методы получателя.
  3. Добавьте в классы отправителей поля для хранения команд. Обычно объекты-отправители принимают готовые объекты команд извне — через конструктор либо через сеттер поля команды.
  4. Измените основной код отправителей так, чтобы они делегировали выполнение действия команде.
  5. Порядок инициализации объектов должен выглядеть так:
    • Создаём объекты получателей.
    • Создаём объекты команд, связав их с получателями.
    • Создаём объекты отправителей, связав их с командами.

Преимущества и недостатки

Преимущества

  • Убирает прямую зависимость между объектами, вызывающими операции, и объектами, которые их непосредственно выполняют.
  • Позволяет реализовать простую отмену и повтор операций.
  • Позволяет реализовать отложенный запуск операций.
  • Позволяет собирать сложные команды из простых.
  • Реализует принцип открытости/закрытости.

Недостатки

  • Усложняет код программы из-за введения множества дополнительных классов.

Примеры реализации паттерна

Python

  • Сложность: 2/3
  • Популярность: 2/3
  • Применимость: Паттерн встречается в Python не так уж часто, так как для его применения нужна цепь объектов, например, связанный список.
  • Признаки применения паттерна: Классы команд построены вокруг одного действия и имеют очень узкий контекст. Объекты команд часто подаются в обработчики событий элементов GUI. Практически любая реализация отмены использует принципа команд.

Этот пример показывает структуру паттерна Команда, а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом.

from __future__ import annotations
from abc import ABC, abstractmethod


class Command(ABC):
    """
    Интерфейс Команды объявляет метод для выполнения команд.
    """

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


class SimpleCommand(Command):
    """
    Некоторые команды способны выполнять простые операции
    самостоятельно.
    """

    def __init__(self, payload: str) -> None:
        self._payload = payload

    def execute(self) -> None:
        print(f"SimpleCommand: See, I can do simple things like "
              f"printing({self._payload})")


class ComplexCommand(Command):
    """
    Но есть и команды, которые делегируют более сложные операции
    другим объектам, называемым «получателями».
    """

    def __init__(self, receiver: Receiver, a: str, b: str) -> None:
        """
        Сложные команды могут принимать один или несколько
        объектов-получателей вместе с любыми данными о контексте через
        конструктор.
        """

        self._receiver = receiver
        self._a = a
        self._b = b

    def execute(self) -> None:
        """
        Команды могут делегировать выполнение любым методам
        получателя.
        """

        print("ComplexCommand: Complex stuff should be done by a "
          + "receiver object", end="")
        self._receiver.do_something(self._a)
        self._receiver.do_something_else(self._b)


class Receiver:
    """
    Классы Получателей содержат некую важную бизнес-логику. Они умеют
    выполнять все виды операций, связанных с выполнением запроса.
    Фактически, любой класс может выступать Получателем.
    """

    def do_something(self, a: str) -> None:
        print(f"\nReceiver: Working on ({a}.)", end="")

    def do_something_else(self, b: str) -> None:
        print(f"\nReceiver: Also working on ({b}.)", end="")


class Invoker:
    """
    Отправитель связан с одной или несколькими командами. Он
    отправляет запрос команде.
    """

    _on_start = None
    _on_finish = None

    """
    Инициализация команд.
    """

    def set_on_start(self, command: Command):
        self._on_start = command

    def set_on_finish(self, command: Command):
        self._on_finish = command

    def do_something_important(self) -> None:
        """
        Отправитель не зависит от классов конкретных команд и
        получателей.
        Отправитель передаёт запрос получателю косвенно, выполняя
        команду.
        """

        print("Invoker: Does anybody want something done before I "
          + "begin?")
        if isinstance(self._on_start, Command):
            self._on_start.execute()

        print("Invoker: ...doing something really important...")

        print("Invoker: Does anybody want something done after I "
          + "finish?")
        if isinstance(self._on_finish, Command):
            self._on_finish.execute()


if __name__ == "__main__":
    """
    Клиентский код может параметризовать отправителя любыми командами.
    """

    invoker = Invoker()
    invoker.set_on_start(SimpleCommand("Say Hi!"))
    receiver = Receiver()
    invoker.set_on_finish(ComplexCommand(
        receiver, "Send email", "Save report"))

    invoker.do_something_important()
Invoker: Does anybody want something done before I begin?
SimpleCommand: See, I can do simple things like printing (Say Hi!)
Invoker: ...doing something really important...
Invoker: Does anybody want something done after I finish?
ComplexCommand: Complex stuff should be done by a receiver object
Receiver: Working on (Send email.)
Receiver: Also working on (Save report.)

PHP

  • Сложность: 2/3
  • Популярность: 2/3
  • Применимость: Паттерн можно часто встретить в PHP-коде, особенно когда нужно откладывать выполнение команд, выстраивать их в очереди, а также хранить историю и делать отмену.
  • Признаки применения паттерна: Классы команд построены вокруг одного действия и имеют очень узкий контекст. Объекты команд часто подаются в обработчики событий элементов GUI. Практически любая реализация отмены использует принципа команд.

Этот пример показывает структуру паттерна Команда, а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом.

После ознакомления со структурой, вам будет легче воспринимать второй пример, который рассматривает реальный случай использования паттерна в мире PHP.

<?php
/**
* Интерфейс Команды объявляет метод для выполнения команд.
*/

interface Command
{
    public function execute(): void;
}

/**
* Некоторые команды способны выполнять простые операции самостоятельно.
*/
class SimpleCommand implements Command
{
    private $payload;

    public function __construct(string $payload)
    {
        $this->payload = $payload;
    }

    public function execute(): void
    {
        echo "SimpleCommand: See, I can do simple things like printing (" . $this->payload . ")\n";
    }
}

/**
* Но есть и команды, которые делегируют более сложные операции другим объектам,
* называемым «получателями».
*/

class ComplexCommand implements Command
{
    /**
    * @var Receiver
    */
    private $receiver;

    /**
    * Данные о контексте, необходимые для запуска методов получателя.
    */
    private $a;

    private $b;

    /**
    * Сложные команды могут принимать один или несколько объектов-получателей
    * вместе с любыми данными о контексте через конструктор.
    */
    public function __construct(Receiver $receiver, string $a, string $b)
    {
        $this->receiver = $receiver;
        $this->a = $a;
        $this->b = $b;
    }

    /**
    * Команды могут делегировать выполнение любым методам получателя.
    */
    public function execute(): void
    {
        echo "ComplexCommand: Complex stuff should be done by a receiver object.\n";
        $this->receiver->doSomething($this->a);
        $this->receiver->doSomethingElse($this->b);
    }
}

/**
* Классы Получателей содержат некую важную бизнес-логику. Они умеют выполнять
* все виды операций, связанных с выполнением запроса. Фактически, любой класс
* может выступать Получателем.
*/

class Receiver
{
    public function doSomething(string $a): void
    {
        echo "Receiver: Working on (" . $a . ".)\n";
    }

    public function doSomethingElse(string $b): void
    {
        echo "Receiver: Also working on (" . $b . ".)\n";
    }
}

/**
* Отправитель связан с одной или несколькими командами. Он отправляет запрос
* команде.
*/

class Invoker
{
    /**
    * @var Command
    */
    private $onStart;

    /**
    * @var Command
    */
    private $onFinish;

    /**
    * Инициализация команд.
    */
    public function setOnStart(Command $command): void
    {
        $this->onStart = $command;
    }

    public function setOnFinish(Command $command): void
    {
        $this->onFinish = $command;
    }

    /**
    * Отправитель не зависит от классов конкретных команд и получателей.
    * Отправитель передаёт запрос получателю косвенно, выполняя команду.
    */
    public function doSomethingImportant(): void
    {
        echo "Invoker: Does anybody want something done before I begin?\n";
        if ($this->onStart instanceof Command) {
            $this->onStart->execute();
        }
        echo "Invoker: ...doing something really important...\n";
        echo "Invoker: Does anybody want something done after I finish?\n";
        if ($this->onFinish instanceof Command) {
            $this->onFinish->execute();
        }
    }
}

/**
* Клиентский код может параметризовать отправителя любыми командами.
*/
$invoker = new Invoker();
$invoker->setOnStart(new SimpleCommand("Say Hi!"));
$receiver = new Receiver();
$invoker->setOnFinish(new ComplexCommand($receiver, "Send email", "Save report"));

$invoker->doSomethingImportant();
Invoker: Does anybody want something done before I begin?
SimpleCommand: See, I can do simple things like printing (Say Hi!)
Invoker: ...doing something really important...
Invoker: Does anybody want something done after I finish?
ComplexCommand: Complex stuff should be done by a receiver object.
Receiver: Working on (Send email.)
Receiver: Also working on (Save report.)

В этом примере паттерн Команда применяется для построения очереди из вызовов скрейпинга (скачивания) отдельных страниц сайта IMDB и выполнения их один за другим. Сама очередь хранится в базе данных, которая помогает не терять команды между запусками скрипта.

<?php
/**
* Интерфейс Команды объявляет основной метод выполнения, а также несколько
* вспомогательных методов для получения метаданных команды.
*/

interface Command
{
    public function execute(): void;

    public function getId(): int;

    public function getStatus(): int;
}

/**
* Базовая Команда скрейпинга устанавливает базовую инфраструктуру загрузки,
* общую для всех конкретных команд скрейпинга.
*/

abstract class WebScrapingCommand implements Command
{
    public $id;

    public $status = 0;

    /**
    * @var string URL для скрейпинга.
    */
    public $url;

    public function __construct(string $url)
    {
        $this->url = $url;
    }

    public function getId(): int
    {
        return $this->id;
    }

    public function getStatus(): int
    {
        return $this->status;
    }

    public function getURL(): string
    {
        return $this->url;
    }

    /**
    * Поскольку методы выполнения для всех команд скрейпинга очень похожи, мы
    * можем предоставить реализацию по умолчанию, позволив подклассам
    * переопределить её при необходимости.
    *
    * Шш! Наблюдательный читатель может обнаружить здесь другой поведенческий
    * паттерн в действии.
    */
    public function execute(): void
    {
        $html = $this->download();
        $this->parse($html);
        $this->complete();
    }

    public function download(): string
    {
        $html = file_get_contents($this->getURL());
        echo "WebScrapingCommand: Downloaded {$this->url}\n";
        return $html;
    }

    abstract public function parse(string $html): void;

    public function complete(): void
    {
        $this->status = 1;
        Queue::get()->completeCommand($this);
    }
}

/**
* Конкретная Команда для извлечения списка жанров фильма.
*/
class IMDBGenresScrapingCommand extends WebScrapingCommand
{
    public function __construct()
    {
        $this->url = "https://www.imdb.com/feature/genre/";
    }

    /**
    * Извлечение всех жанров и их поисковых URL со страницы:
    * https://www.imdb.com/feature/genre/
    */

    public function parse($html): void
    {
        preg_match_all("|href=\"(https://www.imdb.com/search/title\?genres=.*?)\"|", $html, $matches);
        echo "IMDBGenresScrapingCommand: Discovered " . count($matches[1]) . " genres.\n";
        foreach ($matches[1] as $genre) {
            Queue::get()->add(new IMDBGenrePageScrapingCommand($genre));
        }
    }
}

/**
* Конкретная Команда для извлечения списка фильмов определённого жанра.
*/

class IMDBGenrePageScrapingCommand extends WebScrapingCommand
{
    private $page;

    public function __construct(string $url, int $page = 1)
    {
        parent::__construct($url);
        $this->page = $page;
    }

    public function getURL(): string
    {
        return $this->url . '?page=' . $this->page;
    }

    /**
    * Извлечение всех фильмов со страницы вроде этой:
    * https://www.imdb.com/search/title?genres=sci-fi&explore=title_type,genres
    */

    public function parse(string $html): void
    {
        preg_match_all("|href=\"(/title/.*?/)\?ref_=adv_li_tt\"|", $html, $matches);
        echo "IMDBGenrePageScrapingCommand: Discovered " . count($matches[1]) . " movies.\n";
        foreach ($matches[1] as $moviePath) {
            $url = "https://www.imdb.com" . $moviePath;
            Queue::get()->add(new IMDBMovieScrapingCommand($url));
        }

        // Извлечение URL следующей страницы.
        if (preg_match("|Next &#187;</a>|", $html)) {
            Queue::get()->add(new IMDBGenrePageScrapingCommand($this->url, $this->page + 1));
        }
    }
}

/**
* Конкретная Команда для извлечения подробных сведений о фильме.
*/

class IMDBMovieScrapingCommand extends WebScrapingCommand
{
    /**
    * Получить информацию о фильме с подобной страницы:
    * https://www.imdb.com/title/tt4154756/
    */

    public function parse(string $html): void
    {
        if (preg_match("|<h1 itemprop=\"name\" class=\"\">(.*?)</h1>|", $html, $matches)) {
            $title = $matches[1];
        }
        echo "IMDBMovieScrapingCommand: Parsed movie $title.\n";
    }
}

/**
* Класс Очередь действует как Отправитель. Он складывает объекты команд в стек
* и выполняет их поочерёдно. Если выполнение скрипта внезапно завершится,
* очередь и все её команды можно будет легко восстановить, и вам не придётся
* повторять все выполненные команды.
*
* Обратите внимание, что это очень примитивная реализация очереди команд,
* которая хранит команды в локальной базе данных SQLite. Существуют десятки
* надёжных реализаций очереди, доступных для использования в реальных
* приложениях.
*/

class Queue
{
    private $db;

    public function __construct()
    {
        $this->db = new \SQLite3(__DIR__ . '/commands.sqlite',
            SQLITE3_OPEN_CREATE | SQLITE3_OPEN_READWRITE);
        $this->db->query('CREATE TABLE IF NOT EXISTS "commands" (
            "id" INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL,
            "command" TEXT,
            "status" INTEGER
        )');
    }

    public function isEmpty(): bool
    {
        $query = 'SELECT COUNT("id") FROM "commands" WHERE status = 0';
        return $this->db->querySingle($query) === 0;
    }

    public function add(Command $command): void
    {
        $query = 'INSERT INTO commands (command, status) VALUES (:command, :status)';
        $statement = $this->db->prepare($query);
        $statement->bindValue(':command', base64_encode(serialize($command)));
        $statement->bindValue(':status', $command->getStatus());
        $statement->execute();
    }

    public function getCommand(): Command
    {
        $query = 'SELECT * FROM "commands" WHERE "status" = 0 LIMIT 1';
        $record = $this->db->querySingle($query, true);
        $command = unserialize(base64_decode($record["command"]));
        $command->id = $record['id'];
        return $command;
    }

    public function completeCommand(Command $command): void
    {
        $query = 'UPDATE commands SET status = :status WHERE id = :id';
        $statement = $this->db->prepare($query);
        $statement->bindValue(':status', $command->getStatus());
        $statement->bindValue(':id', $command->getId());
        $statement->execute();
    }

    public function work(): void
    {
        while (!$this->isEmpty()) {
            $command = $this->getCommand();
            $command->execute();
        }
    }

    /**
    * Для удобства объект Очереди является Одиночкой.
    */
    public static function get(): Queue
    {
        static $instance;
        if (!$instance) {
            $instance = new Queue();
        }
        return $instance;
    }
}

/**
* Клиентский код.
*/

$queue = Queue::get();

if ($queue->isEmpty()) {
    $queue->add(new IMDBGenresScrapingCommand());
}

$queue->work();
WebScrapingCommand: Downloaded https://www.imdb.com/feature/genre/
IMDBGenresScrapingCommand: Discovered 14 genres.
WebScrapingCommand: Downloaded https://www.imdb.com/search/title?
genres=comedy
IMDBGenrePageScrapingCommand: Discovered 50 movies.
WebScrapingCommand: Downloaded https://www.imdb.com/search/title?
genres=sci-fi
IMDBGenrePageScrapingCommand: Discovered 50 movies.
WebScrapingCommand: Downloaded https://www.imdb.com/search/title?
genres=horror
IMDBGenrePageScrapingCommand: Discovered 50 movies.
WebScrapingCommand: Downloaded https://www.imdb.com/search/title?
genres=romance
IMDBGenrePageScrapingCommand: Discovered 50 movies.
...

Задачи

Задача 1. Текстовый редактор с историей отмены

Реализуйте паттерн Команда для текстового редактора. Создайте команды: InsertTextCommand, DeleteTextCommand, ReplaceTextCommand. Класс Editor хранит текст, CommandHistory — стек выполненных команд. Реализуйте методы execute(cmd), undo() и redo(). Каждая команда должна уметь отменять своё действие.

from abc import ABC, abstractmethod

class Editor:
    def __init__(self):
        self.text: str = ""

    def __str__(self) -> str:
        return f'"{self.text}"'

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

    @abstractmethod
    def undo(self) -> None: pass

class CommandHistory:
    def __init__(self):
        self._history: list[Command] = []
        self._redo_stack: list[Command] = []

    def execute(self, cmd: Command) -> None:
        cmd.execute()
        self._history.append(cmd)
        self._redo_stack.clear()

    def undo(self) -> bool:
        if not self._history:
            print("Нечего отменять")
            return False
        cmd = self._history.pop()
        cmd.undo()
        self._redo_stack.append(cmd)
        return True

    def redo(self) -> bool:
        if not self._redo_stack:
            print("Нечего повторять")
            return False
        cmd = self._redo_stack.pop()
        cmd.execute()
        self._history.append(cmd)
        return True

# TODO: реализовать команды:
# InsertTextCommand(editor, position: int, text: str)
#   — execute: вставить text в editor.text в позицию position
#   — undo: удалить вставленный текст
#
# DeleteTextCommand(editor, position: int, length: int)
#   — execute: удалить length символов начиная с position (сохранить для undo)
#   — undo: восстановить удалённый текст
#
# ReplaceTextCommand(editor, position: int, length: int, new_text: str)
#   — execute: заменить length символов с position на new_text
#   — undo: восстановить исходный текст

# Ожидаемый клиентский код:
# editor  = Editor()
# history = CommandHistory()

# history.execute(InsertTextCommand(editor, 0, "Привет, мир!"))
# print(editor)  # "Привет, мир!"

# history.execute(ReplaceTextCommand(editor, 8, 3, "Python"))
# print(editor)  # "Привет, Python!"

# history.execute(InsertTextCommand(editor, 0, "Всем: "))
# print(editor)  # "Всем: Привет, Python!"

# history.undo()
# print(editor)  # "Привет, Python!"

# history.undo()
# print(editor)  # "Привет, мир!"

# history.redo()
# print(editor)  # "Привет, Python!"
<?php
class Editor {
    public string $text = '';
    public function __toString(): string { return "\"{$this->text}\""; }
}

interface Command {
    public function execute(): void;
    public function undo(): void;
}

class CommandHistory {
    private array $history    = [];
    private array $redoStack  = [];

    public function execute(Command $cmd): void {
        $cmd->execute();
        $this->history[] = $cmd;
        $this->redoStack = [];
    }

    public function undo(): bool {
        if (empty($this->history)) { echo "Нечего отменять\n"; return false; }
        $cmd = array_pop($this->history);
        $cmd->undo();
        $this->redoStack[] = $cmd;
        return true;
    }

    public function redo(): bool {
        if (empty($this->redoStack)) { echo "Нечего повторять\n"; return false; }
        $cmd = array_pop($this->redoStack);
        $cmd->execute();
        $this->history[] = $cmd;
        return true;
    }
}

// TODO: реализовать команды:
// InsertTextCommand(Editor $editor, int $position, string $text)
// DeleteTextCommand(Editor $editor, int $position, int $length)
// ReplaceTextCommand(Editor $editor, int $position, int $length, string $newText)

// Ожидаемый клиентский код:
// $editor  = new Editor();
// $history = new CommandHistory();

// $history->execute(new InsertTextCommand($editor, 0, 'Привет, мир!'));
// echo $editor . "\n";  // "Привет, мир!"

// $history->execute(new ReplaceTextCommand($editor, 8, 3, 'Python'));
// echo $editor . "\n";  // "Привет, Python!"

// $history->undo();
// echo $editor . "\n";  // "Привет, мир!"

// $history->redo();
// echo $editor . "\n";  // "Привет, Python!"

Задача 2. Очередь задач с приоритетами

Реализуйте паттерн Команда для системы фоновых задач. Создайте команды: SendEmailCommand, ResizeImageCommand, GenerateReportCommand. Класс TaskQueue принимает команды с приоритетом и выполняет их в порядке приоритета. Добавьте возможность отменить задачу из очереди до её выполнения.

from abc import ABC, abstractmethod
import heapq

class Task(ABC):
    @abstractmethod
    def execute(self) -> str: pass

    @abstractmethod
    def describe(self) -> str: pass

class TaskQueue:
    def __init__(self):
        self._queue: list[tuple[int, int, Task]] = []  # (priority, seq, task)
        self._seq = 0
        self._cancelled: set[int] = set()

    def enqueue(self, task: Task, priority: int = 5) -> int:
        """priority: 1 = highest, 10 = lowest. Возвращает ID задачи."""
        task_id = self._seq
        heapq.heappush(self._queue, (priority, self._seq, task))
        self._seq += 1
        print(f"[Queue] Задача #{task_id} добавлена: {task.describe()} (приоритет {priority})")
        return task_id

    def cancel(self, task_id: int) -> None:
        self._cancelled.add(task_id)
        print(f"[Queue] Задача #{task_id} отменена")

    def process_all(self) -> None:
        while self._queue:
            priority, task_id, task = heapq.heappop(self._queue)
            if task_id in self._cancelled:
                print(f"[Queue] Пропуск отменённой задачи #{task_id}")
                continue
            print(f"[Queue] Выполняю #{task_id}: {task.execute()}")

# TODO: реализовать команды:
# SendEmailCommand(to: str, subject: str, body: str)
#   — execute() -> f"Email отправлен на {to}: '{subject}'"
#   — describe() -> f"SendEmail → {to}"
#
# ResizeImageCommand(filename: str, width: int, height: int)
#   — execute() -> f"Изображение {filename} изменено до {width}x{height}"
#   — describe() -> f"ResizeImage: {filename}"
#
# GenerateReportCommand(report_type: str, filters: dict)
#   — execute() -> f"Отчёт '{report_type}' сгенерирован"
#   — describe() -> f"GenerateReport: {report_type}"

# Ожидаемый клиентский код:
# queue = TaskQueue()
# id1 = queue.enqueue(SendEmailCommand("[email protected]", "Добро пожаловать!", "..."), priority=3)
# id2 = queue.enqueue(ResizeImageCommand("photo.jpg", 800, 600), priority=7)
# id3 = queue.enqueue(GenerateReportCommand("sales", {"year": 2025}), priority=1)
# id4 = queue.enqueue(SendEmailCommand("[email protected]", "Отчёт готов", "..."), priority=5)

# queue.cancel(id2)
# queue.process_all()
<?php
interface Task {
    public function execute(): string;
    public function describe(): string;
}

class TaskQueue {
    private array $queue     = [];
    private array $cancelled = [];
    private int   $seq       = 0;

    public function enqueue(Task $task, int $priority = 5): int {
        $taskId = $this->seq++;
        $this->queue[] = [$priority, $taskId, $task];
        usort($this->queue, fn($a, $b) => $a[0] <=> $b[0] ?: $a[1] <=> $b[1]);
        echo "[Queue] Задача #{$taskId} добавлена: {$task->describe()} (приоритет {$priority})\n";
        return $taskId;
    }

    public function cancel(int $taskId): void {
        $this->cancelled[] = $taskId;
        echo "[Queue] Задача #{$taskId} отменена\n";
    }

    public function processAll(): void {
        foreach ($this->queue as [$priority, $taskId, $task]) {
            if (in_array($taskId, $this->cancelled)) {
                echo "[Queue] Пропуск отменённой задачи #{$taskId}\n";
                continue;
            }
            echo "[Queue] Выполняю #{$taskId}: " . $task->execute() . "\n";
        }
        $this->queue = [];
    }
}

// TODO: реализовать команды:
// SendEmailCommand(string $to, string $subject, string $body)
// ResizeImageCommand(string $filename, int $width, int $height)
// GenerateReportCommand(string $reportType, array $filters)

// Ожидаемый клиентский код:
// $queue = new TaskQueue();
// $id1 = $queue->enqueue(new SendEmailCommand('[email protected]', 'Добро пожаловать!', '...'), 3);
// $id2 = $queue->enqueue(new ResizeImageCommand('photo.jpg', 800, 600), 7);
// $id3 = $queue->enqueue(new GenerateReportCommand('sales', ['year' => 2025]), 1);
// $id4 = $queue->enqueue(new SendEmailCommand('[email protected]', 'Отчёт готов', '...'), 5);

// $queue->cancel($id2);
// $queue->processAll();

Задача 3. Транзакционные операции с базой данных

Реализуйте паттерн Команда для операций с базой данных с поддержкой транзакций. Создайте команды InsertCommand, UpdateCommand, DeleteCommand. Класс Transaction объединяет несколько команд в одну транзакцию: при вызове commit() выполняет все команды, при rollback() — отменяет уже выполненные в обратном порядке.

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

# Симуляция базы данных
class Database:
    def __init__(self):
        self._tables: dict[str, dict[int, dict]] = {}

    def insert(self, table: str, record_id: int, data: dict) -> None:
        self._tables.setdefault(table, {})[record_id] = data.copy()
        print(f"[DB] INSERT INTO {table} VALUES ({record_id}, {data})")

    def update(self, table: str, record_id: int, data: dict) -> None:
        if table not in self._tables or record_id not in self._tables[table]:
            raise ValueError(f"Запись {record_id} не найдена в {table}")
        self._tables[table][record_id].update(data)
        print(f"[DB] UPDATE {table} SET {data} WHERE id={record_id}")

    def delete(self, table: str, record_id: int) -> dict | None:
        row = self._tables.get(table, {}).pop(record_id, None)
        print(f"[DB] DELETE FROM {table} WHERE id={record_id}")
        return row

    def find(self, table: str, record_id: int) -> dict | None:
        return self._tables.get(table, {}).get(record_id)

    def dump(self) -> None:
        for table, rows in self._tables.items():
            print(f"  {table}: {rows}")

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

    @abstractmethod
    def undo(self) -> None: pass

class Transaction:
    def __init__(self):
        self._commands: list[DbCommand] = []
        self._executed: list[DbCommand] = []

    def add(self, cmd: DbCommand) -> 'Transaction':
        self._commands.append(cmd)
        return self

    def commit(self) -> None:
        print("[TX] Начало транзакции")
        try:
            for cmd in self._commands:
                cmd.execute()
                self._executed.append(cmd)
            print("[TX] Транзакция успешно выполнена")
        except Exception as e:
            print(f"[TX] Ошибка: {e}. Откат...")
            self.rollback()
            raise

    def rollback(self) -> None:
        print("[TX] Откат транзакции")
        for cmd in reversed(self._executed):
            cmd.undo()
        self._executed.clear()

# TODO: реализовать команды:
# InsertCommand(db: Database, table: str, record_id: int, data: dict)
#   — execute: db.insert(...)
#   — undo: db.delete(table, record_id)
#
# UpdateCommand(db: Database, table: str, record_id: int, new_data: dict)
#   — execute: сохранить старые данные, db.update(...)
#   — undo: db.update(table, record_id, old_data)
#
# DeleteCommand(db: Database, table: str, record_id: int)
#   — execute: сохранить удаляемую запись, db.delete(...)
#   — undo: db.insert(table, record_id, saved_data)

# Ожидаемый клиентский код:
# db = Database()
# db.insert("users", 1, {"name": "Анна", "email": "[email protected]", "active": True})

# # Успешная транзакция
# tx = Transaction()
# tx.add(InsertCommand(db, "users", 2, {"name": "Иван", "email": "[email protected]", "active": True})) \
#   .add(UpdateCommand(db, "users", 1, {"active": False})) \
#   .add(InsertCommand(db, "orders", 100, {"user_id": 1, "total": 5000}))
# tx.commit()
# db.dump()

# # Транзакция с ошибкой → откат
# tx2 = Transaction()
# tx2.add(UpdateCommand(db, "users", 2, {"name": "Иван Петров"})) \
#    .add(UpdateCommand(db, "users", 999, {"name": "Несуществующий"}))  # ошибка
# try:
#     tx2.commit()
# except ValueError:
#     pass
# db.dump()  # users[2] должен остаться без изменений
<?php
class Database {
    private array $tables = [];

    public function insert(string $table, int $id, array $data): void {
        $this->tables[$table][$id] = $data;
        echo "[DB] INSERT INTO {$table} VALUES ({$id}, " . json_encode($data) . ")\n";
    }

    public function update(string $table, int $id, array $data): void {
        if (!isset($this->tables[$table][$id]))
            throw new InvalidArgumentException("Запись {$id} не найдена в {$table}");
        $this->tables[$table][$id] = array_merge($this->tables[$table][$id], $data);
        echo "[DB] UPDATE {$table} SET " . json_encode($data) . " WHERE id={$id}\n";
    }

    public function delete(string $table, int $id): ?array {
        $row = $this->tables[$table][$id] ?? null;
        unset($this->tables[$table][$id]);
        echo "[DB] DELETE FROM {$table} WHERE id={$id}\n";
        return $row;
    }

    public function find(string $table, int $id): ?array {
        return $this->tables[$table][$id] ?? null;
    }

    public function dump(): void {
        foreach ($this->tables as $table => $rows)
            echo "  {$table}: " . json_encode($rows) . "\n";
    }
}

interface DbCommand {
    public function execute(): void;
    public function undo(): void;
}

class Transaction {
    private array $commands = [];
    private array $executed = [];

    public function add(DbCommand $cmd): static { $this->commands[] = $cmd; return $this; }

    public function commit(): void {
        echo "[TX] Начало транзакции\n";
        try {
            foreach ($this->commands as $cmd) {
                $cmd->execute();
                $this->executed[] = $cmd;
            }
            echo "[TX] Транзакция успешно выполнена\n";
        } catch (\Exception $e) {
            echo "[TX] Ошибка: {$e->getMessage()}. Откат...\n";
            $this->rollback();
            throw $e;
        }
    }

    public function rollback(): void {
        echo "[TX] Откат транзакции\n";
        foreach (array_reverse($this->executed) as $cmd) $cmd->undo();
        $this->executed = [];
    }
}

// TODO: реализовать команды:
// InsertCommand(Database $db, string $table, int $id, array $data)
// UpdateCommand(Database $db, string $table, int $id, array $newData)
// DeleteCommand(Database $db, string $table, int $id)

// Ожидаемый клиентский код:
// $db = new Database();
// $db->insert('users', 1, ['name' => 'Анна', 'active' => true]);

// $tx = new Transaction();
// $tx->add(new InsertCommand($db, 'users', 2, ['name' => 'Иван', 'active' => true]))
//    ->add(new UpdateCommand($db, 'users', 1, ['active' => false]))
//    ->add(new InsertCommand($db, 'orders', 100, ['user_id' => 1, 'total' => 5000]));
// $tx->commit();
// $db->dump();

Задача 4. Пульт умного дома

Реализуйте паттерн Команда для пульта управления умным домом. Пульт имеет 5 слотов, каждый со своей кнопкой «включить» и «выключить». Создайте команды для управления Light, Fan, SecuritySystem. Реализуйте кнопку «Отмена» для последнего действия и кнопку «Макрос» для выполнения нескольких команд сразу.

from abc import ABC, abstractmethod

# Устройства
class Light:
    def __init__(self, location: str):
        self.location = location
        self._on = False
        self._brightness = 100

    def on(self) -> None:
        self._on = True
        print(f"[Light] {self.location}: включён (яркость {self._brightness}%)")

    def off(self) -> None:
        self._on = False
        print(f"[Light] {self.location}: выключен")

    def set_brightness(self, level: int) -> None:
        self._brightness = level
        print(f"[Light] {self.location}: яркость {level}%")

class Fan:
    def __init__(self, location: str):
        self.location = location
        self._speed = 0  # 0=off, 1=low, 2=med, 3=high

    def set_speed(self, speed: int) -> None:
        self._speed = speed
        labels = {0: "выкл.", 1: "слабый", 2: "средний", 3: "сильный"}
        print(f"[Fan] {self.location}: {labels[speed]}")

class SecuritySystem:
    def __init__(self):
        self._armed = False

    def arm(self, mode: str = "away") -> None:
        self._armed = True
        print(f"[Security] Охрана включена: {mode}")

    def disarm(self) -> None:
        self._armed = False
        print("[Security] Охрана снята")

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

    @abstractmethod
    def undo(self) -> None: pass

# TODO: реализовать команды:
# LightOnCommand(light: Light)  / LightOffCommand(light: Light)
# FanSpeedCommand(fan: Fan, speed: int) — undo восстанавливает предыдущую скорость
# SecurityArmCommand(system: SecuritySystem, mode: str) / SecurityDisarmCommand(...)
# MacroCommand(commands: list[Command])
#   — execute: выполняет все команды по порядку
#   — undo: отменяет все команды в обратном порядке

# TODO: реализовать RemoteControl:
# - set_command(slot: int, on_cmd: Command, off_cmd: Command)
# - press_on(slot: int)
# - press_off(slot: int)
# - press_undo()  — отменяет последнее действие

# Ожидаемый клиентский код:
# living_light   = Light("Гостиная")
# bedroom_light  = Light("Спальня")
# ceiling_fan    = Fan("Потолочный")
# security       = SecuritySystem()

# remote = RemoteControl(slots=5)
# remote.set_command(0, LightOnCommand(living_light),  LightOffCommand(living_light))
# remote.set_command(1, LightOnCommand(bedroom_light), LightOffCommand(bedroom_light))
# remote.set_command(2, FanSpeedCommand(ceiling_fan, 3), FanSpeedCommand(ceiling_fan, 0))
# remote.set_command(3, SecurityArmCommand(security, "away"), SecurityDisarmCommand(security))

# # Макрос "уходим из дома"
# leave_macro = MacroCommand([
#     LightOffCommand(living_light),
#     LightOffCommand(bedroom_light),
#     FanSpeedCommand(ceiling_fan, 0),
#     SecurityArmCommand(security, "away"),
# ])
# remote.set_command(4, leave_macro, MacroCommand([]))

# remote.press_on(0)
# remote.press_on(2)
# remote.press_undo()  # отменяет вентилятор
# remote.press_on(4)   # макрос — уходим
<?php
class Light {
    private bool $on = false;
    private int  $brightness = 100;

    public function __construct(public string $location) {}

    public function on(): void {
        $this->on = true;
        echo "[Light] {$this->location}: включён (яркость {$this->brightness}%)\n";
    }

    public function off(): void {
        $this->on = false;
        echo "[Light] {$this->location}: выключен\n";
    }

    public function setBrightness(int $level): void {
        $this->brightness = $level;
        echo "[Light] {$this->location}: яркость {$level}%\n";
    }
}

class Fan {
    private int $speed = 0;

    public function __construct(public string $location) {}

    public function setSpeed(int $speed): void {
        $this->speed = $speed;
        $labels = [0 => 'выкл.', 1 => 'слабый', 2 => 'средний', 3 => 'сильный'];
        echo "[Fan] {$this->location}: {$labels[$speed]}\n";
    }

    public function getSpeed(): int { return $this->speed; }
}

class SecuritySystem {
    private bool $armed = false;

    public function arm(string $mode = 'away'): void {
        $this->armed = true;
        echo "[Security] Охрана включена: {$mode}\n";
    }

    public function disarm(): void {
        $this->armed = false;
        echo "[Security] Охрана снята\n";
    }
}

interface Command {
    public function execute(): void;
    public function undo(): void;
}

// TODO: реализовать команды:
// LightOnCommand / LightOffCommand
// FanSpeedCommand(Fan $fan, int $speed) — undo восстанавливает предыдущую скорость
// SecurityArmCommand / SecurityDisarmCommand
// MacroCommand(array $commands)

// TODO: реализовать RemoteControl(int $slots = 5):
// - setCommand(slot, onCmd, offCmd)
// - pressOn(slot) / pressOff(slot)
// - pressUndo()

// Ожидаемый клиентский код:
// $livingLight  = new Light('Гостиная');
// $bedroomLight = new Light('Спальня');
// $fan          = new Fan('Потолочный');
// $security     = new SecuritySystem();

// $remote = new RemoteControl(slots: 5);
// $remote->setCommand(0, new LightOnCommand($livingLight),  new LightOffCommand($livingLight));
// $remote->setCommand(2, new FanSpeedCommand($fan, 3),      new FanSpeedCommand($fan, 0));
// $remote->setCommand(3, new SecurityArmCommand($security), new SecurityDisarmCommand($security));

// $remote->pressOn(0);
// $remote->pressOn(2);
// $remote->pressUndo();

Задача 5. Планировщик задач с повтором и логом

Реализуйте паттерн Команда для планировщика задач. Команды: BackupCommand, CleanupCommand, SyncCommand. Класс Scheduler выполняет задачи по расписанию, сохраняет лог выполнения с временными метками и поддерживает повтор неудачных задач (retry). Реализуйте ExecutionLog для хранения истории с результатами.

from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
import random

class TaskStatus(Enum):
    PENDING  = "pending"
    SUCCESS  = "success"
    FAILED   = "failed"
    RETRYING = "retrying"

@dataclass
class LogEntry:
    command_name: str
    status: TaskStatus
    started_at: datetime
    finished_at: datetime | None = None
    error: str | None = None
    attempt: int = 1

    def duration_ms(self) -> float:
        if self.finished_at:
            return (self.finished_at - self.started_at).total_seconds() * 1000
        return 0

    def __str__(self) -> str:
        dur = f"{self.duration_ms():.0f}мс"
        err = f" | Ошибка: {self.error}" if self.error else ""
        return (f"[{self.started_at.strftime('%H:%M:%S')}] "
                f"{self.command_name} — {self.status.value} "
                f"(попытка {self.attempt}, {dur}){err}")

class ScheduledCommand(ABC):
    @property
    @abstractmethod
    def name(self) -> str: pass

    @abstractmethod
    def execute(self) -> None:
        """Бросает Exception при ошибке"""
        pass

class ExecutionLog:
    def __init__(self):
        self._entries: list[LogEntry] = []

    def add(self, entry: LogEntry) -> None:
        self._entries.append(entry)

    def print_all(self) -> None:
        for e in self._entries:
            print(e)

    def get_failed(self) -> list[LogEntry]:
        return [e for e in self._entries if e.status == TaskStatus.FAILED]

    def success_rate(self) -> float:
        if not self._entries: return 0.0
        ok = sum(1 for e in self._entries if e.status == TaskStatus.SUCCESS)
        return ok / len(self._entries) * 100

class Scheduler:
    def __init__(self, max_retries: int = 3):
        self._max_retries = max_retries
        self._log = ExecutionLog()

    def run(self, cmd: ScheduledCommand) -> None:
        for attempt in range(1, self._max_retries + 1):
            entry = LogEntry(cmd.name, TaskStatus.PENDING, datetime.now(), attempt=attempt)
            try:
                if attempt > 1:
                    print(f"[Scheduler] Повтор {cmd.name} (попытка {attempt})")
                    entry.status = TaskStatus.RETRYING
                cmd.execute()
                entry.status  = TaskStatus.SUCCESS
                entry.finished_at = datetime.now()
                self._log.add(entry)
                print(f"[Scheduler] {cmd.name} — OK")
                return
            except Exception as e:
                entry.status  = TaskStatus.FAILED
                entry.error   = str(e)
                entry.finished_at = datetime.now()
                self._log.add(entry)
                if attempt == self._max_retries:
                    print(f"[Scheduler] {cmd.name} — FAILED после {attempt} попыток")

    @property
    def log(self) -> ExecutionLog:
        return self._log

# TODO: реализовать команды:
# BackupCommand(source: str, destination: str, fail_rate: float = 0.0)
#   — execute: "Резервное копирование {source} → {destination}"
#   — если random.random() < fail_rate → raise RuntimeError("Ошибка записи на диск")
#
# CleanupCommand(directory: str, older_than_days: int)
#   — execute: "Удалены файлы старше {days} дней из {directory}"
#
# SyncCommand(source: str, target: str, fail_rate: float = 0.0)
#   — execute: "Синхронизация {source} → {target}"

# Ожидаемый клиентский код:
# scheduler = Scheduler(max_retries=3)

# scheduler.run(BackupCommand("/data", "/backup", fail_rate=0.0))
# scheduler.run(CleanupCommand("/tmp", older_than_days=7))
# scheduler.run(SyncCommand("/data", "s3://bucket", fail_rate=0.7))  # часто падает

# print("\n=== Лог выполнения ===")
# scheduler.log.print_all()
# print(f"\nУспешность: {scheduler.log.success_rate():.1f}%")
# print(f"Неудачных задач: {len(scheduler.log.get_failed())}")

```php linenums="1" <?php enum TaskStatus: string { case PENDING = 'pending'; case SUCCESS = 'success'; case FAILED = 'failed'; case RETRYING = 'retrying'; }

class LogEntry { public ?DateTime $finishedAt = null; public ?string $error = null;

public function __construct(
    public string     $commandName,
    public TaskStatus $status,
    public DateTime   $startedAt,
    public int        $attempt = 1,
) {}

public function durationMs(): float {
    if (!$this->finishedAt) return 0;
    return ($this->finishedAt->getTimestamp() - $this->startedAt->getTimestamp()) * 1000;
}

public function __toString(): string {
    $dur = number_format($this->durationMs(), 0) . 'мс';
    $err = $this->error ? " | Ошибка: {$this->error}" : '';
    return "[{$this->startedAt->format('H:i:s')}] "
        . "{$this->commandName} — {$this->status->value} "
        . "(попытка {$this->attempt}, {$dur}){$err}";
}

}

interface ScheduledCommand { public function getName(): string; public function execute(): void; // бросает Exception при ошибке }

class ExecutionLog { private array $entries = [];

public function add(LogEntry $entry): void { $this->entries[] = $entry; }

public function printAll(): void {
    foreach ($this->entries as $e) echo $e . "\n";
}

public function getFailed(): array {
    return array_filter($this->entries, fn($e) => $e->status === TaskStatus::FAILED);
}

public function successRate(): float {
    if (!$this->entries) return 0.0;
    $ok = count(array_filter($this->entries, fn($e) => $e->status === TaskStatus::SUCCESS));
    return $ok / count($this->entries) * 100;
}

}

class Scheduler { private ExecutionLog $log;

public function __construct(private int $maxRetries = 3) {
    $this->log = new ExecutionLog();
}

public function run(ScheduledCommand $cmd): void {
    for ($attempt = 1; $attempt <= $this->maxRetries; $attempt++) {
        $entry = new LogEntry($cmd->getName(), TaskStatus::PENDING, new DateTime(), $attempt);
        try {
            if ($attempt > 1) echo "[Scheduler] Повтор {$cmd->getName()} (попытка {$attempt})\n";
            $cmd->execute();
            $entry->status     = TaskStatus::SUCCESS;
            $entry->finishedAt = new DateTime();
            $this->log->add($entry);
            echo "[Scheduler] {$cmd->getName()} — OK\n";
            return;
        } catch (\Exception $e) {
            $entry->status     = TaskStatus::FAILED;
            $entry->error      = $e->getMessage();
            $entry->finishedAt = new DateTime();
            $this->log->add($entry);
            if ($attempt === $this->maxRetries)
                echo "[Scheduler] {$cmd->getName()} — FAILED после {$attempt} попыток\n";
        }
    }
}

public function getLog(): ExecutionLog { return $this->log; }

}

// TODO: реализовать команды: // BackupCommand(string $source, string $destination, float $failRate = 0.0) // CleanupCommand(string $directory, int $olderThanDays) // SyncCommand(string $source, string $target, float $failRate = 0.0)

// Ожидаемый клиентский код: // $scheduler = new Scheduler(maxRetries: 3); // $scheduler->run(new BackupCommand('/data', '/backup', failRate: 0.0)); // $scheduler->run(new CleanupCommand('/tmp', olderThanDays: 7)); // $scheduler->run(new SyncCommand('/data', 's3://bucket', failRate: 0.7));

// echo "\n=== Лог выполнения ===\n"; // \(scheduler->getLog()->printAll(); // echo "\nУспешность: " . number_format(\)scheduler->getLog()->successRate(), 1) . "%\n";