代理模式和装饰器模式的区别

Posted

技术标签:

【中文标题】代理模式和装饰器模式的区别【英文标题】:Differences between Proxy and Decorator Pattern 【发布时间】:2013-09-08 06:03:01 【问题描述】:

你能很好地解释一下代理装饰器有什么区别吗?

我看到的主要区别是,当我们假设 Proxy 使用 compositionDecorator 使用 aggregation 时似乎很清楚,通过使用多个(一个或多个)Decorators,您可以修改/添加功能到预先存在的实例(装饰),而 Proxy 有自己的内部实例代理类和委托给它添加一些额外的功能(代理行为)。

问题是 - 使用聚合创建的 Proxy 仍然是 Proxy 还是 Decorator?是否允许(根据 GoF 模式中的定义)使用聚合创建 代理

【问题讨论】:

部分链接:Proxy 和 Decorator 您从哪里得知 Proxy 使用组合而 Decorator 使用聚合? @CPerkins 看到我对 Rahul Tripathi 回答的评论。 还有装饰器 (patterns.cs.up.ac.za/examples/ch2/decorator-theory.cs) - 显然是聚合,代理 (patterns.cs.up.ac.za/examples/ch2/proxy-theory.cs) - 显然是组合。 【参考方案1】:

Decorator 为对象增加了额外的责任,而代理控制对对象的访问,它们都使用组合。如果您的包装类与主题混淆,那么它显然是一个代理。让我通过一个php中的代码示例来解释:

代码示例

给出如下CarRepository:

interface CarRepositoryInterface 

    public function getById(int $id) : Car


class CarRepository implements CarRepositoryInterface 

    public function getById(int $id) : Car 
    
        sleep(3); //... fake some heavy db call
        $car = new Car;
        $car->setId($id);
        $car->setName("Mercedes Benz");
        return $car;
    

CarRepository-Proxy

Proxy 通常用作延迟加载或缓存代理:

class CarRepositoryCacheProxy implements CarRepositoryInterface 

    private $carRepository;

    private function getSubject() : CarRepositoryInterface
    
        if($this->carRepository == null) 
            $this->carRepository = new CarRepository();
        
        return $this->carRepository;
    
    
    /**
     * This method controls the access to the subject
     * based on if there is cache available 
     */
    public function getById(int $id) : Car
    
        if($this->hasCache(__METHOD__)) 
            return unserialize($this->getCache(__METHOD__));
           
        $response = $this->getSubject()->getById($id);
        $this->writeCache(__METHOD__, serialize($response));
        return $response;
    
    
    private function hasCache(string $key) : bool 
    
        //... implementation 
    
    
    private function getCache(string $key) : string 
    
        //... implementation 
    
    
    private function writeCache(string $key, string $result) : string 
    
        //... implementation 
    

CarRepository-Decorator

只要添加的行为不“控制”主题,就可以使用Decorator

class CarRepositoryEventManagerDecorator implements CarRepositoryInterface 

    private $subject, $eventManager;

    /**
     * Subjects in decorators are often passed in the constructor, 
     * where a proxy often takes control over the invocation behavior 
     * somewhere else 
     */
    public function __construct(CarRepositoryInterface $subject, EventManager $eventManager)
    
        $this->subject = $subject;
        $this->eventManager = $eventManager;
    

    public function getById(int $id) : Car 
    
        $this->eventManager->trigger("pre.getById");
        //this method takes no control over the subject
        $result = $this->subject->getById($id);
        $this->eventManager->trigger("post.getById");
        return $result;
    

【讨论】:

【参考方案2】:

让我先解释一下模式,然后再问你问题。

从类图和含义来看,非常相似:

    两者都具有与其委托人相同的界面。 两者都添加/增强其受委托者的行为。 两者都要求受托人执行操作(不应与空受托人一起使用)。

