PHP: Стратегія

Strategy Стратегія Strategy

Стратегія — це поведінковий патерн, який виносить набір алгоритмів у власні класи і робить їх взаємозамінними.

Інші об'єкти містять посилання на об'єкт-стратегію та делегують їй роботу. Програма може підмінити цей об'єкт іншим, якщо потрібен інший спосіб вирішення завдання.

Детальніше про Стратегію

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

Складність:

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

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

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

Цей приклад показує структуру патерну Стратегія, а саме — з яких класів він складається, які ролі ці класи виконують і як вони взаємодіють один з одним. Після ознайомлення зі структурою, вам буде легше сприймати наступний приклад, що розглядає реальний випадок використання патерну в світі PHP.

StrategyStructural.php: Приклад структури патерну

<?php

namespace RefactoringGuru\Strategy\Structural;

/**
 * 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 __constructor(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()
    {
        // ...

        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");

        // ...
    }
}

/**
 * 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($data);
}

/**
 * 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($data)
    {
        sort($data);

        return $data;
    }
}

class ConcreteStrategyB implements Strategy
{
    public function doAlgorithm($data)
    {
        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();

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;

/**
 * Strategy Design Pattern
 *
 * Intent: Define a family of algorithms, encapsulate each one, and make them
 * interchangeable. Strategy lets the algorithm vary independently from clients
 * that use it.
 *
 * Example: In this example, the Strategy pattern is used to represent payment
 * methods in an e-commerce application.
 *
 * Each payment method can display a payment form to collect proper payment
 * details from a user and send it to the payment processing company. Then,
 * after the payment processing company redirects the user back to our website,
 * the payment method validates the return parameters and helps to decide
 * whether the order was completed.
 */

/**
 * 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($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");
        }
    }

    /**
     * Handle GET requests.
     *
     * @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]);

            // 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 {
            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)
    {
        // The actual work is delegated to the payment method object.
        $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 {
            // Another type of work delegated to the payment method.
            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");
        }
    }
}

/**
 * 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()
    {
        $this->status = "completed";
        print("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($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, $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, $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;
    }
}

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

        // ...

        print("Done!\n");

        return true;
    }
}

/**
 * The client code.
 */

$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.