
PHP 策略模式讲解和代码示例
策略是一种行为设计模式, 它将一组行为转换为对象, 并使其在原始上下文对象内部能够相互替换。
原始对象被称为上下文, 它包含指向策略对象的引用并将执行行为的任务分派给策略对象。 为了改变上下文完成其工作的方式, 其他对象可以使用另一个对象来替换当前链接的策略对象。
复杂度:
流行度:
使用示例: PHP 代码中经常使用策略模式, 特别是必须在运行时切换算法的情形中。 但是, PHP 从 2009 年开始支持匿名函数, 以其为代表的强大竞争对手挑战着模式的使用。
识别方法: 策略模式可以通过允许嵌套对象完成实际工作的方法以及允许将该对象替换为不同对象的设置器来识别。
概念示例
本例说明了策略设计模式的结构并重点回答了下面的问题:
- 它由哪些类组成?
- 这些类扮演了哪些角色?
- 模式中的各个元素会以何种方式相互关联?
了解该模式的结构后, 你可以更容易地理解下面基于真实世界的 PHP 应用案例。
index.php: 概念示例
<?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: 执行结果
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; /** * 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 { private static $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: 执行结果
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.