但它们有一些区别:

    不同的意图: 代理使用与其受委托者完全不同的领域知识来增强受委托者(传递的对象)的行为。例如,安全代理添加受委托者的安全控制。发送远程消息的代理需要序列化/反序列化数据并具有网络接口知识,但与如何准备源数据无关。 装饰器帮助受委托人处理的同一问题域。例如,BufferedInputStreaman(一个 IO 装饰器)作用于输入,与它的委托人是同一个问题域(IO),但如果没有提供 IO 数据的委托人,它就无法执行。

    依赖性强与否: 装饰器依赖委托来完成行为,没有委托人(强)就无法完成行为。因此,我们总是使用聚合而不是合成。 即使不需要委托人(弱),代理也可以执行伪造的行为。例如,mockito(单元测试框架)可以仅通过其接口来模拟/监视行为。因此,我们使用合成来表示对真实对象没有强依赖性。

    多次增强(如问题所述): 代理:我们可以利用代理来包装真实对象一次而不是多次。 装饰器:装饰器可以多次包裹真实对象,也可以包裹已经被装饰器包裹的对象(可以是不同的装饰器,也可以是相同的装饰器)。例如,对于订单系统,您可以使用装饰器进行折扣。 PercentageDiscountDecorator 就是砍掉 50% 的折扣,而 DeductionAmountDiscountDecorator 就是如果金额大于 10$() 就直接扣 5$。所以, 1)。当您想减 50% 并扣除 5$ 时,您可以这样做: new DeductionAmountDiscountDecorator(new PercentageDiscountDecorator(delegee)) 2)。当你想扣除 10 美元时,你可以做 new DeductionAmountDiscountDecorator(new DeductionAmountDiscountDecorator(delegee))。

问题的答案与Proxy和Decorator的区别无关。为什么?

    设计模式只是为不擅长 OO 技能的人使用 OO 解决方案提供的模式。如果您熟悉 OO,则无需知道其中有多少设计模式(在设计模式发明之前,具有相同问题的技术人员可以找出相同的解决方案)。 没有两片叶子是完全相同的,你遇到的问题也是如此。人们总是会发现他们的问题与设计模式给出的问题不同。

如果您指定的问题与 Proxy 和 Decorator 处理的问题确实不同,并且确实需要聚合,为什么不使用呢?我认为将 OO 应用于您的问题比您将其标记为代理或装饰器更重要。

【讨论】:

【参考方案3】:

真正的区别不是所有权(组合与聚合),而是类型信息。

装饰器总是传递给它的委托人。 代理 可能自己创建它,或者他可能注入它。

但是代理 总是知道被委托人的(更多)具体类型。换句话说,Proxy 和它的委托人将具有相同的基类型,但 Proxy 指向某个派生类型。 Decorator 指向它自己的基本类型。因此,区别在于受委托者类型的编译时信息。

在动态语言中,如果被委托者被注入,并且恰好有相同的接口,那么没有区别。

您的问题的答案是“是”。

【讨论】:

“但代理总是知道受委托者的(更多)特定类型。”我认为这不是真的。想象一下远程代理。代理机制不需要知道远程对象的任何细节。远程系统用指定的接口注册对象。并且本地代理暴露了相同的接口。 我在亚马逊上了一堂关于这方面的课,来自一位了解他的东西的客座讲师。使用“代理”可执行文件(例如,使用 Web 服务)和代理设计模式是有区别的。代理模式和装饰器模式的 UML 可以不同。但是没有什么能阻止 Proxy 拥有与其委托人相同的 API。 Decorator 是 Proxy 的严格子集,但 Decorator 仍可能被称为 Proxy,具体取决于底层 API 是否保证相同。【参考方案4】:

这是来自 GoF 的直接引述(第 216 页)。

虽然装饰器可以有与代理类似的实现,但装饰器有不同的用途。装饰器为对象添加一个或多个职责,而代理控制对对象的访问。

代理的实现程度不同,就像装饰器一样。一种 保护代理可以像装饰器一样实现。在另一 手,远程代理将不包含对其真实主题的直接引用,而仅包含 间接引用,例如“主机 ID 和主机上的本地地址”。虚拟代理 将以间接引用(例如文件名)开始,但最终会 获取并使用直接引用。

流行的答案表明代理知道其委托的具体类型。从这句话我们可以看出,这并不总是正确的。

根据GoF,Proxy和Decorator的区别在于Proxy限制客户端。装饰器没有。代理可以通过控制对功能的访问来限制客户端做什么;或者它可能通过执行客户端不可见和未知的操作来限制客户端知道的内容。 Decorator 则相反:它以一种对客户可见的方式增强了其委托的功能。

