Стратегия — это поведенческий паттерн, выносит набор алгоритмов в собственные классы и делает их взаимозаменимыми.
Другие объекты содержат ссылку на объект-стратегию и делегируют ей работу. Программа может подменить этот объект другим, если требуется иной способ решения задачи.
Концептуальный пример
Этот пример показывает структуру паттерна Стратегия , а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом.
После ознакомления со структурой, вам будет легче воспринимать второй пример, который рассматривает реальный случай использования паттерна в мире 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.