Laravel 13 装饰器模式深度解析

装饰器模式是一种结构型设计模式,它允许在不修改对象自身代码的情况下动态地给对象添加新的行为。本文将深入探讨 Laravel 13 中装饰器模式的高级用法。

装饰器模式基础

什么是装饰器模式

装饰器模式通过创建包装对象来包裹原始对象,在保持原始对象接口不变的情况下添加额外功能。

1
2
3
4
5
6
7
8
<?php

namespace App\Contracts;

interface ComponentInterface
{
public function operation(): mixed;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
<?php

namespace App\Components;

use App\Contracts\ComponentInterface;

class ConcreteComponent implements ComponentInterface
{
public function operation(): mixed
{
return 'Base operation';
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php

namespace App\Decorators;

use App\Contracts\ComponentInterface;

abstract class Decorator implements ComponentInterface
{
protected ComponentInterface $component;

public function __construct(ComponentInterface $component)
{
$this->component = $component;
}

abstract public function operation(): mixed;
}

日志装饰器

服务接口

1
2
3
4
5
6
7
8
9
10
11
12
<?php

namespace App\Contracts\Services;

interface PaymentServiceInterface
{
public function process(float $amount, array $data = []): PaymentResult;

public function refund(string $transactionId): bool;

public function getStatus(string $transactionId): 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
<?php

namespace App\Services\Payments;

use App\Contracts\Services\PaymentServiceInterface;

class StripePaymentService implements PaymentServiceInterface
{
public function process(float $amount, array $data = []): PaymentResult
{
$charge = \Stripe\Charge::create([
'amount' => $amount * 100,
'currency' => $data['currency'] ?? 'usd',
'source' => $data['token'],
]);

return new PaymentResult(
success: $charge->status === 'succeeded',
transactionId: $charge->id,
amount: $amount
);
}

public function refund(string $transactionId): bool
{
$refund = \Stripe\Refund::create(['charge' => $transactionId]);
return $refund->status === 'succeeded';
}

public function getStatus(string $transactionId): string
{
$charge = \Stripe\Charge::retrieve($transactionId);
return $charge->status;
}
}

日志装饰器

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
<?php

namespace App\Decorators\Payments;

use App\Contracts\Services\PaymentServiceInterface;
use Illuminate\Support\Facades\Log;

class LoggingDecorator implements PaymentServiceInterface
{
protected PaymentServiceInterface $service;

public function __construct(PaymentServiceInterface $service)
{
$this->service = $service;
}

public function process(float $amount, array $data = []): PaymentResult
{
Log::info('Payment processing started', [
'amount' => $amount,
'currency' => $data['currency'] ?? 'usd',
]);

try {
$result = $this->service->process($amount, $data);

Log::info('Payment processing completed', [
'transaction_id' => $result->transactionId,
'success' => $result->success,
]);

return $result;
} catch (\Exception $e) {
Log::error('Payment processing failed', [
'amount' => $amount,
'error' => $e->getMessage(),
]);

throw $e;
}
}

public function refund(string $transactionId): bool
{
Log::info('Refund initiated', ['transaction_id' => $transactionId]);

$result = $this->service->refund($transactionId);

Log::info('Refund completed', [
'transaction_id' => $transactionId,
'success' => $result,
]);

return $result;
}

public function getStatus(string $transactionId): string
{
return $this->service->getStatus($transactionId);
}
}

缓存装饰器

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
<?php

namespace App\Decorators\Payments;

use App\Contracts\Services\PaymentServiceInterface;
use Illuminate\Support\Facades\Cache;

class CachingDecorator implements PaymentServiceInterface
{
protected PaymentServiceInterface $service;
protected int $cacheTtl = 300;

public function __construct(PaymentServiceInterface $service)
{
$this->service = $service;
}

public function process(float $amount, array $data = []): PaymentResult
{
return $this->service->process($amount, $data);
}

public function refund(string $transactionId): bool
{
Cache::forget("payment.status.{$transactionId}");
return $this->service->refund($transactionId);
}

public function getStatus(string $transactionId): string
{
return Cache::remember(
"payment.status.{$transactionId}",
$this->cacheTtl,
fn() => $this->service->getStatus($transactionId)
);
}
}

重试装饰器

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
<?php

namespace App\Decorators\Payments;

use App\Contracts\Services\PaymentServiceInterface;
use Illuminate\Support\Facades\Log;

class RetryDecorator implements PaymentServiceInterface
{
protected PaymentServiceInterface $service;
protected int $maxAttempts;
protected int $delayMs;

public function __construct(
PaymentServiceInterface $service,
int $maxAttempts = 3,
int $delayMs = 1000
) {
$this->service = $service;
$this->maxAttempts = $maxAttempts;
$this->delayMs = $delayMs;
}

public function process(float $amount, array $data = []): PaymentResult
{
$attempts = 0;
$lastException = null;

while ($attempts < $this->maxAttempts) {
try {
return $this->service->process($amount, $data);
} catch (\Exception $e) {
$lastException = $e;
$attempts++;

if ($attempts < $this->maxAttempts) {
Log::warning('Payment attempt failed, retrying', [
'attempt' => $attempts,
'error' => $e->getMessage(),
]);

usleep($this->delayMs * 1000);
}
}
}

throw $lastException;
}

public function refund(string $transactionId): bool
{
return $this->service->refund($transactionId);
}

public function getStatus(string $transactionId): string
{
return $this->service->getStatus($transactionId);
}
}

验证装饰器

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
<?php

namespace App\Decorators\Payments;

use App\Contracts\Services\PaymentServiceInterface;
use Illuminate\Support\Facades\Validator;
use InvalidArgumentException;

class ValidationDecorator implements PaymentServiceInterface
{
protected PaymentServiceInterface $service;

protected array $processRules = [
'amount' => 'required|numeric|min:0.01|max:999999.99',
'currency' => 'sometimes|string|size:3',
'token' => 'required|string',
];

public function __construct(PaymentServiceInterface $service)
{
$this->service = $service;
}

public function process(float $amount, array $data = []): PaymentResult
{
$data['amount'] = $amount;

$validator = Validator::make($data, $this->processRules);

if ($validator->fails()) {
throw new InvalidArgumentException(
'Invalid payment data: ' . json_encode($validator->errors()->all())
);
}

return $this->service->process($amount, $data);
}

public function refund(string $transactionId): bool
{
if (empty($transactionId) || !preg_match('/^ch_[a-zA-Z0-9]+$/', $transactionId)) {
throw new InvalidArgumentException('Invalid transaction ID format');
}

return $this->service->refund($transactionId);
}

public function getStatus(string $transactionId): string
{
return $this->service->getStatus($transactionId);
}
}

限流装饰器

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\Decorators\Payments;

use App\Contracts\Services\PaymentServiceInterface;
use Illuminate\Support\Facades\Cache;
use RuntimeException;

class RateLimitDecorator implements PaymentServiceInterface
{
protected PaymentServiceInterface $service;
protected int $maxRequests;
protected int $windowSeconds;

public function __construct(
PaymentServiceInterface $service,
int $maxRequests = 100,
int $windowSeconds = 60
) {
$this->service = $service;
$this->maxRequests = $maxRequests;
$this->windowSeconds = $windowSeconds;
}

public function process(float $amount, array $data = []): PaymentResult
{
$this->checkRateLimit($data['user_id'] ?? 'default');

return $this->service->process($amount, $data);
}

public function refund(string $transactionId): bool
{
return $this->service->refund($transactionId);
}

public function getStatus(string $transactionId): string
{
return $this->service->getStatus($transactionId);
}

protected function checkRateLimit(string $identifier): void
{
$key = "payment.rate_limit.{$identifier}";
$current = Cache::get($key, 0);

if ($current >= $this->maxRequests) {
throw new RuntimeException('Rate limit exceeded. Please try again later.');
}

Cache::put($key, $current + 1, $this->windowSeconds);
}
}

审计装饰器

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
<?php

namespace App\Decorators\Payments;

use App\Contracts\Services\PaymentServiceInterface;
use App\Models\AuditLog;
use Illuminate\Support\Facades\Auth;

class AuditDecorator implements PaymentServiceInterface
{
protected PaymentServiceInterface $service;

public function __construct(PaymentServiceInterface $service)
{
$this->service = $service;
}

public function process(float $amount, array $data = []): PaymentResult
{
$result = $this->service->process($amount, $data);

$this->logAudit('payment.process', [
'amount' => $amount,
'transaction_id' => $result->transactionId,
'success' => $result->success,
'user_id' => Auth::id(),
]);

return $result;
}

public function refund(string $transactionId): bool
{
$result = $this->service->refund($transactionId);

$this->logAudit('payment.refund', [
'transaction_id' => $transactionId,
'success' => $result,
'user_id' => Auth::id(),
]);

return $result;
}

public function getStatus(string $transactionId): string
{
return $this->service->getStatus($transactionId);
}

protected function logAudit(string $action, array $data): void
{
AuditLog::create([
'action' => $action,
'data' => $data,
'user_id' => Auth::id(),
'ip_address' => request()->ip(),
'user_agent' => request()->userAgent(),
]);
}
}

装饰器组合

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\Services\Payments;

use App\Contracts\Services\PaymentServiceInterface;
use App\Decorators\Payments\{
LoggingDecorator,
CachingDecorator,
RetryDecorator,
ValidationDecorator,
RateLimitDecorator,
AuditDecorator
};

class PaymentServiceFactory
{
public static function create(): PaymentServiceInterface
{
$service = new StripePaymentService();

$service = new ValidationDecorator($service);
$service = new RateLimitDecorator($service, 100, 60);
$service = new RetryDecorator($service, 3, 1000);
$service = new CachingDecorator($service);
$service = new AuditDecorator($service);
$service = new LoggingDecorator($service);

return $service;
}
}

消息队列装饰器

1
2
3
4
5
6
7
8
9
10
11
12
<?php

namespace App\Contracts\Services;

interface MessageQueueInterface
{
public function publish(string $queue, mixed $message): bool;

public function consume(string $queue, callable $handler): void;

public function acknowledge(mixed $message): bool;
}
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
<?php

namespace App\Decorators\Queue;

use App\Contracts\Services\MessageQueueInterface;
use Illuminate\Support\Facades\Log;

class QueueLoggingDecorator implements MessageQueueInterface
{
protected MessageQueueInterface $queue;

public function __construct(MessageQueueInterface $queue)
{
$this->queue = $queue;
}

public function publish(string $queue, mixed $message): bool
{
Log::debug('Message published', [
'queue' => $queue,
'message_id' => $message['id'] ?? null,
]);

return $this->queue->publish($queue, $message);
}

public function consume(string $queue, callable $handler): void
{
$this->queue->consume($queue, function ($message) use ($handler, $queue) {
Log::debug('Message received', [
'queue' => $queue,
'message_id' => $message['id'] ?? null,
]);

return $handler($message);
});
}

public function acknowledge(mixed $message): bool
{
$result = $this->queue->acknowledge($message);

Log::debug('Message acknowledged', [
'message_id' => $message['id'] ?? null,
'success' => $result,
]);

return $result;
}
}
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
<?php

namespace App\Decorators\Queue;

use App\Contracts\Services\MessageQueueInterface;
use Illuminate\Support\Facades\Metrics;

class QueueMetricsDecorator implements MessageQueueInterface
{
protected MessageQueueInterface $queue;

public function __construct(MessageQueueInterface $queue)
{
$this->queue = $queue;
}

public function publish(string $queue, mixed $message): bool
{
$start = microtime(true);

try {
$result = $this->queue->publish($queue, $message);

Metrics::increment('queue.publish.success', ['queue' => $queue]);
Metrics::timing('queue.publish.duration', (microtime(true) - $start) * 1000, ['queue' => $queue]);

return $result;
} catch (\Exception $e) {
Metrics::increment('queue.publish.error', ['queue' => $queue]);
throw $e;
}
}

public function consume(string $queue, callable $handler): void
{
$this->queue->consume($queue, function ($message) use ($handler, $queue) {
$start = microtime(true);

try {
$result = $handler($message);

Metrics::increment('queue.consume.success', ['queue' => $queue]);
Metrics::timing('queue.consume.duration', (microtime(true) - $start) * 1000, ['queue' => $queue]);

return $result;
} catch (\Exception $e) {
Metrics::increment('queue.consume.error', ['queue' => $queue]);
throw $e;
}
});
}

public function acknowledge(mixed $message): bool
{
return $this->queue->acknowledge($message);
}
}

HTTP 客户端装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php

namespace App\Contracts\Http;

interface HttpClientInterface
{
public function get(string $url, array $options = []): HttpResponse;

public function post(string $url, array $data = [], array $options = []): HttpResponse;

public function put(string $url, array $data = [], array $options = []): HttpResponse;

public function delete(string $url, array $options = []): HttpResponse;
}
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
<?php

namespace App\Decorators\Http;

use App\Contracts\Http\HttpClientInterface;
use Illuminate\Support\Facades\Cache;

class CacheDecorator implements HttpClientInterface
{
protected HttpClientInterface $client;
protected int $defaultTtl;
protected array $cacheableMethods = ['GET'];

public function __construct(HttpClientInterface $client, int $defaultTtl = 3600)
{
$this->client = $client;
$this->defaultTtl = $defaultTtl;
}

public function get(string $url, array $options = []): HttpResponse
{
$ttl = $options['cache_ttl'] ?? $this->defaultTtl;

if ($ttl > 0) {
$key = $this->getCacheKey('GET', $url, $options);

return Cache::remember($key, $ttl, function () use ($url, $options) {
return $this->client->get($url, $options);
});
}

return $this->client->get($url, $options);
}

public function post(string $url, array $data = [], array $options = []): HttpResponse
{
return $this->client->post($url, $data, $options);
}

public function put(string $url, array $data = [], array $options = []): HttpResponse
{
return $this->client->put($url, $data, $options);
}

public function delete(string $url, array $options = []): HttpResponse
{
return $this->client->delete($url, $options);
}

protected function getCacheKey(string $method, string $url, array $options): string
{
return 'http:' . md5($method . $url . json_encode($options));
}
}

依赖注入配置

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
<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\Contracts\Services\PaymentServiceInterface;
use App\Services\Payments\StripePaymentService;
use App\Decorators\Payments\{
LoggingDecorator,
CachingDecorator,
RetryDecorator,
ValidationDecorator,
AuditDecorator
};

class DecoratorServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->bind(PaymentServiceInterface::class, function ($app) {
$service = new StripePaymentService();

$service = new ValidationDecorator($service);
$service = new RetryDecorator($service, 3, 1000);
$service = new CachingDecorator($service);
$service = new AuditDecorator($service);
$service = new LoggingDecorator($service);

return $service;
});
}
}

测试装饰器

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
<?php

namespace Tests\Unit\Decorators;

use Tests\TestCase;
use App\Contracts\Services\PaymentServiceInterface;
use App\Decorators\Payments\LoggingDecorator;
use App\Decorators\Payments\CachingDecorator;
use Mockery;

class PaymentDecoratorTest extends TestCase
{
public function test_logging_decorator_logs_operations(): void
{
$mockService = Mockery::mock(PaymentServiceInterface::class);
$mockService->shouldReceive('process')
->once()
->andReturn(new PaymentResult(true, 'txn_123', 100.00));

Log::shouldReceive('info')->twice();

$decorator = new LoggingDecorator($mockService);
$result = $decorator->process(100.00, ['token' => 'tok_test']);

$this->assertTrue($result->success);
}

public function test_caching_decorator_caches_status(): void
{
$mockService = Mockery::mock(PaymentServiceInterface::class);
$mockService->shouldReceive('getStatus')
->once()
->with('txn_123')
->andReturn('succeeded');

$decorator = new CachingDecorator($mockService);

$status1 = $decorator->getStatus('txn_123');
$status2 = $decorator->getStatus('txn_123');

$this->assertEquals('succeeded', $status1);
$this->assertEquals('succeeded', $status2);
}

public function test_retry_decorator_retries_on_failure(): void
{
$mockService = Mockery::mock(PaymentServiceInterface::class);
$mockService->shouldReceive('process')
->twice()
->andThrow(new \Exception('Network error'))
->once()
->andReturn(new PaymentResult(true, 'txn_123', 100.00));

$decorator = new RetryDecorator($mockService, 3, 0);
$result = $decorator->process(100.00, ['token' => 'tok_test']);

$this->assertTrue($result->success);
}
}

最佳实践

1. 保持装饰器单一职责

1
2
3
4
5
6
7
8
9
10
11
12
<?php

namespace App\Decorators;

class LoggingOnlyDecorator implements ServiceInterface
{
public function operation(): mixed
{
Log::info('Operation called');
return $this->service->operation();
}
}

2. 装饰器顺序很重要

1
2
3
4
5
6
7
8
9
<?php

$service = new ValidationDecorator(
new RateLimitDecorator(
new RetryDecorator(
new BasePaymentService()
)
)
);

3. 接口一致性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php

namespace App\Decorators;

abstract class AbstractDecorator implements ServiceInterface
{
protected ServiceInterface $service;

public function __construct(ServiceInterface $service)
{
$this->service = $service;
}

public function __call(string $method, array $arguments): mixed
{
return $this->service->{$method}(...$arguments);
}
}

总结

Laravel 13 的装饰器模式提供了一种灵活的方式来扩展对象功能。通过合理使用装饰器模式,可以在不修改原有代码的情况下添加新功能,提高代码的可维护性和可扩展性。