我们可以说 Proxy 是一个黑盒子,而 Decorator 是一个白盒子。

在对比Proxy和Decorator时,wrapper和delegate之间的组合关系是错误的关系,因为组合是这两种模式的共同特征。包装器和客户端之间的关系是这两种模式的区别。

装饰者通知并授权其客户。 代理限制和剥夺其客户端的权力。

【讨论】:

如果(例如保护访问)代理完全像装饰器一样实现,那么区分它们的原因仅仅是为了传达意图:限制访问或增强行为? 根据 GoF,是的。他们在区分模式时经常强调意图。我要补充一点,客户端不太可能知道代理而不是装饰器,这意味着 实例化它是一个重要的区别。 如果它是混合物怎么办?如果包装器同时增加 限制其客户端怎么办?这就是专注于意图的问题。这仍然是一个很好的答案,这似乎是 GoF 的想法。但是很容易误判意图,或者只是主观地不同意。在大多数情况下,查看包装器的实现就足够了。 @cdunn2001,我不一定不同意principle;但是为了争论:如果包装器是混合的,它可能违反了单一责任原则。如果它的意图很容易被误诊,那么它就没有充分传达意图;并且可以提出一个强有力的论点,即“好”代码和“坏”代码之间的主要区别在于,好的代码传达了意图,而糟糕的代码则没有。在任何一种情况下,包装器都比识别设计模式存在更大的问题。 我刚刚注意到一个“已删除”的答案,这是从powerdream5.wordpress.com/2007/11/17/…抄袭的,仍然是一个很好的链接。该UML 显示了区别。我的意思是,如果受委托者的类型比包装器的基本类型更具体,那么包装器 not 就是装饰器。意图仍然是无价的知识。我不认为我们不同意。【参考方案5】:

Proxy为被包装的对象提供相同的接口,Decorator为其提供增强的接口,Proxy通常自己管理其服务对象的生命周期,而装饰器的组成始终由客户端控制。

【讨论】:

【参考方案6】:

花了一段时间才弄清楚this 的答案及其真正含义。举几个例子应该更清楚。

Proxy第一:

public interface Authorization 
    String getToken();
 

还有:

// goes to the DB and gets a token for example
public class DBAuthorization implements Authorization 
    @Override
    public String getToken() 
        return "DB-Token";
    

还有这个Authorization的来电者,一个相当愚蠢的人:

class Caller 
    void authenticatedUserAction(Authorization authorization) 
        System.out.println("doing some action with : " + authorization.getToken());
    

到目前为止没有什么不寻常的,对吧?从某个服务获取令牌,使用该令牌。现在对图片又多了一个要求,添加日志记录:意思是每次都记录令牌。这种情况很简单,只需创建一个Proxy

public class LoggingDBAuthorization implements Authorization 

    private final DBAuthorization dbAuthorization = new DBAuthorization();

    @Override
    public String getToken() 
        String token = dbAuthorization.getToken();
        System.out.println("Got token : " + token);
        return token;
    

我们将如何使用它?

public static void main(String[] args) 
    LoggingDBAuthorization loggingDBAuthorization = new LoggingDBAuthorization();

    Caller caller = new Caller();
    caller.authenticatedUserAction(loggingDBAuthorization);


注意LoggingDBAuthorization 拥有 DBAuthorization 的一个实例。 LoggingDBAuthorizationDBAuthorization 实现 Authorization

代理将保存基本接口 (Authorization) 的一些具体实现 (DBAuthorization)。换句话说,代理确切地知道正在代理什么。

Decorator:

它的开头与Proxy 几乎相同,带有一个界面:

public interface JobSeeker 
    int interviewScore();

及其实现:

class Newbie implements JobSeeker  
    @Override
    public int interviewScore() 
        return 10;
    

现在我们想添加一个更有经验的候选人,将其面试分数加上另一个JobSeeker的分数:

@RequiredArgsConstructor 
public class TwoYearsInTheIndustry implements JobSeeker 

    private final JobSeeker jobSeeker;

    @Override
    public int interviewScore() 
        return jobSeeker.interviewScore() + 20;
     

