4、设计模式(结构型):适配器模式、装饰器模式、代理模式、组合模式、外观模式
结构型设计模式,说白了就是解决「类与对象如何组合」的问题。我刚开始写PHP那几年,经常把代码堆成一团,后来才明白——好的结构设计,能让你的系统像乐高积木一样灵活。今天咱们聊聊五个最常用的结构型模式,每个我都踩过坑,也尝过甜头。
4.1 适配器模式(Adapter)
适配器模式,就是让两个不兼容的接口能一起工作。你想想看,现实中的电源适配器,不就是把220V转成手机能用的5V吗?代码里也一样。
核心思想:把一个类的接口,转换成客户端期望的另一个接口。
我遇到过的场景:
有一次,公司要对接三个不同的第三方支付网关。每个网关的接口签名都不一样,有的用XML,有的用JSON,有的甚至用Key-Value。如果直接写if-else,那代码就炸了。我用了适配器模式,统一成我们自己的支付接口,每个网关写一个适配器。后来换支付渠道,改一行配置就行。
- 想复用现有类,但接口不匹配
- 想统一多个相似但接口不同的类
- 想引入第三方库,又不想改业务代码
// 目标接口:统一的支付接口
interface PaymentAdapter {
public function pay(float $amount): bool;
public function refund(string $transactionId): bool;
}
// 第三方支付宝SDK(假设接口不兼容)
class AlipaySDK {
public function alipayTransfer(float $money): string {
// 返回交易号
return 'ALI_' . uniqid();
}
public function alipayRefund(string $tradeNo): bool {
return true;
}
}
// 适配器:把支付宝SDK适配成我们的PaymentAdapter
class AlipayAdapter implements PaymentAdapter {
private AlipaySDK $alipay;
public function __construct(AlipaySDK $alipay) {
$this->alipay = $alipay;
}
public function pay(float $amount): bool {
$result = $this->alipay->alipayTransfer($amount);
return str_starts_with($result, 'ALI_');
}
public function refund(string $transactionId): bool {
return $this->alipay->alipayRefund($transactionId);
}
}
// 客户端代码
$payment = new AlipayAdapter(new AlipaySDK());
$payment->pay(100.00);
4.2 装饰器模式(Decorator)
装饰器模式,就是动态地给对象添加新功能。不用改原有类,也不用继承。说白了,就是一层层「包装」。
核心思想:用组合代替继承,运行时动态扩展功能。
避坑指南:
我曾经在一个项目中,为了给订单加各种优惠策略,写了一个巨大的Order类,里面塞满了if-else。后来维护起来简直噩梦。改用装饰器模式后,每个优惠策略就是一个装饰器,自由组合,清爽多了。
// 组件接口
interface OrderComponent {
public function getTotal(): float;
public function getDescription(): string;
}
// 具体组件:基础订单
class BaseOrder implements OrderComponent {
private float $amount;
public function __construct(float $amount) {
$this->amount = $amount;
}
public function getTotal(): float {
return $this->amount;
}
public function getDescription(): string {
return "基础订单: {$this->amount}元";
}
}
// 装饰器基类
abstract class OrderDecorator implements OrderComponent {
protected OrderComponent $order;
public function __construct(OrderComponent $order) {
$this->order = $order;
}
}
// 具体装饰器:满减优惠
class FullReductionDecorator extends OrderDecorator {
private float $threshold;
private float $reduction;
public function __construct(OrderComponent $order, float $threshold, float $reduction) {
parent::__construct($order);
$this->threshold = $threshold;
$this->reduction = $reduction;
}
public function getTotal(): float {
$total = $this->order->getTotal();
return $total >= $this->threshold ? $total - $this->reduction : $total;
}
public function getDescription(): string {
return $this->order->getDescription() . " + 满{$this->threshold}减{$this->reduction}";
}
}
// 具体装饰器:会员折扣
class MemberDiscountDecorator extends OrderDecorator {
private float $discountRate;
public function __construct(OrderComponent $order, float $discountRate) {
parent::__construct($order);
$this->discountRate = $discountRate;
}
public function getTotal(): float {
return $this->order->getTotal() * $this->discountRate;
}
public function getDescription(): string {
return $this->order->getDescription() . " + 会员折扣({$this->discountRate}折)";
}
}
// 使用示例
$order = new BaseOrder(200.00);
$order = new FullReductionDecorator($order, 150, 30); // 满150减30
$order = new MemberDiscountDecorator($order, 0.9); // 会员9折
echo $order->getDescription(); // 基础订单: 200元 + 满150减30 + 会员折扣(0.9折)
echo $order->getTotal(); // (200 - 30) * 0.9 = 153
4.3 代理模式(Proxy)
代理模式,就是给一个对象提供一个替身,控制对这个对象的访问。你想想看,明星的经纪人就是代理——你想找明星,得先过经纪人这关。
核心思想:控制对目标对象的访问,可以在访问前后做额外操作。
我常用的场景:
- 延迟加载:大对象创建成本高,用代理等到真正需要时才加载
- 访问控制:检查权限,决定是否允许访问
- 日志记录:自动记录方法调用日志
// 主题接口
interface Image {
public function display(): void;
}
// 真实主题:高分辨率图片
class RealImage implements Image {
private string $filename;
public function __construct(string $filename) {
$this->filename = $filename;
$this->loadFromDisk(); // 构造函数里加载,很耗时
}
private function loadFromDisk(): void {
echo "从磁盘加载图片: {$this->filename}\n";
// 模拟耗时操作
sleep(2);
}
public function display(): void {
echo "显示图片: {$this->filename}\n";
}
}
// 代理:延迟加载代理
class ProxyImage implements Image {
private string $filename;
private ?RealImage $realImage = null;
public function __construct(string $filename) {
$this->filename = $filename;
}
public function display(): void {
// 真正需要显示时才加载
if ($this->realImage === null) {
$this->realImage = new RealImage($this->filename);
}
$this->realImage->display();
}
}
// 客户端代码
$image = new ProxyImage("photo.jpg");
// 此时并没有加载图片
$image->display(); // 第一次调用,才真正加载
$image->display(); // 第二次调用,直接显示缓存
4.4 组合模式(Composite)
组合模式,就是把对象组合成树形结构,让客户端可以一致地处理单个对象和组合对象。说白了,就是「部分-整体」的层次结构。
核心思想:让客户端不用区分叶子节点和容器节点,统一调用。
我遇到过的场景:
做权限管理系统时,菜单是树形结构——有父菜单、子菜单、按钮。如果用if-else去遍历,代码会很难看。用组合模式,每个菜单项都实现同一个接口,递归渲染就完事了。
// 组件接口
interface MenuComponent {
public function getName(): string;
public function getUrl(): string;
public function display(int $depth = 0): void;
}
// 叶子节点:菜单项
class MenuItem implements MenuComponent {
private string $name;
private string $url;
public function __construct(string $name, string $url) {
$this->name = $name;
$this->url = $url;
}
public function getName(): string {
return $this->name;
}
public function getUrl(): string {
return $this->url;
}
public function display(int $depth = 0): void {
echo str_repeat("--", $depth) . " {$this->name} ({$this->url})\n";
}
}
// 容器节点:菜单组
class MenuGroup implements MenuComponent {
private string $name;
private array $children = [];
public function __construct(string $name) {
$this->name = $name;
}
public function add(MenuComponent $component): void {
$this->children[] = $component;
}
public function remove(MenuComponent $component): void {
$this->children = array_filter($this->children, fn($c) => $c !== $component);
}
public function getName(): string {
return $this->name;
}
public function getUrl(): string {
return ''; // 容器节点没有URL
}
public function display(int $depth = 0): void {
echo str_repeat("--", $depth) . " [{$this->name}]\n";
foreach ($this->children as $child) {
$child->display($depth + 1);
}
}
}
// 使用示例
$root = new MenuGroup("系统管理");
$userMenu = new MenuGroup("用户管理");
$userMenu->add(new MenuItem("用户列表", "/user/list"));
$userMenu->add(new MenuItem("新增用户", "/user/add"));
$userMenu->add(new MenuItem("角色管理", "/role/list"));
$root->add($userMenu);
$root->add(new MenuItem("系统日志", "/log/list"));
$root->display();
// 输出:
// [系统管理]
// --[用户管理]
// ---- 用户列表 (/user/list)
// ---- 新增用户 (/user/add)
// ---- 角色管理 (/role/list)
// -- 系统日志 (/log/list)
4.5 外观模式(Facade)
外观模式,就是给复杂子系统提供一个统一的、简单的接口。说白了,就是「门面」——你进商场,不需要知道水电怎么走、空调怎么开,你只需要走大门就行。
核心思想:封装复杂性,提供简单接口。
避坑指南:
我曾经接手过一个项目,下单流程涉及库存检查、价格计算、优惠券核销、积分扣除、物流分配、短信通知……十几个类互相调用。新同事看了直接懵。我后来写了一个OrderFacade,把整个下单流程封装成一个方法。调用方只需要一行代码:$orderFacade->placeOrder($request)。
// 子系统:库存服务
class InventoryService {
public function checkStock(string $sku, int $quantity): bool {
echo "检查库存: {$sku} x {$quantity}\n";
return true;
}
public function deductStock(string $sku, int $quantity): void {
echo "扣减库存: {$sku} x {$quantity}\n";
}
}
// 子系统:价格服务
class PricingService {
public function calculatePrice(string $sku, int $quantity): float {
echo "计算价格: {$sku} x {$quantity}\n";
return 100.00 * $quantity;
}
public function applyCoupon(float $total, string $couponCode): float {
echo "应用优惠券: {$couponCode}\n";
return $total * 0.9;
}
}
// 子系统:物流服务
class ShippingService {
public function assignLogistics(string $address): string {
echo "分配物流: {$address}\n";
return 'SF123456789';
}
}
// 子系统:通知服务
class NotificationService {
public function sendOrderConfirmation(string $orderId, string $phone): void {
echo "发送订单确认短信: {$orderId} -> {$phone}\n";
}
}
// 外观类:统一的下单接口
class OrderFacade {
private InventoryService $inventory;
private PricingService $pricing;
private ShippingService $shipping;
private NotificationService $notification;
public function __construct() {
$this->inventory = new InventoryService();
$this->pricing = new PricingService();
$this->shipping = new ShippingService();
$this->notification = new NotificationService();
}
public function placeOrder(array $items, string $address, string $phone, ?string $couponCode = null): string {
// 1. 检查库存
foreach ($items as $item) {
if (!$this->inventory->checkStock($item['sku'], $item['quantity'])) {
throw new \RuntimeException("库存不足: {$item['sku']}");
}
}
// 2. 计算价格
$total = 0;
foreach ($items as $item) {
$total += $this->pricing->calculatePrice($item['sku'], $item['quantity']);
}
if ($couponCode) {
$total = $this->pricing->applyCoupon($total, $couponCode);
}
// 3. 扣减库存
foreach ($items as $item) {
$this->inventory->deductStock($item['sku'], $item['quantity']);
}
// 4. 分配物流
$trackingNumber = $this->shipping->assignLogistics($address);
// 5. 发送通知
$orderId = 'ORD_' . uniqid();
$this->notification->sendOrderConfirmation($orderId, $phone);
return $orderId;
}
}
// 客户端代码:一行搞定
$facade = new OrderFacade();
$orderId = $facade->placeOrder(
[['sku' => 'IPHONE15', 'quantity' => 1]],
'北京市朝阳区...',
'13800138000',
'WELCOME2024'
);
小结
这五个结构型模式,各有各的用处:
| 模式 | 核心作用 | 我常用的场景 |
|---|---|---|
| 适配器模式 | 接口转换,让不兼容的类协同工作 | 统一第三方支付、短信、邮件等外部接口 |
| 装饰器模式 | 动态扩展功能,避免继承爆炸 | 订单优惠策略、日志记录、权限校验 |
| 代理模式 | 控制访问,延迟加载或权限控制 | ORM延迟加载、远程服务代理、缓存代理 |
| 组合模式 | 树形结构,统一处理叶子与容器 | 菜单树、组织架构、分类树 |
| 外观模式 | 封装复杂子系统,提供简单接口 | 下单流程、支付流程、注册流程 |
嗯,结构型模式就聊到这儿。下一章咱们聊行为型模式,那才是真正让代码「活起来」的东西。到时候见。