Laravel 13 桥接模式深度解析

桥接模式是一种结构型设计模式,它将抽象部分与实现部分分离,使它们可以独立变化。本文将深入探讨 Laravel 13 中桥接模式的高级用法。

桥接模式基础

什么是桥接模式

桥接模式通过将继承关系转换为组合关系,实现了抽象和实现的解耦。

1
2
3
4
5
6
7
8
<?php

namespace App\Contracts;

interface ImplementationInterface
{
public function operationImplementation(): string;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php

namespace App\Abstractions;

abstract class Abstraction
{
protected ImplementationInterface $implementation;

public function __construct(ImplementationInterface $implementation)
{
$this->implementation = $implementation;
}

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

namespace App\RefinedAbstractions;

use App\Abstractions\Abstraction;

class RefinedAbstraction extends Abstraction
{
public function operation(): string
{
return "Refined: " . $this->implementation->operationImplementation();
}
}

消息通知桥接

消息发送器接口

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

namespace App\Contracts\Messaging;

interface MessageSenderInterface
{
public function send(string $to, string $subject, string $body): bool;

public function sendBatch(array $recipients, string $subject, string $body): array;

public function getProviderName(): 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
<?php

namespace App\Messaging\Senders;

use App\Contracts\Messaging\MessageSenderInterface;
use Illuminate\Support\Facades\Mail;

class EmailSender implements MessageSenderInterface
{
public function send(string $to, string $subject, string $body): bool
{
try {
Mail::raw($body, function ($message) use ($to, $subject) {
$message->to($to)->subject($subject);
});

return true;
} catch (\Exception $e) {
return false;
}
}

public function sendBatch(array $recipients, string $subject, string $body): array
{
$results = [];

foreach ($recipients as $recipient) {
$results[$recipient] = $this->send($recipient, $subject, $body);
}

return $results;
}

public function getProviderName(): string
{
return 'email';
}
}
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
<?php

namespace App\Messaging\Senders;

use App\Contracts\Messaging\MessageSenderInterface;
use Twilio\Rest\Client;

class SmsSender implements MessageSenderInterface
{
protected Client $client;
protected string $fromNumber;

public function __construct(array $config)
{
$this->client = new Client($config['sid'], $config['token']);
$this->fromNumber = $config['from'];
}

public function send(string $to, string $subject, string $body): bool
{
try {
$this->client->messages->create($to, [
'from' => $this->fromNumber,
'body' => $body,
]);

return true;
} catch (\Exception $e) {
return false;
}
}

public function sendBatch(array $recipients, string $subject, string $body): array
{
$results = [];

foreach ($recipients as $recipient) {
$results[$recipient] = $this->send($recipient, $subject, $body);
}

return $results;
}

public function getProviderName(): string
{
return 'sms';
}
}
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\Messaging\Senders;

use App\Contracts\Messaging\MessageSenderInterface;

class PushSender implements MessageSenderInterface
{
public function send(string $to, string $subject, string $body): bool
{
try {
$notification = [
'to' => $to,
'notification' => [
'title' => $subject,
'body' => $body,
],
];

$response = Http::withToken(config('services.fcm.token'))
->post('https://fcm.googleapis.com/fcm/send', $notification);

return $response->successful();
} catch (\Exception $e) {
return false;
}
}

public function sendBatch(array $recipients, string $subject, string $body): array
{
$results = [];

foreach ($recipients as $recipient) {
$results[$recipient] = $this->send($recipient, $subject, $body);
}

return $results;
}

public function getProviderName(): string
{
return 'push';
}
}

消息抽象层

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

namespace App\Messaging\Messages;

use App\Contracts\Messaging\MessageSenderInterface;

abstract class Message
{
protected MessageSenderInterface $sender;
protected string $subject;
protected string $body;

public function __construct(MessageSenderInterface $sender)
{
$this->sender = $sender;
}

public function setSubject(string $subject): self
{
$this->subject = $subject;
return $this;
}

public function setBody(string $body): self
{
$this->body = $body;
return $this;
}

abstract public function send(string $to): bool;

abstract public function formatBody(): 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
<?php

namespace App\Messaging\Messages;

class WelcomeMessage extends Message
{
protected string $userName;

public function setUserName(string $name): self
{
$this->userName = $name;
return $this;
}

public function send(string $to): bool
{
$formattedBody = $this->formatBody();

return $this->sender->send($to, $this->subject, $formattedBody);
}

public function formatBody(): string
{
return str_replace('{name}', $this->userName, $this->body);
}
}
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\Messaging\Messages;

class OrderConfirmationMessage extends Message
{
protected string $orderNumber;
protected float $total;

public function setOrderNumber(string $orderNumber): self
{
$this->orderNumber = $orderNumber;
return $this;
}

public function setTotal(float $total): self
{
$this->total = $total;
return $this;
}

public function send(string $to): bool
{
$formattedBody = $this->formatBody();

return $this->sender->send($to, $this->subject, $formattedBody);
}

public function formatBody(): string
{
return str_replace(
['{order_number}', '{total}'],
[$this->orderNumber, number_format($this->total, 2)],
$this->body
);
}
}

数据库驱动桥接

数据库驱动接口

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

namespace App\Contracts\Database;

interface DriverInterface
{
public function connect(array $config): void;

public function disconnect(): void;

public function query(string $sql, array $bindings = []): array;

public function execute(string $sql, array $bindings = []): int;

public function beginTransaction(): void;

public function commit(): void;

public function rollback(): void;

public function getLastInsertId(): 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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<?php

namespace App\Database\Drivers;

use App\Contracts\Database\DriverInterface;
use PDO;

class MySqlDriver implements DriverInterface
{
protected ?PDO $pdo = null;

public function connect(array $config): void
{
$dsn = sprintf(
'mysql:host=%s;port=%s;dbname=%s;charset=%s',
$config['host'],
$config['port'] ?? 3306,
$config['database'],
$config['charset'] ?? 'utf8mb4'
);

$this->pdo = new PDO($dsn, $config['username'], $config['password'], [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
}

public function disconnect(): void
{
$this->pdo = null;
}

public function query(string $sql, array $bindings = []): array
{
$stmt = $this->pdo->prepare($sql);
$stmt->execute($bindings);
return $stmt->fetchAll();
}

public function execute(string $sql, array $bindings = []): int
{
$stmt = $this->pdo->prepare($sql);
$stmt->execute($bindings);
return $stmt->rowCount();
}

public function beginTransaction(): void
{
$this->pdo->beginTransaction();
}

public function commit(): void
{
$this->pdo->commit();
}

public function rollback(): void
{
$this->pdo->rollBack();
}

public function getLastInsertId(): string
{
return $this->pdo->lastInsertId();
}
}
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
64
65
<?php

namespace App\Database\Drivers;

use App\Contracts\Database\DriverInterface;
use PDO;

class PostgresDriver implements DriverInterface
{
protected ?PDO $pdo = null;

public function connect(array $config): void
{
$dsn = sprintf(
'pgsql:host=%s;port=%s;dbname=%s',
$config['host'],
$config['port'] ?? 5432,
$config['database']
);

$this->pdo = new PDO($dsn, $config['username'], $config['password'], [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
}

public function disconnect(): void
{
$this->pdo = null;
}

public function query(string $sql, array $bindings = []): array
{
$stmt = $this->pdo->prepare($sql);
$stmt->execute($bindings);
return $stmt->fetchAll();
}

public function execute(string $sql, array $bindings = []): int
{
$stmt = $this->pdo->prepare($sql);
$stmt->execute($bindings);
return $stmt->rowCount();
}

public function beginTransaction(): void
{
$this->pdo->beginTransaction();
}

public function commit(): void
{
$this->pdo->commit();
}

public function rollback(): void
{
$this->pdo->rollBack();
}

public function getLastInsertId(): string
{
return $this->pdo->lastInsertId();
}
}

查询构建器抽象

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\Database\Query;

use App\Contracts\Database\DriverInterface;

abstract class QueryBuilder
{
protected DriverInterface $driver;
protected string $table;
protected array $wheres = [];
protected array $columns = ['*'];
protected array $bindings = [];

public function __construct(DriverInterface $driver)
{
$this->driver = $driver;
}

public function table(string $table): self
{
$this->table = $table;
return $this;
}

public function select(array $columns = ['*']): self
{
$this->columns = $columns;
return $this;
}

public function where(string $column, string $operator, mixed $value): self
{
$this->wheres[] = compact('column', 'operator', 'value');
$this->bindings[] = $value;
return $this;
}

abstract public function get(): array;

abstract public function first(): ?array;

abstract protected function compileSelect(): 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
<?php

namespace App\Database\Query;

class MySqlQueryBuilder extends QueryBuilder
{
public function get(): array
{
$sql = $this->compileSelect();
return $this->driver->query($sql, $this->bindings);
}

public function first(): ?array
{
$results = $this->get();
return $results[0] ?? null;
}

protected function compileSelect(): string
{
$columns = implode(', ', $this->columns);
$sql = "SELECT {$columns} FROM {$this->table}";

if (!empty($this->wheres)) {
$clauses = array_map(
fn($where) => "{$where['column']} {$where['operator']} ?",
$this->wheres
);
$sql .= ' WHERE ' . implode(' AND ', $clauses);
}

return $sql;
}
}

渲染器桥接

渲染器接口

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

namespace App\Contracts\Rendering;

interface RendererInterface
{
public function renderTitle(string $title): string;

public function renderText(string $text): string;

public function renderLink(string $text, string $url): string;

public function renderImage(string $src, string $alt): string;

public function renderList(array $items): string;

public function renderTable(array $headers, array $rows): 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\Rendering\Renderers;

use App\Contracts\Rendering\RendererInterface;

class HtmlRenderer implements RendererInterface
{
public function renderTitle(string $title): string
{
return "<h1>{$title}</h1>";
}

public function renderText(string $text): string
{
return "<p>{$text}</p>";
}

public function renderLink(string $text, string $url): string
{
return "<a href=\"{$url}\">{$text}</a>";
}

public function renderImage(string $src, string $alt): string
{
return "<img src=\"{$src}\" alt=\"{$alt}\">";
}

public function renderList(array $items): string
{
$listItems = array_map(fn($item) => "<li>{$item}</li>", $items);
return "<ul>" . implode('', $listItems) . "</ul>";
}

public function renderTable(array $headers, array $rows): string
{
$headerCells = array_map(fn($h) => "<th>{$h}</th>", $headers);
$headerRow = "<tr>" . implode('', $headerCells) . "</tr>";

$bodyRows = array_map(function ($row) {
$cells = array_map(fn($cell) => "<td>{$cell}</td>", $row);
return "<tr>" . implode('', $cells) . "</tr>";
}, $rows);

return "<table><thead>{$headerRow}</thead><tbody>" . implode('', $bodyRows) . "</tbody></table>";
}
}
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\Rendering\Renderers;

use App\Contracts\Rendering\RendererInterface;

class MarkdownRenderer implements RendererInterface
{
public function renderTitle(string $title): string
{
return "# {$title}\n\n";
}

public function renderText(string $text): string
{
return "{$text}\n\n";
}

public function renderLink(string $text, string $url): string
{
return "[{$text}]({$url})";
}

public function renderImage(string $src, string $alt): string
{
return "![{$alt}]({$src})";
}

public function renderList(array $items): string
{
$listItems = array_map(fn($item) => "- {$item}", $items);
return implode("\n", $listItems) . "\n\n";
}

public function renderTable(array $headers, array $rows): string
{
$headerLine = "| " . implode(" | ", $headers) . " |";
$separatorLine = "| " . implode(" | ", array_fill(0, count($headers), "---")) . " |";

$bodyLines = array_map(function ($row) {
return "| " . implode(" | ", $row) . " |";
}, $rows);

return $headerLine . "\n" . $separatorLine . "\n" . implode("\n", $bodyLines) . "\n\n";
}
}
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\Rendering\Renderers;

use App\Contracts\Rendering\RendererInterface;

class JsonRenderer implements RendererInterface
{
protected array $data = [];

public function renderTitle(string $title): string
{
$this->data['title'] = $title;
return json_encode(['type' => 'title', 'content' => $title]);
}

public function renderText(string $text): string
{
return json_encode(['type' => 'text', 'content' => $text]);
}

public function renderLink(string $text, string $url): string
{
return json_encode(['type' => 'link', 'text' => $text, 'url' => $url]);
}

public function renderImage(string $src, string $alt): string
{
return json_encode(['type' => 'image', 'src' => $src, 'alt' => $alt]);
}

public function renderList(array $items): string
{
return json_encode(['type' => 'list', 'items' => $items]);
}

public function renderTable(array $headers, array $rows): string
{
return json_encode(['type' => 'table', 'headers' => $headers, 'rows' => $rows]);
}
}

页面抽象

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

namespace App\Rendering\Pages;

use App\Contracts\Rendering\RendererInterface;

abstract class Page
{
protected RendererInterface $renderer;

public function __construct(RendererInterface $renderer)
{
$this->renderer = $renderer;
}

abstract public function render(): 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
<?php

namespace App\Rendering\Pages;

class ArticlePage extends Page
{
protected string $title;
protected string $content;
protected array $links = [];

public function setTitle(string $title): self
{
$this->title = $title;
return $this;
}

public function setContent(string $content): self
{
$this->content = $content;
return $this;
}

public function addLink(string $text, string $url): self
{
$this->links[] = ['text' => $text, 'url' => $url];
return $this;
}

public function render(): string
{
$output = $this->renderer->renderTitle($this->title);
$output .= $this->renderer->renderText($this->content);

foreach ($this->links as $link) {
$output .= $this->renderer->renderLink($link['text'], $link['url']);
}

return $output;
}
}

测试桥接模式

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 Tests\Unit\Bridge;

use Tests\TestCase;
use App\Messaging\Messages\WelcomeMessage;
use App\Messaging\Senders\EmailSender;
use App\Messaging\Senders\SmsSender;
use App\Rendering\Pages\ArticlePage;
use App\Rendering\Renderers\HtmlRenderer;
use App\Rendering\Renderers\MarkdownRenderer;

class BridgeTest extends TestCase
{
public function test_message_with_different_senders(): void
{
$emailSender = new EmailSender();
$smsSender = $this->createMock(\App\Contracts\Messaging\MessageSenderInterface::class);

$emailMessage = new WelcomeMessage($emailSender);
$smsMessage = new WelcomeMessage($smsSender);

$this->assertInstanceOf(WelcomeMessage::class, $emailMessage);
$this->assertInstanceOf(WelcomeMessage::class, $smsMessage);
}

public function test_page_with_different_renderers(): void
{
$htmlRenderer = new HtmlRenderer();
$markdownRenderer = new MarkdownRenderer();

$htmlPage = new ArticlePage($htmlRenderer);
$markdownPage = new ArticlePage($markdownRenderer);

$htmlPage->setTitle('Test Title')->setContent('Test Content');
$markdownPage->setTitle('Test Title')->setContent('Test Content');

$htmlOutput = $htmlPage->render();
$markdownOutput = $markdownPage->render();

$this->assertStringContainsString('<h1>Test Title</h1>', $htmlOutput);
$this->assertStringContainsString('# Test Title', $markdownOutput);
}
}

最佳实践

1. 优先组合而非继承

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

class GoodBridge
{
protected ImplementationInterface $implementation;

public function __construct(ImplementationInterface $implementation)
{
$this->implementation = $implementation;
}
}

2. 接口设计要稳定

1
2
3
4
5
6
<?php

interface StableInterface
{
public function coreOperation(): mixed;
}

3. 抽象层应委托给实现

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

abstract class Abstraction
{
protected ImplementationInterface $implementation;

public function operation(): string
{
return $this->implementation->operationImplementation();
}
}

总结

Laravel 13 的桥接模式提供了一种灵活的方式来分离抽象和实现。通过合理使用桥接模式,可以创建可扩展、可维护的系统架构,同时支持多个维度的独立变化。