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);
我的习惯:适配器类名统一用「目标接口名 + Adapter」命名,比如WechatPayAdapter、UnionPayAdapter。这样一看就知道是干嘛的。

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();  // 第二次调用,直接显示缓存
我的经验:在PHP中,代理模式特别适合做ORM的延迟加载。比如Doctrine的Proxy类,就是典型的代理模式。你查询一个关联对象时,它先返回一个代理,等你真正访问属性时才去数据库查询。

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)
注意:组合模式的安全性问题。叶子节点不应该有add/remove方法,但为了统一接口,有时不得不让叶子节点也实现这些方法(抛出异常)。我建议用「透明方式」——统一接口,叶子节点的方法抛出UnsupportedOperationException。

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'
);
我的习惯:外观类不要做太多事情。如果外观类的方法超过5个,或者每个方法内部调用超过10个子系统,说明子系统划分有问题。我一般把外观类当作「入口」,真正的业务逻辑还是放在子系统里。

小结

这五个结构型模式,各有各的用处:

模式 核心作用 我常用的场景
适配器模式 接口转换,让不兼容的类协同工作 统一第三方支付、短信、邮件等外部接口
装饰器模式 动态扩展功能,避免继承爆炸 订单优惠策略、日志记录、权限校验
代理模式 控制访问,延迟加载或权限控制 ORM延迟加载、远程服务代理、缓存代理
组合模式 树形结构,统一处理叶子与容器 菜单树、组织架构、分类树
外观模式 封装复杂子系统,提供简单接口 下单流程、支付流程、注册流程

嗯,结构型模式就聊到这儿。下一章咱们聊行为型模式,那才是真正让代码「活起来」的东西。到时候见。