Laravel 13 策略模式深度解析
策略模式是一种行为型设计模式,它定义了一系列算法,将每个算法封装起来,并使它们可以互相替换。本文将深入探讨 Laravel 13 中策略模式的高级用法。
策略模式基础
什么是策略模式
策略模式允许在运行时选择算法的行为,使算法的变化不会影响到使用算法的客户端。
1 2 3 4 5 6 7 8 9 10
| <?php
namespace App\Contracts\Strategies;
interface StrategyInterface { public function execute(mixed $data): mixed;
public function getName(): string; }
|
基础策略实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <?php
namespace App\Contracts\Strategies;
interface PaymentStrategy { public function pay(float $amount, array $options = []): PaymentResult;
public function refund(string $transactionId, float $amount = null): bool;
public function getName(): string;
public function getFee(float $amount): float; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| <?php
namespace App\Strategies\Payment;
use App\Contracts\Strategies\PaymentStrategy;
class CreditCardStrategy implements PaymentStrategy { protected float $feeRate = 0.029;
public function pay(float $amount, array $options = []): PaymentResult { $result = Stripe::charge([ 'amount' => $amount * 100, 'currency' => $options['currency'] ?? 'usd', 'source' => $options['token'], 'description' => $options['description'] ?? '', ]);
return new PaymentResult( success: $result->status === 'succeeded', transactionId: $result->id, amount: $amount, fee: $this->getFee($amount) ); }
public function refund(string $transactionId, float $amount = null): bool { $refund = Stripe::refund($transactionId, $amount ? $amount * 100 : null); return $refund->status === 'succeeded'; }
public function getName(): string { return 'credit_card'; }
public function getFee(float $amount): float { return $amount * $this->feeRate; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| <?php
namespace App\Strategies\Payment;
use App\Contracts\Strategies\PaymentStrategy;
class PayPalStrategy implements PaymentStrategy { protected float $feeRate = 0.034;
public function pay(float $amount, array $options = []): PaymentResult { $payment = PayPal::createPayment([ 'intent' => 'sale', 'amount' => [ 'total' => $amount, 'currency' => $options['currency'] ?? 'USD', ], ]);
return new PaymentResult( success: $payment->state === 'approved', transactionId: $payment->id, amount: $amount, fee: $this->getFee($amount) ); }
public function refund(string $transactionId, float $amount = null): bool { $refund = PayPal::refund($transactionId, $amount); return $refund->state === 'completed'; }
public function getName(): string { return 'paypal'; }
public function getFee(float $amount): float { return $amount * $this->feeRate; } }
|
策略上下文
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| <?php
namespace App\Services;
use App\Contracts\Strategies\PaymentStrategy; use InvalidArgumentException;
class PaymentContext { protected array $strategies = []; protected ?PaymentStrategy $currentStrategy = null;
public function registerStrategy(PaymentStrategy $strategy): self { $this->strategies[$strategy->getName()] = $strategy; return $this; }
public function setStrategy(string $name): self { if (!isset($this->strategies[$name])) { throw new InvalidArgumentException("Payment strategy [{$name}] not found."); }
$this->currentStrategy = $this->strategies[$name]; return $this; }
public function pay(float $amount, array $options = []): PaymentResult { $this->ensureStrategySet();
return $this->currentStrategy->pay($amount, $options); }
public function refund(string $transactionId, float $amount = null): bool { $this->ensureStrategySet();
return $this->currentStrategy->refund($transactionId, $amount); }
public function getAvailableStrategies(): array { return array_keys($this->strategies); }
public function getStrategyFee(string $name, float $amount): float { if (!isset($this->strategies[$name])) { throw new InvalidArgumentException("Payment strategy [{$name}] not found."); }
return $this->strategies[$name]->getFee($amount); }
protected function ensureStrategySet(): void { if ($this->currentStrategy === null) { throw new \RuntimeException('No payment strategy has been set.'); } } }
|
价格计算策略
1 2 3 4 5 6 7 8 9 10 11 12
| <?php
namespace App\Contracts\Strategies;
interface PricingStrategy { public function calculate(float $basePrice, array $context = []): float;
public function getName(): string;
public function getDescription(): string; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <?php
namespace App\Strategies\Pricing;
use App\Contracts\Strategies\PricingStrategy;
class StandardPricingStrategy implements PricingStrategy { public function calculate(float $basePrice, array $context = []): float { return $basePrice; }
public function getName(): string { return 'standard'; }
public function getDescription(): string { return 'Standard pricing without any discounts'; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| <?php
namespace App\Strategies\Pricing;
use App\Contracts\Strategies\PricingStrategy;
class VipPricingStrategy implements PricingStrategy { protected float $discountRate;
public function __construct(float $discountRate = 0.15) { $this->discountRate = $discountRate; }
public function calculate(float $basePrice, array $context = []): float { return $basePrice * (1 - $this->discountRate); }
public function getName(): string { return 'vip'; }
public function getDescription(): string { return sprintf('VIP pricing with %.0f%% discount', $this->discountRate * 100); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| <?php
namespace App\Strategies\Pricing;
use App\Contracts\Strategies\PricingStrategy;
class WholesalePricingStrategy implements PricingStrategy { protected array $tierDiscounts = [ 10 => 0.05, 50 => 0.10, 100 => 0.15, 500 => 0.20, ];
public function calculate(float $basePrice, array $context = []): float { $quantity = $context['quantity'] ?? 1; $discount = $this->getDiscountForQuantity($quantity);
return $basePrice * $quantity * (1 - $discount); }
protected function getDiscountForQuantity(int $quantity): float { $discount = 0;
foreach ($this->tierDiscounts as $threshold => $rate) { if ($quantity >= $threshold) { $discount = $rate; } }
return $discount; }
public function getName(): string { return 'wholesale'; }
public function getDescription(): string { return 'Wholesale pricing with tiered quantity discounts'; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| <?php
namespace App\Strategies\Pricing;
use App\Contracts\Strategies\PricingStrategy;
class PromotionalPricingStrategy implements PricingStrategy { protected float $discountRate; protected ?\DateTime $expiresAt;
public function __construct(float $discountRate, ?\DateTime $expiresAt = null) { $this->discountRate = $discountRate; $this->expiresAt = $expiresAt; }
public function calculate(float $basePrice, array $context = []): float { if ($this->isExpired()) { return $basePrice; }
return $basePrice * (1 - $this->discountRate); }
protected function isExpired(): bool { return $this->expiresAt !== null && new \DateTime() > $this->expiresAt; }
public function getName(): string { return 'promotional'; }
public function getDescription(): string { return sprintf('Promotional pricing with %.0f%% off', $this->discountRate * 100); } }
|
排序策略
1 2 3 4 5 6 7 8 9 10 11 12
| <?php
namespace App\Contracts\Strategies;
use Illuminate\Support\Collection;
interface SortingStrategy { public function sort(Collection $items): Collection;
public function getName(): string; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <?php
namespace App\Strategies\Sorting;
use App\Contracts\Strategies\SortingStrategy; use Illuminate\Support\Collection;
class PriceAscendingStrategy implements SortingStrategy { public function sort(Collection $items): Collection { return $items->sortBy('price')->values(); }
public function getName(): string { return 'price_asc'; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <?php
namespace App\Strategies\Sorting;
use App\Contracts\Strategies\SortingStrategy; use Illuminate\Support\Collection;
class PriceDescendingStrategy implements SortingStrategy { public function sort(Collection $items): Collection { return $items->sortByDesc('price')->values(); }
public function getName(): string { return 'price_desc'; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| <?php
namespace App\Strategies\Sorting;
use App\Contracts\Strategies\SortingStrategy; use Illuminate\Support\Collection;
class RelevanceStrategy implements SortingStrategy { protected string $query;
public function __construct(string $query) { $this->query = strtolower($query); }
public function sort(Collection $items): Collection { return $items->sortByDesc(function ($item) { $score = 0; $name = strtolower($item->name); $description = strtolower($item->description ?? '');
if (str_contains($name, $this->query)) { $score += 10; }
if (str_contains($description, $this->query)) { $score += 5; }
if (str_starts_with($name, $this->query)) { $score += 5; }
return $score; })->values(); }
public function getName(): string { return 'relevance'; } }
|
导出策略
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <?php
namespace App\Contracts\Strategies;
interface ExportStrategy { public function export(array $data): string;
public function getMimeType(): string;
public function getFileExtension(): string;
public function getName(): string; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| <?php
namespace App\Strategies\Export;
use App\Contracts\Strategies\ExportStrategy;
class CsvExportStrategy implements ExportStrategy { protected string $delimiter = ','; protected string $enclosure = '"';
public function export(array $data): string { if (empty($data)) { return ''; }
$output = fopen('php://temp', 'r+');
fputcsv($output, array_keys($data[0]), $this->delimiter, $this->enclosure);
foreach ($data as $row) { fputcsv($output, $row, $this->delimiter, $this->enclosure); }
rewind($output); $csv = stream_get_contents($output); fclose($output);
return $csv; }
public function getMimeType(): string { return 'text/csv'; }
public function getFileExtension(): string { return 'csv'; }
public function getName(): string { return 'csv'; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| <?php
namespace App\Strategies\Export;
use App\Contracts\Strategies\ExportStrategy; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
class ExcelExportStrategy implements ExportStrategy { public function export(array $data): string { $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet();
if (!empty($data)) { $sheet->fromArray(array_keys($data[0]), null, 'A1'); $sheet->fromArray($data, null, 'A2'); }
$writer = new Xlsx($spreadsheet);
$tempFile = tempnam(sys_get_temp_dir(), 'excel_'); $writer->save($tempFile);
$content = file_get_contents($tempFile); unlink($tempFile);
return $content; }
public function getMimeType(): string { return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; }
public function getFileExtension(): string { return 'xlsx'; }
public function getName(): string { return 'excel'; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| <?php
namespace App\Strategies\Export;
use App\Contracts\Strategies\ExportStrategy;
class JsonExportStrategy implements ExportStrategy { protected int $options = JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE;
public function export(array $data): string { return json_encode($data, $this->options); }
public function getMimeType(): string { return 'application/json'; }
public function getFileExtension(): string { return 'json'; }
public function getName(): string { return 'json'; } }
|
策略管理器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| <?php
namespace App\Services;
use App\Contracts\Strategies\PricingStrategy; use InvalidArgumentException;
class PricingStrategyManager { protected array $strategies = [];
public function __construct() { $this->registerDefaultStrategies(); }
protected function registerDefaultStrategies(): void { $this->strategies = [ 'standard' => new \App\Strategies\Pricing\StandardPricingStrategy(), 'vip' => new \App\Strategies\Pricing\VipPricingStrategy(), 'wholesale' => new \App\Strategies\Pricing\WholesalePricingStrategy(), ]; }
public function get(string $name): PricingStrategy { if (!isset($this->strategies[$name])) { throw new InvalidArgumentException("Pricing strategy [{$name}] not found."); }
return $this->strategies[$name]; }
public function register(string $name, PricingStrategy $strategy): self { $this->strategies[$name] = $strategy; return $this; }
public function calculate(string $strategy, float $basePrice, array $context = []): float { return $this->get($strategy)->calculate($basePrice, $context); }
public function getAvailableStrategies(): array { return collect($this->strategies)->map(fn($s) => [ 'name' => $s->getName(), 'description' => $s->getDescription(), ])->values()->toArray(); } }
|
策略选择器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| <?php
namespace App\Services;
use App\Contracts\Strategies\PricingStrategy; use App\Models\User;
class PricingStrategySelector { protected PricingStrategyManager $manager;
public function __construct(PricingStrategyManager $manager) { $this->manager = $manager; }
public function selectForUser(User $user): PricingStrategy { if ($user->isVip()) { return $this->manager->get('vip'); }
if ($user->isWholesale()) { return $this->manager->get('wholesale'); }
return $this->manager->get('standard'); }
public function selectForOrder(array $orderData): PricingStrategy { $quantity = $orderData['quantity'] ?? 1;
if ($quantity >= 100) { return $this->manager->get('wholesale'); }
return $this->manager->get('standard'); }
public function selectByContext(array $context): PricingStrategy { $strategyName = $context['pricing_strategy'] ?? 'standard';
if (isset($context['promo_code'])) { $promo = $this->getPromo($context['promo_code']); if ($promo) { return new \App\Strategies\Pricing\PromotionalPricingStrategy( $promo->discount_rate, $promo->expires_at ); } }
return $this->manager->get($strategyName); }
protected function getPromo(string $code): ?object { return \App\Models\PromoCode::where('code', $code)->first(); } }
|
依赖注入集成
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| <?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider; use App\Services\PricingStrategyManager; use App\Services\PaymentContext; use App\Strategies\Payment\CreditCardStrategy; use App\Strategies\Payment\PayPalStrategy;
class StrategyServiceProvider extends ServiceProvider { public function register(): void { $this->app->singleton(PricingStrategyManager::class, function ($app) { return new PricingStrategyManager(); });
$this->app->singleton(PaymentContext::class, function ($app) { $context = new PaymentContext(); $context->registerStrategy(new CreditCardStrategy()); $context->registerStrategy(new PayPalStrategy()); return $context; }); } }
|
测试策略
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| <?php
namespace Tests\Unit\Strategies;
use Tests\TestCase; use App\Strategies\Pricing\StandardPricingStrategy; use App\Strategies\Pricing\VipPricingStrategy; use App\Strategies\Pricing\WholesalePricingStrategy;
class PricingStrategyTest extends TestCase { public function test_standard_pricing_returns_base_price(): void { $strategy = new StandardPricingStrategy();
$price = $strategy->calculate(100.00);
$this->assertEquals(100.00, $price); }
public function test_vip_pricing_applies_discount(): void { $strategy = new VipPricingStrategy(0.15);
$price = $strategy->calculate(100.00);
$this->assertEquals(85.00, $price); }
public function test_wholesale_pricing_applies_tier_discount(): void { $strategy = new WholesalePricingStrategy();
$price = $strategy->calculate(10.00, ['quantity' => 100]);
$this->assertEquals(850.00, $price); }
public function test_strategy_manager_returns_correct_strategy(): void { $manager = app(\App\Services\PricingStrategyManager::class);
$strategy = $manager->get('standard');
$this->assertInstanceOf(StandardPricingStrategy::class, $strategy); } }
|
最佳实践
1. 策略无状态
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <?php
namespace App\Strategies;
use App\Contracts\Strategies\TaxStrategy;
class FixedTaxStrategy implements TaxStrategy { public function __construct( protected float $rate ) {}
public function calculate(float $amount): float { return $amount * $this->rate; } }
|
2. 策略组合
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| <?php
namespace App\Strategies;
use App\Contracts\Strategies\PricingStrategy;
class CompositePricingStrategy implements PricingStrategy { protected array $strategies;
public function __construct(array $strategies) { $this->strategies = $strategies; }
public function calculate(float $basePrice, array $context = []): float { $price = $basePrice;
foreach ($this->strategies as $strategy) { $price = $strategy->calculate($price, $context); }
return $price; }
public function getName(): string { return 'composite'; }
public function getDescription(): string { return 'Composite pricing strategy'; } }
|
3. 默认策略
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <?php
namespace App\Services;
class StrategyManager { protected string $defaultStrategy = 'standard';
public function getDefault(): StrategyInterface { return $this->get($this->defaultStrategy); }
public function setDefault(string $name): self { $this->defaultStrategy = $name; return $this; } }
|
总结
Laravel 13 的策略模式提供了一种灵活的方式来定义和切换算法。通过合理使用策略模式,可以使代码更加灵活、可扩展,并遵循开闭原则。