编程

装饰器模式 vs. 代理模式

236 2024-02-23 07:35:00

在 PHP 中,有两个模式非常相似:装饰器模式和代理模式。因此,你很容易就会把其中一个误认为另一个。这有关系吗?也许没有,但我认为在交流时了解差异是件好事。

装饰器和代理的相似之处

装饰器(Decorator)模式和代理(Proxy)模式都围绕着用一个类包装现有接口的实例(让我们称之为内部实例)的想法,该类实现了相同的接口,并将其函数调用委托给其内部实例上的相同函数。

这些模式对于在不破坏封装的情况下添加或更改实例的功能非常有用。它还可以更改或扩展最终函数和类的功能。因为它们通常有一个目的,所以可以很容易地进行测试。

示例

interface SubscriberInterface {
    public function subscribe(string $email): void;
}
 
class SubscriberDecorator implements SubscriberInterface {
    private SubscriberInterface $inner_subscriber;
 
    public function subscribe(string $email): void {
        $this->inner_subscriber->subscribe($email);
    }
}

本例中,可以看到 SubscriberDecorator 实现了 SubscriberInterface,它还需要一些 SubscriberInterface 的实例。之后,它将subscribe() 函数委托给该实例上的同一个函数。

装饰器和代理的不同之处

当涉及到将类命名为 Decorator 或 Proxy 时,你必须查看其意图。这个类实际上对它所包装的实例做了什么? 

必需 vs. 可选依赖

你可能已经注意到,我在前面的示例中没有包含 __construct() 方法。这是故意的,因为这是第一个明显的区别。

Decorator 需要它所包装的接口的实例,而 Proxy 不需要这样的实例。代理可以接收实例,但也可以自己创建实例。所以你可以自己创建一个新(new)的代理,而装饰器需要另一个实例作为依赖。

//// Decorator
 public function __construct(public SubscriberInterface $inner_subscriber){}
 
// Proxy
 public function __construct(?SubscriberInterface $inner_subscriber = null){
    $this->inner_subscriber = $inner_subscriber ?? new InnerSubscriber();
}

添加剂 vs. 限制剂

装饰物是添加剂;这意味着它们只通过包装函数调用并返回原始值来添加新功能。然而,它可以在调用之前或之后做任何事情。例如,你可以在调用函数或调度事件时记录每个值。只需确保返回原始值。

代理是有限制的;也就是说它们可以通过抛出异常来改变函数的行为,甚至限制调用特定函数。

小贴士:Decorator 和 Proxy 都可以添加任何不在接口上的额外函数或参数。因此,明智的做法是在 Decorator 或 Proxy 上实现一些魔术的 __isset()__get() 和  __call() 方法,将这些调用也传递给它们的内部实例。这样,即使在上面添加了多个装饰器,你仍然可以调用这些方法和参数。

public function __call($name, $arguments)
{
    return $this->inner_subscriber->{$name}(...$arguments);
}
 
public function __get($name)
{
    return $this->inner_subscriber->{$name};
}
 
public function __isset($name)
{
    return isset($this->inner_subscriber->{$name});
}

通用目的 vs. 特定目的

装饰器有一个通用的用途。无论包装的实例是什么,它都会添加一些功能。这意味着多个装饰器应该能够以任何随机顺序叠加应用,并且仍然产生相同的结果和添加的功能。

代理有一个更为特定的目的。它主要用于更改或将功能附加到接口的特定实例。代理通常也不会堆叠在一起,因为单个代理通常就足够了。

装饰器和代理的小贴士

以下是使用装饰器和代理时可能会考虑的一些小贴士。

创建一个基础的抽象

如果您为同一个接口创建多个装饰器或代理,那么创建接口的抽象类(abstract class)或满足接口的 trait 可能是有益的,其中每个函数都已委托给内部实例上的函数。如果你是包的创建者,你甚至可以考虑在包中提供此实现。通过这种方式,装饰器或代理可以扩展或使用此实现,并且只(重新)声明它需要的函数。

interface SubscriberInterface
{
    public function subscribe(string $email): bool;
 
    public function unsubscribe(string $email): bool;
}
 
trait SubscriberTrait { ... }
 
// You can now extend this class, or implement the interface and trait.
abstract class SubscriberDecorator implements SubscriberInterface
{
    use SubscriberTrait;
 
    public function __construct(SubscriberInterface $inner_subscriber)
    {
        $this->inner_subscriber = $inner_subscriber;
    }
}

单一职责装饰器

在装饰器上添加多个功能可能很诱人,但它们的美妙之处在于,可以在不更改底层代码的情况下添加或删除它们。所以,试着制作专注于一件事的小型装饰器,并将它们叠加在一起使用。同样,这种简单性也使它们更容易测试。

示例

你可以在 Symfony 中找到几个装饰器和代理的好例子。

例如,他们的开发者工具栏显示了许多关于事件和缓存的信息。他们通过在开发环境中用 TraceableEventDispatcher 装饰当前 EventDispatch,用 TraceableAdapter 装饰当前缓存适配器来记录这些信息。

可以在 symfony/cache 包的 DeflateMarshaller 中找到代理的示例。这个 Marshaller 是有限制的,因为它依赖于 gzonflate()gzdeflate() 并且修改了内部示例的输出。