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

Стратегия

Также известен как: Strategy

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

Паттерн Стратегия

Проблема

Вы решили написать приложение-навигатор для путешественников. Оно должно показывать красивую и удобную карту, позволяющую с лёгкостью ориентироваться в незнакомом городе.

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

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

Через некоторое время выяснилось, что некоторые люди предпочитают ездить по городу на общественном транспорте. Поэтому вы добавили и такую опцию прокладывания пути.

Но и это ещё не всё. В ближайшей перспективе вы хотели бы добавить прокладывание маршрутов по велодорожкам. А в отдалённом будущем — интересные маршруты посещения достопримечательностей.

Код навигатора становится слишком раздутым

Код навигатора становится слишком раздутым.

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

Любое изменение алгоритмов поиска, будь то исправление багов или добавление нового алгоритма, затрагивало основной класс. Это повышало риск сделать ошибку, случайно задев остальной работающий код.

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

Решение

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

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

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

Стратегии построения пути

Стратегии построения пути.

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

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

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

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

Способы передвижения

Различные стратегии попадания в аэропорт.

Вам нужно добраться до аэропорта. Можно доехать на автобусе, такси или велосипеде. Здесь вид транспорта является стратегией. Вы выбираете конкретную стратегию в зависимости от контекста — наличия денег или времени до отлёта.

Структура

Структура классов паттерна Стратегия

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

Псевдокод

В этом примере контекст использует Стратегию для выполнения той или иной арифметической операции.

// Общий интерфейс всех стратегий.
interface Strategy is
    method execute(a, b)

// Каждая конкретная стратегия реализует общий интерфейс своим
// способом.
class ConcreteStrategyAdd implements Strategy is
    method execute(a, b) is
        return a + b

class ConcreteStrategySubtract implements Strategy is
    method execute(a, b) is
        return a - b

class ConcreteStrategyMultiply implements Strategy is
    method execute(a, b) is
        return a * b

// Контекст всегда работает со стратегиями через общий
// интерфейс. Он не знает, какая именно стратегия ему подана.
class Context is
    private strategy: Strategy

    method setStrategy(Strategy strategy) is
        this.strategy = strategy

    method executeStrategy(int a, int b) is
        return strategy.execute(a, b)


// Конкретная стратегия выбирается на более высоком уровне,
// например, конфигуратором всего приложения. Готовый объект-
// стратегия подаётся в клиентский объект, а затем может быть
// заменён другой стратегией в любой момент на лету.
class ExampleApplication is
    method main() is
        // 1. Создать объект контекста.
        // 2. Получить первое число (n1).
        // 3. Получить второе число (n2).
        // 4. Получить желаемую операцию.
        // 5. Затем, выбрать стратегию:

        if (action == addition) then
            context.setStrategy(new ConcreteStrategyAdd())

        if (action == subtraction) then
            context.setStrategy(new ConcreteStrategySubtract())

        if (action == multiplication) then
            context.setStrategy(new ConcreteStrategyMultiply())

        // 6. Выполнить операцию с помощью стратегии:
        result = context.executeStrategy(n1, n2)

        // 7. Вывести результат на экран.

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

Когда вам нужно использовать разные вариации какого-то алгоритма внутри одного объекта.

Стратегия позволяет варьировать поведение объекта во время выполнения программы, подставляя в него различные объекты-поведения (например, отличающиеся балансом скорости и потребления ресурсов).

Когда у вас есть множество похожих классов, отличающихся только некоторым поведением.

Стратегия позволяет вынести отличающееся поведение в отдельную иерархию классов, а затем свести первоначальные классы к одному, сделав поведение этого класса настраиваемым.

Когда вы не хотите обнажать детали реализации алгоритмов для других классов.

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

Когда различные вариации алгоритмов реализованы в виде развесистого условного оператора. Каждая ветка такого оператора представляет собой вариацию алгоритма.

Стратегия помещает каждую лапу такого оператора в отдельный класс-стратегию. Затем контекст получает определённый объект-стратегию от клиента и делегирует ему работу. Если вдруг понадобится сменить алгоритм, в контекст можно подать другую стратегию.

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

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

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

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

  • Горячая замена алгоритмов на лету.
  • Изолирует код и данные алгоритмов от остальных классов.
  • Уход от наследования к делегированию.
  • Реализует принцип открытости/закрытости.

Недостатки

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

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

Python

  • Сложность: 1/3
  • Популярность: 3/3
  • Применимость: Стратегия часто используется в Python-коде, особенно там, где нужно подменять алгоритм во время выполнения программы. Многие примеры стратегии можно заменить простыми lambda-выражениями.
  • Признаки применения паттерна: Класс делегирует выполнение вложенному объекту абстрактного типа или интерфейса.

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

from __future__ import annotations
from abc import ABC, abstractmethod
from typing import List


class Context():
    """
    Контекст определяет интерфейс, представляющий интерес для
    клиентов.
    """

    def __init__(self, strategy: Strategy) -> None:
        """
        Обычно Контекст принимает стратегию через конструктор, а также
        предоставляет сеттер для её изменения во время выполнения.
        """

        self._strategy = strategy

    @property
    def strategy(self) -> Strategy:
        """
        Контекст хранит ссылку на один из объектов Стратегии. Контекст
        не знает конкретного класса стратегии. Он должен работать со
        всеми стратегиями через интерфейс Стратегии.
        """

        return self._strategy

    @strategy.setter
    def strategy(self, strategy: Strategy) -> None:
        """
        Обычно Контекст позволяет заменить объект Стратегии во время
        выполнения.
        """

        self._strategy = strategy

    def do_some_business_logic(self) -> None:
        """
        Вместо того, чтобы самостоятельно реализовывать множественные
        версии алгоритма, Контекст делегирует некоторую работу
        объекту Стратегии.
        """

        # ...

        print("Context: Sorting data using the strategy (not sure "
            + "how it'll do it)")
        result = self._strategy.do_algorithm(
            ["a", "b", "c", "d", "e"])
        print(",".join(result))

        # ...


class Strategy(ABC):
    """
    Интерфейс Стратегии объявляет операции, общие для всех
    поддерживаемых версий некоторого алгоритма.

    Контекст использует этот интерфейс для вызова алгоритма,
    определённого Конкретными Стратегиями.
    """

    @abstractmethod
    def do_algorithm(self, data: List):
        pass


"""
Конкретные Стратегии реализуют алгоритм, следуя базовому интерфейсу
Стратегии. Этот интерфейс делает их взаимозаменяемыми в Контексте.
"""


