
Стратегия на PHP
Стратегия — это поведенческий паттерн, выносит набор алгоритмов в собственные классы и делает их взаимозаменимыми.
Другие объекты содержат ссылку на объект-стратегию и делегируют ей работу. Программа может подменить этот объект другим, если требуется иной способ решения задачи.
Сложность:
Популярность:
Применимость: Стратегию часто используют в PHP-коде, особенно там, где нужно подменять алгоритм во время выполнения программы. Но у паттерна есть довольно сильный конкурент в лице анонимных функций, которые PHP уже довольно давно поддерживает.
Признаки применения паттерна: Класс делегирует выполнение вложенному объекту абстрактного типа или интерфейса.
Концептуальный пример
Этот пример показывает структуру паттерна Стратегия, а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом.
После ознакомления со структурой, вам будет легче воспринимать второй пример, который рассматривает реальный случай использования паттерна в мире PHP.
index.php: Пример структуры паттерна
<?php namespace RefactoringGuru\Strategy\Conceptual; /** * Контекст определяет интерфейс, представляющий интерес для клиентов. */ 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();
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
Пример из реальной жизни
В этом примере паттерн Стратегия используется для представления способов оплаты в приложении электронной коммерции.
Каждый способ оплаты может отображать форму оплаты для сбора надлежащих платёжных реквизитов пользователя и отправки его в компанию по обработке платежей. После того, как компания по обработке платежей перенаправляет пользователя обратно на сайт, метод оплаты проверяет возвращаемые параметры и помогает решить, был ли заказ завершён.
index.php: Пример из реальной жизни
<?php namespace RefactoringGuru\Strategy\RealWorld; /** * Это роутер и контроллер нашего приложения. Получив запрос, этот класс решает, * какое поведение должно выполняться. Когда приложение получает требование об * оплате, класс 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" => "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, ]); 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");
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.