注意我怎么说加上另一个求职者的那个不是NewbieDecorator 不知道确切它在装饰什么,它只知道那个装饰实例的契约(它知道JobSeeker)。请注意,这与Proxy 不同;相比之下,它确切地知道它在装饰什么。

您可能会质疑在这种情况下这两种设计模式之间是否真的有任何区别?如果我们尝试将Decorator 写成Proxy 会怎样?

public class TwoYearsInTheIndustry implements JobSeeker 

    private final Newbie newbie = new Newbie();

    @Override
    public int interviewScore() 
        return newbie.interviewScore() + 20;
    

这绝对是一个选项,并强调了这些模式有多接近;如其他答案中所述,它们仍然适用于不同的场景。

【讨论】:

【参考方案7】:

Proxy 和 Decorator 的用途不同,它们侧重于内部实现。代理用于使用远程、跨进程或跨网络对象,就好像它是本地对象一样。装饰器用于在原始界面中添加新的行为。

虽然这两种模式在结构上相似,但代理的大部分复杂性在于确保与源对象的正确通信。另一方面,装饰器专注于添加行为的实现。

【讨论】:

你在说什么与这里已经存在的其他 4 个答案不同? 不知道有没有。看完之前的回答,我就是有种想插话的冲动。【参考方案8】:

主要区别:

    代理提供相同的接口。 装饰器提供了增强的界面。 DecoratorProxy 用途不同,但结构相似。两者都描述了如何为另一个对象提供一定程度的间接性,并且实现保留了对它们转发请求的对象的引用。 Decorator 可以被视为只有一个组件的退化组合。但是,装饰器增加了额外的职责 - 它不适合对象聚合。 装饰器支持递归组合 Decorator 类声明了与 LCD(最低类分母)接口的 composition 关系,并且该数据成员在其构造函数中初始化。 使用 代理 进行延迟初始化,通过缓存对象和控制对客户端/调用者的访问来提高性能

Sourcemaking 的文章以极好的方式引用了相同点和不同点。

相关的 SE 问题/链接:

When to Use the Decorator Pattern?

What is the exact difference between Adapter and Proxy patterns?

【讨论】:

【参考方案9】:

Decorator 模式侧重于向对象动态添加功能,而 Proxy 模式侧重于控制对对象的访问。

编辑:-

Proxy 和真实主体之间的关系通常在编译时设置,Proxy 以某种方式实例化它,而 Decorator 被分配在运行时到主体,只知道主体的接口。

【讨论】:

代理仍然可以用来添加功能。想想 AOP 代理。 完全同意先生。换句话说,我将其转换为代理模式,代理类可以向其客户端隐藏对象的详细信息。因此,在使用代理模式时,我们通常会在代理类内部创建一个 abject 的实例。而在使用装饰器模式时,我们通常会将原始对象作为参数传递给装饰器的构造函数。 在这种情况下,当实例在代理中“隐藏”时,差异对我来说很明显(正如我所写的)但是我认为人们经常调用代理类来获取传递的代理对象作为构造函数参数。在这种情况下,添加新功能或控制的区别对我来说(非常)微不足道。 Proxy 和真实主体之间的关系通常在编译时设置,Proxy 以某种方式实例化它,而装饰器或适配器在运行时分配给主体,只知道主体的接口。希望有道理!!! :) 您可以将此行添加到您的答案中。【参考方案10】:

Decorator 获取装饰对象的引用(通常通过构造函数),而 Proxy 负责自己完成。

代理可能根本不实例化包装对象(如果不使用对象字段/获取器,ORM 会这样做以防止对 DB 的不必要访问),而 Decorator 始终保持链接到实际包装的实例。

代理通常由框架用于添加安全性或缓存/延迟,并由框架构建(而不是由常规开发人员本身)。

装饰器通常用于由开发者自己基于接口而不是实际类为旧类或遗留类添加新行为(因此它适用于广泛的接口实例,代理 围绕具体类)。

【讨论】:

以上是关于代理模式和装饰器模式的区别的主要内容,如果未能解决你的问题,请参考以下文章

设计模式之-装饰器模式

代理模式

代理和装饰模式之间的差异

Java开发设计模式 09:装饰器模式

设计模式之装饰器模式

代理模式