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 的策略模式提供了一种灵活的方式来定义和切换算法。通过合理使用策略模式,可以使代码更加灵活、可扩展,并遵循开闭原则。