PHP: Стратегия

Strategy Стратегия Strategy

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

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

Подробней о Стратегии

Особенности паттерна на PHP

Сложность:

Популярность:

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

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

Пример: Структура паттерна

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

StrategyStructural.php: Пример структуры паттерна

<?php

namespace RefactoringGuru\Strategy\Structural;

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

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

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

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

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

        // ...
    }
}

/**
 * Интерфейс Стратегии объявляет операции, общие для всех поддерживаемых версий
 * некоторого алгоритма.
 *
 * Контекст использует этот интерфейс для вызова алгоритма, определённого
 * Конкретными Стратегиями.
 */
interface Strategy
{
    public function doAlgorithm($data);
}

/**
 * Конкретные Стратегии реализуют алгоритм, следуя базовому интерфейсу
 * Стратегии. Этот интерфейс делает их взаимозаменяемыми в Контексте.
 */
class ConcreteStrategyA implements Strategy
{
    public function doAlgorithm($data)
    {
        sort($data);

        return $data;
    }
}

class ConcreteStrategyB implements Strategy
{
    public function doAlgorithm($data)
    {
        rsort($data);

        return $data;
    }
}

/**
 * Клиентский код выбирает конкретную стратегию и передаёт её в контекст. Клиент
 * должен знать о различиях между стратегиями, чтобы сделать правильный выбор.
 */
$context = new Context();

print("Client: Strategy is set to normal sorting.\n");
$context->setStrategy(new ConcreteStrategyA());
$context->doSomeBusinessLogic();
print("\n");

print("Client: Strategy is set to reverse sorting.\n");
$context->setStrategy(new ConcreteStrategyB());
$context->doSomeBusinessLogic();

Output.txt: Результат выполнения

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

Пример: Пример из жизни

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

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

StrategyRealWorld.php: Пример из жизни

<?php

namespace RefactoringGuru\Strategy\RealWorld;

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

/**
 * Это роутер и контроллер нашего приложения. Получив запрос, этот класс решает,
 * какое поведение должно выполняться. Когда приложение получает требование об
 * оплате, класс OrderController также решает, какой способ оплаты следует
 * использовать для его обработки. Таким образом, этот класс действует как
 * Контекст и в то же время как Клиент.
 */
class OrderController
{
    /**
     * Обрабатываем запросы POST.
     *
     * @param $url
     * @param $data
     * @throws \Exception
     */
    public function post($url, $data)
    {
        print("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 {
            print("Controller: 404 page\n");
        }
    }

    /**
     * Обрабатываем запросы GET.
     *
     * @param $url
     * @throws \Exception
     */
    public function get($url)
    {
        print("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 {
            print("Controller: 404 page\n");
        }
    }

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

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

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

    /**
     * GET /order/123/payment/XXX/return?key=AJHKSJHJ3423&success=true
     */
    public function getPaymentReturn(PaymentMethod $method, Order $order, array $data)
    {
        try {
            // Другой тип работы, делегированный методу оплаты.
            if ($method->validateReturn($order, $data)) {
                print("Controller: Thanks for your order!\n");
                $order->complete();
            }
        } catch (\Exception $e) {
            print("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()
    {
        $this->status = "completed";
        print("Order: #{$this->id} is now {$this->status}.");
    }
}

/**
 * Этот класс помогает создать правильный объект стратегии для обработки
 * платежа.
 */
class PaymentFactory
{
    /**
     * Получаем способ оплаты по его ID.
     *
     * @param $id
     * @return PaymentMethod
     * @throws \Exception
     */
    public static function getPaymentMethod($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, $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, $data): bool
    {
        print("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.");
        }

        print("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, $data): bool
    {
        print("PayPalPayment: ...validating... ");

        // ...

        print("Done!\n");

        return true;
    }
}

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

$controller = new OrderController();

print("Client: Let's create some orders\n");

$controller->post("/orders", [
    "email" => "me@example.com",
    "product" => "ABC Cat food (XL)",
    "total" => 9.95,
]);

$controller->post("/orders", [
    "email" => "me@example.com",
    "product" => "XYZ Cat litter (XXL)",
    "total" => 19.95,
]);

print("\nClient: List my orders, please\n");

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

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

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

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

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

Output.txt: Результат выполнения

Client: Let's create some orders
Controller: POST request to /orders with {"email":"me@example.com","product":"ABC Cat food (XL)","total":9.95}
Controller: Created the order #0.
Controller: POST request to /orders with {"email":"me@example.com","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": "me@example.com",
    "product": "ABC Cat food (XL)",
    "total": 9.95
}
{
    "id": 1,
    "status": "new",
    "email": "me@example.com",
    "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="me@example.com">
    <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.