La Stratégie est un patron de conception comportemental qui transforme un ensemble de comportements en objets, et les rend interchangeables à l’intérieur de l’objet du contexte original.
L’objet original, que l’on appelle contexte, garde une référence vers un objet stratégie et lui délègue l’exécution du comportement. Les autres objets doivent remplacer l’objet stratégie associé afin de modifier la manière dont le contexte fonctionne.
Exemple conceptuel
Dans cet exemple, nous allons voir la structure de la Stratégie et répondre aux questions suivantes :
Que contiennent les classes ?
Quels rôles jouent-elles ?
Comment les éléments du patron sont-ils reliés ?
Après avoir étudié la structure du patron, vous pourrez plus facilement comprendre l’exemple suivant qui est basé sur un cas d’utilisation réel en PHP.
index.php: Exemple conceptuel
<?php
namespace RefactoringGuru\Strategy\Conceptual;
/**
* The Context defines the interface of interest to clients.
*/
class Context
{
/**
* @var Strategy The Context maintains a reference to one of the Strategy
* objects. The Context does not know the concrete class of a strategy. It
* should work with all strategies via the Strategy interface.
*/
private $strategy;
/**
* Usually, the Context accepts a strategy through the constructor, but also
* provides a setter to change it at runtime.
*/
public function __construct(Strategy $strategy)
{
$this->strategy = $strategy;
}
/**
* Usually, the Context allows replacing a Strategy object at runtime.
*/
public function setStrategy(Strategy $strategy)
{
$this->strategy = $strategy;
}
/**
* The Context delegates some work to the Strategy object instead of
* implementing multiple versions of the algorithm on its own.
*/
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";
// ...
}
}
/**
* The Strategy interface declares operations common to all supported versions
* of some algorithm.
*
* The Context uses this interface to call the algorithm defined by Concrete
* Strategies.
*/
interface Strategy
{
public function doAlgorithm(array $data): array;
}
/**
* Concrete Strategies implement the algorithm while following the base Strategy
* interface. The interface makes them interchangeable in the Context.
*/
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;
}
}
/**
* The client code picks a concrete strategy and passes it to the context. The
* client should be aware of the differences between strategies in order to make
* the right choice.
*/
$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: Résultat de l’exécution
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
Analogie du monde réel
Dans cet exemple, la Stratégie est utilisée pour représenter les diverses méthodes de paiement pour une boutique en ligne.
Chaque méthode de paiement peut afficher un formulaire pour récupérer les informations de paiement de l’utilisateur, puis les envoie à la société qui s’en occupe. Une fois le paiement effectué, la société redirige l’utilisateur vers notre site Web. La méthode de paiement contrôle les paramètres de retour et en déduit si la commande a été validée ou non.
index.php: Exemple du monde réel
<?php
namespace RefactoringGuru\Strategy\RealWorld;
/**
* This is the router and controller of our application. Upon receiving a
* request, this class decides what behavior should be executed. When the app
* receives a payment request, the OrderController class also decides which
* payment method it should use to process the request. Thus, the class acts as
* the Context and the Client at the same time.
*/
class OrderController
{
/**
* Handle POST requests.
*
* @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";
}
}
/**
* Handle GET requests.
*
* @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]);
// The payment method (strategy) is selected according to the value
// passed along with the request.
$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
{
// The actual work is delegated to the payment method object.
$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 {
// Another type of work delegated to the payment method.
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";
}
}
}
/**
* A simplified representation of the Order class.
*/
class Order
{
/**
* For the sake of simplicity, we'll store all created orders here...
*
* @var array
*/
private static $orders = [];
/**
* ...and access them from here.
*
* @param int $orderId
* @return mixed
*/
public static function get(int $orderId = null)
{
if ($orderId === null) {
return static::$orders;
} else {
return static::$orders[$orderId];
}
}
/**
* The Order constructor assigns the values of the order's fields. To keep
* things simple, there is no validation whatsoever.
*
* @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;
}
/**
* The method to call when an order gets paid.
*/
public function complete(): void
{
$this->status = "completed";
echo "Order: #{$this->id} is now {$this->status}.";
}
}
/**
* This class helps to produce a proper strategy object for handling a payment.
*/
class PaymentFactory
{
/**
* Get a payment method by its 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");
}
}
}
/**
* The Strategy interface describes how a client can use various Concrete
* Strategies.
*
* Note that in most examples you can find on the Web, strategies tend to do
* some tiny thing within one method. However, in reality, your strategies can
* be much more robust (by having several methods, for example).
*/
interface PaymentMethod
{
public function getPaymentForm(Order $order): string;
public function validateReturn(Order $order, array $data): bool;
}
/**
* This Concrete Strategy provides a payment form and validates returns for
* credit card payments.
*/
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;
}
}
/**
* This Concrete Strategy provides a payment form and validates returns for
* PayPal payments.
*/
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;
}
}
/**
* The client code.
*/
$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: Résultat de l’exécution
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.