class ConcreteStrategyA(Strategy):
    def do_algorithm(self, data: List) -> List:
        return sorted(data)


class ConcreteStrategyB(Strategy):
    def do_algorithm(self, data: List) -> List:
        return reversed(sorted(data))


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

    context = Context(ConcreteStrategyA())
    print("Client: Strategy is set to normal sorting.")
    context.do_some_business_logic()
    print()

    print("Client: Strategy is set to reverse sorting.")
    context.strategy = ConcreteStrategyB()
    context.do_some_business_logic()
Client: Strategy is set to normal sorting.
Context: Sorting data using the strategy (not sure how it'll do it)
a,b,c,d,e

Client: Strategy is set to reverse sorting.
Context: Sorting data using the strategy (not sure how it'll do it)
e,d,c,b,a

PHP

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

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

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

<?php
/**
* Контекст определяет интерфейс, представляющий интерес для клиентов.
*/

class Context
{
    /**
    * @var Strategy Контекст хранит ссылку на один из объектов Стратегии.
    * Контекст не знает конкретного класса стратегии. Он должен работать со
    * всеми стратегиями через интерфейс Стратегии.
    */
    private $strategy;

    /**
    * Обычно Контекст принимает стратегию через конструктор, а также
    * предоставляет сеттер для её изменения во время выполнения.
    */
    public function __construct(Strategy $strategy)
    {
        $this->strategy = $strategy;
    }

    /**
    * Обычно Контекст позволяет заменить объект Стратегии во время выполнения.
    */
    public function setStrategy(Strategy $strategy)
    {
        $this->strategy = $strategy;
    }

    /**
    * Вместо того, чтобы самостоятельно реализовывать множественные версии
    * алгоритма, Контекст делегирует некоторую работу объекту Стратегии.
    */
    public function doSomeBusinessLogic(): void
    {
        // ...

        echo "Context: Sorting data using the strategy (not sure how it'll do it)\n";
        $result = $this->strategy->doAlgorithm(["a", "b", "c", "d", "e"]);
        echo implode(",", $result) . "\n";
        // ...
    }
}

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

interface Strategy
{
    public function doAlgorithm(array $data): array;
}

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

class ConcreteStrategyA implements Strategy
{
    public function doAlgorithm(array $data): array
    {
        sort($data);
        return $data;
    }
}

class ConcreteStrategyB implements Strategy
{
    public function doAlgorithm(array $data): array
    {
        rsort($data);
        return $data;
    }
}

/**
* Клиентский код выбирает конкретную стратегию и передаёт её в контекст. Клиент
* должен знать о различиях между стратегиями, чтобы сделать правильный выбор.
*/
$context = new Context(new ConcreteStrategyA());
echo "Client: Strategy is set to normal sorting.\n";
$context->doSomeBusinessLogic();

echo "\n";

echo "Client: Strategy is set to reverse sorting.\n";
$context->setStrategy(new ConcreteStrategyB());
$context->doSomeBusinessLogic();
Client: Strategy is set to normal sorting.
Context: Sorting data using the strategy (not sure how it'll do it)
a,b,c,d,e

Client: Strategy is set to reverse sorting.
Context: Sorting data using the strategy (not sure how it'll do it)
e,d,c,b,a

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

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

<?php
/**
* Это роутер и контроллер нашего приложения. Получив запрос, этот класс решает,
* какое поведение должно выполняться. Когда приложение получает требование об
* оплате, класс OrderController также решает, какой способ оплаты следует
* использовать для его обработки. Таким образом, этот класс действует как
* Контекст и в то же время как Клиент.
*/

class OrderController
{
    /**
    * Обрабатываем запросы POST.
    *
    * @param $url
    * @param $data
    * @throws \Exception
    */
    public function post(string $url, array $data)
    {
        echo "Controller: POST request to $url with " . json_encode($data) . "\n";
        $path = parse_url($url, PHP_URL_PATH);
        if (preg_match('#^/orders?$#', $path, $matches)) {
            $this->postNewOrder($data);
        } else {
            echo "Controller: 404 page\n";
        }
    }

    /**
    * Обрабатываем запросы GET.
    *
    * @param $url
    * @throws \Exception
    */
    public function get(string $url): void
    {
        echo "Controller: GET request to $url\n";

        $path = parse_url($url, PHP_URL_PATH);
        $query = parse_url($url, PHP_URL_QUERY);
        parse_str($query, $data);

        if (preg_match('#^/orders?$#', $path, $matches)) {
            $this->getAllOrders();
        } elseif (preg_match('#^/order/([0-9]+?)/payment/([a-z]+?)(/return)?$#', $path, $matches)) {
            $order = Order::get($matches[1]);
            // Способ оплаты (стратегия) выбирается в соответствии со значением,
            // переданным в запросе.
            $paymentMethod = PaymentFactory::getPaymentMethod($matches[2]); 
            if (!isset($matches[3])) {
                $this->getPayment($paymentMethod, $order, $data);
            } else {
                $this->getPaymentReturn($paymentMethod, $order, $data);
            }
        } else {
            echo "Controller: 404 page\n";
        }
    }

    /**
    * POST /order {data}
    */
    public function postNewOrder(array $data): void
    {
        $order = new Order($data);
        echo "Controller: Created the order #{$order->id}.\n";
    }

    /**
    * GET /orders
    */
    public function getAllOrders(): void
    {
        echo "Controller: Here's all orders:\n";
        foreach (Order::get() as $order) {
            echo json_encode($order, JSON_PRETTY_PRINT) . "\n";
        }
    }

    /**
    * GET /order/123/payment/XX
    */
    public function getPayment(PaymentMethod $method, Order $order, array $data): void
    {
        // Фактическая работа делегируется объекту метода оплаты.
        $form = $method->getPaymentForm($order);
        echo "Controller: here's the payment form:\n";
        echo $form . "\n";
    }

    /**
    * GET /order/123/payment/XXX/return?key=AJHKSJHJ3423&success=true
    */
    public function getPaymentReturn(PaymentMethod $method, Order $order, array $data): void
    {
        try {
            // Другой тип работы, делегированный методу оплаты.
            if ($method->validateReturn($order, $data)) {
                echo "Controller: Thanks for your order!\n";
                $order->complete();
            }
        } catch (\Exception $e) {
            echo "Controller: got an exception (" . $e->getMessage() . ")\n";
        }
    }
}

/**
* Упрощенное представление класса Заказа.
*/

class Order
{
    /**
    * Для простоты, мы будем хранить все созданные заказы здесь...
    */
    private static $orders = [];

    /**
    * ...и получать к ним доступ отсюда.
    *
    * @param int $orderId
    * @return mixed
    */
    public static function get(int $orderId = null)
    {
        if ($orderId === null) {
            return static::$orders;
        } else {
            return static::$orders[$orderId];
        }
    }

    /**
    * Конструктор Заказа присваивает значения полям заказа. Чтобы всё было
    * просто, нет никакой проверки.
    *
    * @param array $attributes
    */
    public function __construct(array $attributes)
    {
        $this->id = count(static::$orders);
        $this->status = "new";
        foreach ($attributes as $key => $value) {
            $this->{$key} = $value;
        }
        static::$orders[$this->id] = $this;
    }

    /**
    * Метод позвонить при оплате заказа.
    */
    public function complete(): void
    {
        $this->status = "completed";
        echo "Order: #{$this->id} is now {$this->status}.";
    }
}

/**
* Этот класс помогает создать правильный объект стратегии для обработки
* платежа.
*/

class PaymentFactory
{
    /**
    * Получаем способ оплаты по его ID.
    *
    * @param $id
    * @return PaymentMethod
    * @throws \Exception
    */
    public static function getPaymentMethod(string $id): PaymentMethod
    {
        switch ($id) {
            case "cc":
                return new CreditCardPayment();
            case "paypal":
                return new PayPalPayment();
            default:
                throw new \Exception("Unknown Payment Method");
        }
    }
}

/**
* Интерфейс Стратегии описывает, как клиент может использовать различные
* Конкретные Стратегии.
*
* Обратите внимание, что в большинстве примеров, которые можно найти в
* интернете, стратегии чаще всего делают какую-нибудь мелочь в рамках одного
* метода.
*/

interface PaymentMethod
{
    public function getPaymentForm(Order $order): string;

    public function validateReturn(Order $order, array $data): bool;
}

/**
* Эта Конкретная Стратегия предоставляет форму оплаты и проверяет результаты
* платежей кредитными картам.
*/

class CreditCardPayment implements PaymentMethod
{
    static private $store_secret_key = "swordfish";

    public function getPaymentForm(Order $order): string
    {
        $returnURL = "https://our-website.com/" .
            "order/{$order->id}/payment/cc/return";
        return <<<FORM
<form action="https://my-credit-card-processor.com/charge" method="POST">
    <input type="hidden" id='email' value="{$order->email}">
    <input type="hidden" id='total' value="{$order->total}">
    <input type="hidden" id='returnURL' value="$returnURL">
    <input type="text" id="cardholder-name">
    <input type="text" id="credit-card">
    <input type="text" id="expiration-date">
    <input type="text" id="ccv-number">
    <input type="submit" value="Pay">
</form>
FORM;
    }

    public function validateReturn(Order $order, array $data): bool
    {
        echo "CreditCardPayment: ...validating... ";

        if ($data['key'] != md5($order->id . static::$store_secret_key)) {
            throw new \Exception("Payment key is wrong.");
        }

        if (!isset($data['success']) || !$data['success'] || $data['success'] == 'false') {
            throw new \Exception("Payment failed.");
        }

        // ...

        if (floatval($data['total']) < $order->total) {
            throw new \Exception("Payment amount is wrong.");
        }

        echo "Done!\n";

        return true;
    }
}

/**
* Эта Конкретная Стратегия предоставляет форму оплаты и проверяет результаты
* платежей PayPal.
*/

class PayPalPayment implements PaymentMethod
{
    public function getPaymentForm(Order $order): string
    {
        $returnURL = "https://our-website.com/" .
            "order/{$order->id}/payment/paypal/return";
        return <<<FORM
<form action="https://paypal.com/payment" method="POST">
    <input type="hidden" id='email' value="{$order->email}">
    <input type="hidden" id='total' value="{$order->total}">
    <input type="hidden" id='returnURL' value="$returnURL">
    <input type="submit" value="Pay on PayPal">
</form>
FORM;
    }

    public function validateReturn(Order $order, array $data): bool
    {
        echo "PayPalPayment: ...validating... ";

        // ...

        echo "Done!\n";

        return true;
    }
}

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

$controller = new OrderController();

echo "Client: Let's create some orders\n";

$controller->post("/orders", [
    "email" => "[email protected]",
    "product" => "ABC Cat food (XL)",
    "total" => 9.95,
]);

$controller->post("/orders", [
    "email" => "[email protected]",
    "product" => "XYZ Cat litter (XXL)",
    "total" => 19.95,
]);

echo "\nClient: List my orders, please\n";

$controller->get("/orders");

echo "\nClient: I'd like to pay for the second, show me the payment form\n";

$controller->get("/order/1/payment/paypal");

echo "\nClient: ...pushes the Pay button...\n";
echo "\nClient: Oh, I'm redirected to the PayPal.\n";
echo "\nClient: ...pays on the PayPal...\n";
echo "\nClient: Alright, I'm back with you, guys.\n";

$controller->get("/order/1/payment/paypal/return" .
    "?key=c55a3964833a4b0fa4469ea94a057152&success=true&total=19.95");

]]>
Client: Let's create some orders
Controller: POST request to /orders with {"email":"[email protected]",
    "product":"ABC Cat food (XL)","total":9.95}
Controller: Created the order #0.
Controller: POST request to /orders with {"email":"[email protected]",
    "product":"XYZ Cat litter (XXL)","total":19.95}
Controller: Created the order #1.

Client: List my orders, please
Controller: GET request to /orders
Controller: Here's all orders:
{
    "id": 0,
    "status": "new",
    "email": "[email protected]",
    "product": "ABC Cat food (XL)",
    "total": 9.95
}
{
    "id": 1,
    "status": "new",
    "email": "[email protected]",
    "product": "XYZ Cat litter (XXL)",
    "total": 19.95
}

Client: I'd like to pay for the second, show me the payment form
Controller: GET request to /order/1/payment/paypal
Controller: here's the payment form:
<form action="https://paypal.com/payment" method="POST">
    <input type="hidden" id="email" value="[email protected]">
    <input type="hidden" id="total" value="19.95">
    <input type="hidden" id="returnURL" value="https://our-website.com/order/1/payment/paypal/return">
    <input type="submit" value="Pay on PayPal">
</form>

Client: ...pushes the Pay button...

Client: Oh, I'm redirected to the PayPal.

Client: ...pays on the PayPal...

Client: Alright, I'm back with you, guys.
Controller: GET request to /order/1/payment/paypal/return?key=c55a3964833a4b0fa4469ea94a057152&success=true&total=19.95
PayPalPayment: ...validating... Done!
Controller: Thanks for your order!
Order: #1 is now completed.

Задачи

Задача 1. Сортировщик с выбором алгоритма

Реализуйте класс Sorter, который сортирует список с помощью сменной стратегии. Реализуйте стратегии: BubbleSort, QuickSort, MergeSort. Контекст должен позволять менять стратегию на лету и измерять время выполнения каждой.

from __future__ import annotations
from abc import ABC, abstractmethod
import time


class SortStrategy(ABC):
    @abstractmethod
    def sort(self, data: list) -> list: ...

    @property
    @abstractmethod
    def name(self) -> str: ...


class BubbleSort(SortStrategy):
    @property
    def name(self) -> str:
        return "BubbleSort"

    def sort(self, data: list) -> list:
        # TODO: реализовать сортировку пузырьком
        pass


class QuickSort(SortStrategy):
    @property
    def name(self) -> str:
        return "QuickSort"

    def sort(self, data: list) -> list:
        # TODO: реализовать быструю сортировку (рекурсивно)
        pass


class MergeSort(SortStrategy):
    @property
    def name(self) -> str:
        return "MergeSort"

    def sort(self, data: list) -> list:
        # TODO: реализовать сортировку слиянием
        pass


class Sorter:
    def __init__(self, strategy: SortStrategy) -> None:
        self._strategy = strategy

    def set_strategy(self, strategy: SortStrategy) -> None:
        self._strategy = strategy
        print(f"[Sorter] Стратегия изменена на {strategy.name}")

    def sort(self, data: list) -> list:
        # TODO: выполнить сортировку, вывести название стратегии и время
        pass

    def benchmark(self, data: list,
                  strategies: list[SortStrategy]) -> dict[str, float]:
        # TODO: запустить все стратегии на копии data, вернуть {name: time_ms}
        pass


# Ожидаемый результат:
# data = [64, 34, 25, 12, 22, 11, 90]
# sorter = Sorter(BubbleSort())
# print(sorter.sort(data))   # [11, 12, 22, 25, 34, 64, 90]
# sorter.set_strategy(QuickSort())
# print(sorter.sort(data))   # [11, 12, 22, 25, 34, 64, 90]
# import random
# big = random.sample(range(10000), 1000)
# results = sorter.benchmark(big, [BubbleSort(), QuickSort(), MergeSort()])
# for name, ms in results.items():
#     print(f"{name}: {ms:.2f}ms")
<?php
interface SortStrategy
{
    public function sort(array $data): array;
    public function getName(): string;
}

class BubbleSort implements SortStrategy
{
    public function getName(): string { return "BubbleSort"; }

    public function sort(array $data): array
    {
        // TODO: реализовать сортировку пузырьком
    }
}

class QuickSort implements SortStrategy
{
    public function getName(): string { return "QuickSort"; }

    public function sort(array $data): array
    {
        // TODO: реализовать быструю сортировку
    }
}

class MergeSort implements SortStrategy
{
    public function getName(): string { return "MergeSort"; }

    public function sort(array $data): array
    {
        // TODO: реализовать сортировку слиянием
    }
}

class Sorter
{
    private SortStrategy $strategy;

    public function __construct(SortStrategy $strategy)
    {
        $this->strategy = $strategy;
    }

    public function setStrategy(SortStrategy $strategy): void
    {
        $this->strategy = $strategy;
        echo "[Sorter] Стратегия изменена на {$strategy->getName()}\n";
    }

    public function sort(array $data): array
    {
        // TODO: выполнить сортировку, вывести название и время
    }

    public function benchmark(array $data, array $strategies): array
    {
        // TODO: запустить каждую стратегию, вернуть [name => time_ms]
    }
}

// Ожидаемый результат:
// $data = [64, 34, 25, 12, 22, 11, 90];
// $sorter = new Sorter(new BubbleSort());
// print_r($sorter->sort($data));   // [11, 12, 22, 25, 34, 64, 90]
// $sorter->setStrategy(new QuickSort());
// print_r($sorter->sort($data));   // [11, 12, 22, 25, 34, 64, 90]
// $big = range(1, 1000); shuffle($big);
// $results = $sorter->benchmark($big, [new BubbleSort(), new QuickSort(), new MergeSort()]);
// foreach ($results as $name => $ms) { echo "$name: {$ms}ms\n"; }

Задача 2. Калькулятор скидок

Реализуйте систему расчёта скидок для интернет-магазина. Стратегии: NoDiscount (без скидки), PercentDiscount (процент от суммы), FixedDiscount (фиксированная сумма), BulkDiscount (скидка при кол-ве > N), LoyaltyDiscount (скидка по баллам лояльности). Контекст Cart автоматически выбирает лучшую стратегию.

from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass


@dataclass
class CartItem:
    name: str
    price: float
    quantity: int

    @property
    def subtotal(self) -> float:
        return self.price * self.quantity


class DiscountStrategy(ABC):
    @abstractmethod
    def calculate(self, items: list[CartItem],
                  loyalty_points: int = 0) -> float:
        """Вернуть сумму скидки."""
        ...

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


class NoDiscount(DiscountStrategy):
    def calculate(self, items: list[CartItem],
                  loyalty_points: int = 0) -> float:
        return 0.0

    def describe(self) -> str:
        return "Без скидки"


class PercentDiscount(DiscountStrategy):
    def __init__(self, percent: float) -> None:
        self.percent = percent

    def calculate(self, items: list[CartItem],
                  loyalty_points: int = 0) -> float:
        # TODO: процент от общей суммы
        pass

    def describe(self) -> str:
        return f"Скидка {self.percent}%"


class FixedDiscount(DiscountStrategy):
    def __init__(self, amount: float) -> None:
        self.amount = amount

    def calculate(self, items: list[CartItem],
                  loyalty_points: int = 0) -> float:
        # TODO: фиксированная сумма, но не больше итога
        pass

    def describe(self) -> str:
        return f"Скидка -{self.amount}"


class BulkDiscount(DiscountStrategy):
    def __init__(self, min_quantity: int, percent: float) -> None:
        self.min_quantity = min_quantity
        self.percent = percent

    def calculate(self, items: list[CartItem],
                  loyalty_points: int = 0) -> float:
        # TODO: скидка если суммарное кол-во товаров >= min_quantity
        pass

    def describe(self) -> str:
        return f"Скидка {self.percent}% при заказе от {self.min_quantity} шт."


class LoyaltyDiscount(DiscountStrategy):
    POINTS_TO_CURRENCY = 0.01  # 1 балл = 0.01 единицы валюты

    def __init__(self, max_points_to_use: int) -> None:
        self.max_points = max_points_to_use

    def calculate(self, items: list[CartItem],
                  loyalty_points: int = 0) -> float:
        # TODO: использовать min(loyalty_points, max_points) * коэффициент
        pass

    def describe(self) -> str:
        return f"Скидка по баллам лояльности (макс. {self.max_points} баллов)"


class Cart:
    def __init__(self) -> None:
        self._items: list[CartItem] = []
        self._strategy: DiscountStrategy = NoDiscount()
        self.loyalty_points = 0

    def add_item(self, item: CartItem) -> None:
        self._items.append(item)

    def set_strategy(self, strategy: DiscountStrategy) -> None:
        self._strategy = strategy

    def set_best_strategy(self,
                           strategies: list[DiscountStrategy]) -> None:
        # TODO: выбрать стратегию с максимальной скидкой
        pass

    @property
    def subtotal(self) -> float:
        return sum(i.subtotal for i in self._items)

    @property
    def discount(self) -> float:
        return self._strategy.calculate(self._items, self.loyalty_points)

    @property
    def total(self) -> float:
        return max(0.0, self.subtotal - self.discount)

    def receipt(self) -> str:
        lines = [f"  {i.name} x{i.quantity} = {i.subtotal:.2f}"
                 for i in self._items]
        lines.append(f"Подытог: {self.subtotal:.2f}")
        lines.append(f"Скидка ({self._strategy.describe()}): -{self.discount:.2f}")
        lines.append(f"Итого: {self.total:.2f}")
        return "\n".join(lines)


# Ожидаемый результат:
# cart = Cart()
# cart.add_item(CartItem("Apple", 1.0, 10))
# cart.add_item(CartItem("Milk", 2.5, 3))
# cart.loyalty_points = 500
# strategies = [
#     NoDiscount(),
#     PercentDiscount(10),
#     FixedDiscount(3.0),
#     BulkDiscount(min_quantity=10, percent=15),
#     LoyaltyDiscount(max_points_to_use=300),
# ]
# cart.set_best_strategy(strategies)
# print(cart.receipt())
<?php
class CartItem
{
    public function __construct(
        public readonly string $name,
        public readonly float $price,
        public readonly int $quantity,
    ) {}

    public function getSubtotal(): float
    {
        return $this->price * $this->quantity;
    }
}

interface DiscountStrategy
{
    public function calculate(array $items, int $loyaltyPoints = 0): float;
    public function describe(): string;
}

class NoDiscount implements DiscountStrategy
{
    public function calculate(array $items, int $loyaltyPoints = 0): float { return 0.0; }
    public function describe(): string { return "Без скидки"; }
}

class PercentDiscount implements DiscountStrategy
{
    public function __construct(private readonly float $percent) {}

    public function calculate(array $items, int $loyaltyPoints = 0): float
    {
        // TODO: процент от общей суммы
    }

    public function describe(): string { return "Скидка {$this->percent}%"; }
}

class FixedDiscount implements DiscountStrategy
{
    public function __construct(private readonly float $amount) {}

    public function calculate(array $items, int $loyaltyPoints = 0): float
    {
        // TODO: фиксированная сумма, но не больше итога
    }

    public function describe(): string { return "Скидка -{$this->amount}"; }
}

class BulkDiscount implements DiscountStrategy
{
    public function __construct(
        private readonly int $minQuantity,
        private readonly float $percent,
    ) {}

    public function calculate(array $items, int $loyaltyPoints = 0): float
    {
        // TODO: скидка если суммарное кол-во >= minQuantity
    }

    public function describe(): string
    {
        return "Скидка {$this->percent}% при заказе от {$this->minQuantity} шт.";
    }
}

class LoyaltyDiscount implements DiscountStrategy
{
    private const POINTS_TO_CURRENCY = 0.01;

    public function __construct(private readonly int $maxPoints) {}

    public function calculate(array $items, int $loyaltyPoints = 0): float
    {
        // TODO: min(loyaltyPoints, maxPoints) * коэффициент
    }

    public function describe(): string
    {
        return "Скидка по баллам (макс. {$this->maxPoints})";
    }
}

class Cart
{
    private array $items = [];
    private DiscountStrategy $strategy;
    public int $loyaltyPoints = 0;

    public function __construct()
    {
        $this->strategy = new NoDiscount();
    }

    public function addItem(CartItem $item): void { $this->items[] = $item; }
    public function setStrategy(DiscountStrategy $strategy): void { $this->strategy = $strategy; }

    public function setBestStrategy(array $strategies): void
    {
        // TODO: выбрать стратегию с максимальной скидкой
    }

    public function getSubtotal(): float
    {
        return array_sum(array_map(fn($i) => $i->getSubtotal(), $this->items));
    }

    public function getDiscount(): float
    {
        return $this->strategy->calculate($this->items, $this->loyaltyPoints);
    }

    public function getTotal(): float
    {
        return max(0.0, $this->getSubtotal() - $this->getDiscount());
    }

    public function receipt(): string
    {
        $lines = array_map(
            fn($i) => "  {$i->name} x{$i->quantity} = {$i->getSubtotal()}",
            $this->items
        );
        $lines[] = "Подытог: " . $this->getSubtotal();
        $lines[] = "Скидка ({$this->strategy->describe()}): -" . $this->getDiscount();
        $lines[] = "Итого: " . $this->getTotal();
        return implode("\n", $lines);
    }
}

// Ожидаемый результат:
// $cart = new Cart();
// $cart->addItem(new CartItem("Apple", 1.0, 10));
// $cart->addItem(new CartItem("Milk", 2.5, 3));
// $cart->loyaltyPoints = 500;
// $strategies = [
//     new NoDiscount(),
//     new PercentDiscount(10),
//     new FixedDiscount(3.0),
//     new BulkDiscount(10, 15),
//     new LoyaltyDiscount(300),
// ];
// $cart->setBestStrategy($strategies);
// echo $cart->receipt();

Задача 3. Экспортер отчётов

Реализуйте систему экспорта отчётов в разные форматы. Контекст ReportExporter принимает данные (список строк-записей) и экспортирует через сменную стратегию. Стратегии: CsvExporter, JsonExporter, XmlExporter, MarkdownTableExporter. Добавьте возможность экспорта сразу в несколько форматов.

from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass


@dataclass
class ReportRow:
    id: int
    name: str
    value: float
    category: str


class ExportStrategy(ABC):
    @abstractmethod
    def export(self, rows: list[ReportRow], title: str = "Report") -> str: ...

    @property
    @abstractmethod
    def extension(self) -> str: ...


class CsvExporter(ExportStrategy):
    @property
    def extension(self) -> str:
        return "csv"

    def export(self, rows: list[ReportRow], title: str = "Report") -> str:
        # TODO: вернуть CSV-строку с заголовком id,name,value,category
        pass


class JsonExporter(ExportStrategy):
    @property
    def extension(self) -> str:
        return "json"

    def export(self, rows: list[ReportRow], title: str = "Report") -> str:
        # TODO: вернуть JSON {"title": ..., "rows": [...]}
        pass


class XmlExporter(ExportStrategy):
    @property
    def extension(self) -> str:
        return "xml"

    def export(self, rows: list[ReportRow], title: str = "Report") -> str:
        # TODO: вернуть XML-строку
        pass


class MarkdownTableExporter(ExportStrategy):
    @property
    def extension(self) -> str:
        return "md"

    def export(self, rows: list[ReportRow], title: str = "Report") -> str:
        # TODO: вернуть Markdown-таблицу с заголовком ## title
        pass


class ReportExporter:
    def __init__(self, strategy: ExportStrategy) -> None:
        self._strategy = strategy

    def set_strategy(self, strategy: ExportStrategy) -> None:
        self._strategy = strategy

    def export(self, rows: list[ReportRow],
               title: str = "Report") -> str:
        # TODO: делегировать стратегии и вернуть результат
        pass

    def export_all(self, rows: list[ReportRow],
                   strategies: list[ExportStrategy],
                   title: str = "Report") -> dict[str, str]:
        # TODO: вернуть {extension: content} для каждой стратегии
        pass


# Ожидаемый результат:
# rows = [
#     ReportRow(1, "Alice", 1500.0, "Sales"),
#     ReportRow(2, "Bob", 2300.5, "IT"),
#     ReportRow(3, "Carol", 1800.0, "Sales"),
# ]
# exporter = ReportExporter(CsvExporter())
# print(exporter.export(rows, "Q1 Report"))
# # id,name,value,category
# # 1,Alice,1500.0,Sales
# # ...
# exporter.set_strategy(MarkdownTableExporter())
# print(exporter.export(rows, "Q1 Report"))
# results = exporter.export_all(rows, [CsvExporter(), JsonExporter(), XmlExporter()], "Q1")
# print(list(results.keys()))  # ['csv', 'json', 'xml']
<?php
class ReportRow
{
    public function __construct(
        public readonly int $id,
        public readonly string $name,
        public readonly float $value,
        public readonly string $category,
    ) {}
}

interface ExportStrategy
{
    public function export(array $rows, string $title = "Report"): string;
    public function getExtension(): string;
}

class CsvExporter implements ExportStrategy
{
    public function getExtension(): string { return "csv"; }

    public function export(array $rows, string $title = "Report"): string
    {
        // TODO: вернуть CSV-строку с заголовком id,name,value,category
    }
}

class JsonExporter implements ExportStrategy
{
    public function getExtension(): string { return "json"; }

    public function export(array $rows, string $title = "Report"): string
    {
        // TODO: вернуть JSON {"title": ..., "rows": [...]}
    }
}

class XmlExporter implements ExportStrategy
{
    public function getExtension(): string { return "xml"; }

    public function export(array $rows, string $title = "Report"): string
    {
        // TODO: вернуть XML-строку
    }
}

class MarkdownTableExporter implements ExportStrategy
{
    public function getExtension(): string { return "md"; }

    public function export(array $rows, string $title = "Report"): string
    {
        // TODO: вернуть Markdown-таблицу с заголовком ## title
    }
}

class ReportExporter
{
    private ExportStrategy $strategy;

    public function __construct(ExportStrategy $strategy)
    {
        $this->strategy = $strategy;
    }

    public function setStrategy(ExportStrategy $strategy): void
    {
        $this->strategy = $strategy;
    }

    public function export(array $rows, string $title = "Report"): string
    {
        // TODO: делегировать стратегии
    }

    public function exportAll(array $rows, array $strategies,
                               string $title = "Report"): array
    {
        // TODO: вернуть [extension => content]
    }
}

// Ожидаемый результат:
// $rows = [
//     new ReportRow(1, "Alice", 1500.0, "Sales"),
//     new ReportRow(2, "Bob", 2300.5, "IT"),
//     new ReportRow(3, "Carol", 1800.0, "Sales"),
// ];
// $exporter = new ReportExporter(new CsvExporter());
// echo $exporter->export($rows, "Q1 Report");
// // id,name,value,category
// // 1,Alice,1500.0,Sales
// // ...
// $exporter->setStrategy(new MarkdownTableExporter());
// echo $exporter->export($rows, "Q1 Report");
// $results = $exporter->exportAll($rows, [new CsvExporter(), new JsonExporter(), new XmlExporter()], "Q1");
// print_r(array_keys($results));  // ['csv', 'json', 'xml']

Задача 4. Навигатор с маршрутными стратегиями

Реализуйте навигатор, прокладывающий маршруты между точками с помощью сменных стратегий. Стратегии: CarRoute (по дорогам, скорость 60 км/ч), WalkingRoute (пешком, 5 км/ч), BicycleRoute (велосипед, 15 км/ч), PublicTransportRoute (транспорт, с пересадками). Контекст выбирает стратегию по предпочтениям пользователя.

from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass
import math


@dataclass(frozen=True)
class Point:
    lat: float
    lon: float
    name: str = ""

    def distance_to(self, other: "Point") -> float:
        """Расстояние в километрах (упрощённая формула)."""
        dlat = (other.lat - self.lat) * 111.0
        dlon = (other.lon - self.lon) * 111.0 * math.cos(math.radians(self.lat))
        return math.sqrt(dlat ** 2 + dlon ** 2)


@dataclass
class Route:
    points: list[Point]
    distance_km: float
    duration_min: float
    description: str


class RoutingStrategy(ABC):
    @abstractmethod
    def build_route(self, origin: Point, destination: Point) -> Route: ...

    @property
    @abstractmethod
    def mode(self) -> str: ...


class CarRoute(RoutingStrategy):
    SPEED_KMH = 60.0

    @property
    def mode(self) -> str:
        return "Автомобиль"

    def build_route(self, origin: Point, destination: Point) -> Route:
        # TODO: прямой маршрут, скорость SPEED_KMH
        pass


class WalkingRoute(RoutingStrategy):
    SPEED_KMH = 5.0

    @property
    def mode(self) -> str:
        return "Пешком"

    def build_route(self, origin: Point, destination: Point) -> Route:
        # TODO: пешеходный маршрут
        pass


class BicycleRoute(RoutingStrategy):
    SPEED_KMH = 15.0

    @property
    def mode(self) -> str:
        return "Велосипед"

    def build_route(self, origin: Point, destination: Point) -> Route:
        # TODO: велосипедный маршрут
        pass


class PublicTransportRoute(RoutingStrategy):
    def __init__(self, stops: list[Point]) -> None:
        self.stops = stops  # промежуточные остановки

    @property
    def mode(self) -> str:
        return "Общественный транспорт"

    def build_route(self, origin: Point, destination: Point) -> Route:
        # TODO: маршрут через stops, скорость 30 км/ч + 5 мин на каждую пересадку
        pass


class Navigator:
    def __init__(self, strategy: RoutingStrategy) -> None:
        self._strategy = strategy

    def set_strategy(self, strategy: RoutingStrategy) -> None:
        self._strategy = strategy
        print(f"[Navigator] Режим: {strategy.mode}")

    def navigate(self, origin: Point, destination: Point) -> Route:
        # TODO: построить маршрут и вывести информацию
        pass

    def compare_routes(self, origin: Point, destination: Point,
                       strategies: list[RoutingStrategy]) -> None:
        # TODO: вывести сравнительную таблицу всех стратегий
        pass


# Ожидаемый результат:
# home = Point(55.751, 37.618, "Дом")
# work = Point(55.796, 37.537, "Работа")
# nav = Navigator(CarRoute())
# route = nav.navigate(home, work)
# print(f"{route.description}: {route.distance_km:.1f} км, {route.duration_min:.0f} мин")
# nav.set_strategy(WalkingRoute())
# nav.compare_routes(home, work, [CarRoute(), WalkingRoute(), BicycleRoute()])
<?php
class Point
{
    public function __construct(
        public readonly float $lat,
        public readonly float $lon,
        public readonly string $name = "",
    ) {}

    public function distanceTo(Point $other): float
    {
        $dlat = ($other->lat - $this->lat) * 111.0;
        $dlon = ($other->lon - $this->lon) * 111.0 * cos(deg2rad($this->lat));
        return sqrt($dlat ** 2 + $dlon ** 2);
    }
}

class Route
{
    public function __construct(
        public readonly array $points,
        public readonly float $distanceKm,
        public readonly float $durationMin,
        public readonly string $description,
    ) {}
}

interface RoutingStrategy
{
    public function buildRoute(Point $origin, Point $destination): Route;
    public function getMode(): string;
}

class CarRoute implements RoutingStrategy
{
    private const SPEED = 60.0;

    public function getMode(): string { return "Автомобиль"; }

    public function buildRoute(Point $origin, Point $destination): Route
    {
        // TODO: прямой маршрут, скорость SPEED км/ч
    }
}

class WalkingRoute implements RoutingStrategy
{
    private const SPEED = 5.0;

    public function getMode(): string { return "Пешком"; }

    public function buildRoute(Point $origin, Point $destination): Route
    {
        // TODO: пешеходный маршрут
    }
}

class BicycleRoute implements RoutingStrategy
{
    private const SPEED = 15.0;

    public function getMode(): string { return "Велосипед"; }

    public function buildRoute(Point $origin, Point $destination): Route
    {
        // TODO: велосипедный маршрут
    }
}

class PublicTransportRoute implements RoutingStrategy
{
    public function __construct(private readonly array $stops = []) {}

    public function getMode(): string { return "Общественный транспорт"; }

    public function buildRoute(Point $origin, Point $destination): Route
    {
        // TODO: маршрут через stops, 30 км/ч + 5 мин на пересадку
    }
}

class Navigator
{
    private RoutingStrategy $strategy;

    public function __construct(RoutingStrategy $strategy)
    {
        $this->strategy = $strategy;
    }

    public function setStrategy(RoutingStrategy $strategy): void
    {
        $this->strategy = $strategy;
        echo "[Navigator] Режим: {$strategy->getMode()}\n";
    }

    public function navigate(Point $origin, Point $destination): Route
    {
        // TODO: построить маршрут и вывести информацию
    }

    public function compareRoutes(Point $origin, Point $destination,
                                   array $strategies): void
    {
        // TODO: вывести сравнительную таблицу
    }
}

// Ожидаемый результат:
// $home = new Point(55.751, 37.618, "Дом");
// $work = new Point(55.796, 37.537, "Работа");
// $nav = new Navigator(new CarRoute());
// $route = $nav->navigate($home, $work);
// echo "{$route->description}: {$route->distanceKm} км, {$route->durationMin} мин\n";
// $nav->compareRoutes($home, $work, [new CarRoute(), new WalkingRoute(), new BicycleRoute()]);

Задача 5. Валидатор с цепочкой стратегий

Реализуйте систему валидации данных пользователя. Каждое поле валидируется набором стратегий. Стратегии: RequiredValidator, LengthValidator, RegexValidator, RangeValidator, UniqueValidator. Контекст FieldValidator применяет стратегии последовательно и собирает все ошибки.

from __future__ import annotations
from abc import ABC, abstractmethod
import re


class ValidationStrategy(ABC):
    @abstractmethod
    def validate(self, value, context: dict | None = None) -> str | None:
        """Вернуть сообщение об ошибке или None если валидно."""
        ...


class RequiredValidator(ValidationStrategy):
    def validate(self, value, context: dict | None = None) -> str | None:
        # TODO: вернуть ошибку если value пустое (None, "", [], 0)
        pass


class LengthValidator(ValidationStrategy):
    def __init__(self, min_len: int = 0,
                 max_len: int | None = None) -> None:
        self.min_len = min_len
        self.max_len = max_len

    def validate(self, value, context: dict | None = None) -> str | None:
        # TODO: проверить длину строки
        pass


class RegexValidator(ValidationStrategy):
    def __init__(self, pattern: str, message: str) -> None:
        self._pattern = re.compile(pattern)
        self.message = message

    def validate(self, value, context: dict | None = None) -> str | None:
        # TODO: проверить совпадение с pattern
        pass


class RangeValidator(ValidationStrategy):
    def __init__(self, min_val: float | None = None,
                 max_val: float | None = None) -> None:
        self.min_val = min_val
        self.max_val = max_val

    def validate(self, value, context: dict | None = None) -> str | None:
        # TODO: проверить числовое значение в диапазоне
        pass


class UniqueValidator(ValidationStrategy):
    def __init__(self, existing: set) -> None:
        self.existing = existing

    def validate(self, value, context: dict | None = None) -> str | None:
        # TODO: проверить отсутствие value в existing
        pass


class FieldValidator:
    def __init__(self, field_name: str) -> None:
        self.field_name = field_name
        self._strategies: list[ValidationStrategy] = []

    def add(self, strategy: ValidationStrategy) -> "FieldValidator":
        # TODO: добавить стратегию, вернуть self для цепочки
        pass

    def validate(self, value,
                 context: dict | None = None) -> list[str]:
        # TODO: применить все стратегии, собрать список ошибок
        pass

    def is_valid(self, value, context: dict | None = None) -> bool:
        return len(self.validate(value, context)) == 0


class FormValidator:
    def __init__(self) -> None:
        self._fields: dict[str, FieldValidator] = {}

    def add_field(self, validator: FieldValidator) -> "FormValidator":
        self._fields[validator.field_name] = validator
        return self

    def validate(self, data: dict) -> dict[str, list[str]]:
        # TODO: валидировать каждое поле, вернуть {field: [errors]}
        pass

    def is_valid(self, data: dict) -> bool:
        return all(len(e) == 0 for e in self.validate(data).values())


# Ожидаемый результат:
# existing_emails = {"[email protected]", "[email protected]"}
# form = FormValidator()
# form.add_field(
#     FieldValidator("username")
#     .add(RequiredValidator())
#     .add(LengthValidator(min_len=3, max_len=20))
#     .add(RegexValidator(r"^[a-zA-Z0-9_]+$", "только буквы, цифры и _"))
# ).add_field(
#     FieldValidator("email")
#     .add(RequiredValidator())
#     .add(RegexValidator(r"^[^@]+@[^@]+\.[^@]+$", "неверный формат email"))
#     .add(UniqueValidator(existing_emails))
# ).add_field(
#     FieldValidator("age")
#     .add(RequiredValidator())
#     .add(RangeValidator(min_val=18, max_val=120))
# )
# errors = form.validate({"username": "Al", "email": "[email protected]", "age": 15})
# print(errors)
# # {'username': ['длина должна быть от 3 до 20 символов'],
# #  'email': ['значение уже занято'],
# #  'age': ['значение должно быть не менее 18']}
<?php
interface ValidationStrategy
{
    /** Вернуть сообщение об ошибке или null если валидно. */
    public function validate(mixed $value, array $context = []): ?string;
}

class RequiredValidator implements ValidationStrategy
{
    public function validate(mixed $value, array $context = []): ?string
    {
        // TODO: ошибка если пустое значение
    }
}

class LengthValidator implements ValidationStrategy
{
    public function __construct(
        private readonly int $minLen = 0,
        private readonly ?int $maxLen = null,
    ) {}

    public function validate(mixed $value, array $context = []): ?string
    {
        // TODO: проверить длину строки
    }
}

class RegexValidator implements ValidationStrategy
{
    public function __construct(
        private readonly string $pattern,
        private readonly string $message,
    ) {}

    public function validate(mixed $value, array $context = []): ?string
    {
        // TODO: проверить совпадение с pattern
    }
}

class RangeValidator implements ValidationStrategy
{
    public function __construct(
        private readonly ?float $minVal = null,
        private readonly ?float $maxVal = null,
    ) {}

    public function validate(mixed $value, array $context = []): ?string
    {
        // TODO: проверить числовое значение в диапазоне
    }
}

class UniqueValidator implements ValidationStrategy
{
    public function __construct(private readonly array $existing) {}

    public function validate(mixed $value, array $context = []): ?string
    {
        // TODO: проверить отсутствие в existing
    }
}

class FieldValidator
{
    private array $strategies = [];

    public function __construct(private readonly string $fieldName) {}

    public function getFieldName(): string { return $this->fieldName; }

    public function add(ValidationStrategy $strategy): static
    {
        // TODO: добавить стратегию, вернуть $this
    }

    public function validate(mixed $value, array $context = []): array
    {
        // TODO: применить все стратегии, собрать ошибки
    }

    public function isValid(mixed $value, array $context = []): bool
    {
        return empty($this->validate($value, $context));
    }
}

class FormValidator
{
    private array $fields = [];

    public function addField(FieldValidator $validator): static
    {
        $this->fields[$validator->getFieldName()] = $validator;
        return $this;
    }

    public function validate(array $data): array
    {
        // TODO: валидировать все поля, вернуть [field => [errors]]
    }

    public function isValid(array $data): bool
    {
        return empty(array_filter($this->validate($data)));
    }
}

// Ожидаемый результат:
// $existingEmails = ["[email protected]", "[email protected]"];
// $form = (new FormValidator())
//     ->addField(
//         (new FieldValidator("username"))
//             ->add(new RequiredValidator())
//             ->add(new LengthValidator(minLen: 3, maxLen: 20))
//             ->add(new RegexValidator('/^[a-zA-Z0-9_]+$/', "только буквы, цифры и _"))
//     )
//     ->addField(
//         (new FieldValidator("email"))
//             ->add(new RequiredValidator())
//             ->add(new RegexValidator('/^[^@]+@[^@]+\.[^@]+$/', "неверный формат"))
//             ->add(new UniqueValidator($existingEmails))
//     )
//     ->addField(
//         (new FieldValidator("age"))
//             ->add(new RequiredValidator())
//             ->add(new RangeValidator(minVal: 18, maxVal: 120))
//     );
// $errors = $form->validate(["username" => "Al", "email" => "[email protected]", "age" => 15]);
// print_r($errors);
// // ['username' => ['длина должна быть от 3 до 20'],
// //  'email'    => ['значение уже занято'],
// //  'age'      => ['значение должно быть не менее 18']]