代理模式、装饰器模式、适配器模式和桥接模式有何不同?

469

我在研究代理模式,感觉它与装饰器、适配器和桥接模式非常相似。我是否有什么误解?它们之间有何区别?为什么要使用代理模式而不是其他模式?你过去如何在实际项目中使用它们?


4
经常会出现看起来非常相似但意图不同的模式(例如策略模式和状态模式)。我认为这通常是因为设计模式基于共同的稳定设计原则。 - Jason Down
6
这四种模式的具体实现细节是完全相同的。状态模式和策略模式至少可以总结为有状态和无状态(大部分情况下)。通常,策略模式只是方法注入,而状态模式使用接口不仅可以抽象出方法调用。而策略模式最终也只是在面向对象世界中实现函数式编程的一种方式。 - Charles Graham
13个回答

738

代理、装饰器、适配器和桥接是“包装”类的不同变体。但它们的用途不同。

  • 代理可用于惰性实例化对象,隐藏调用远程服务的事实或控制对对象的访问权限。

  • 装饰器也称为“智能代理”。当您想要向对象添加功能,但不是通过扩展该对象的类型来实现时,可以使用此选项。这允许您在运行时完成此操作。

  • 适配器用于具有抽象接口并希望将该接口映射到具有类似功能角色但不同接口的另一个对象的情况。

  • 桥接与适配器非常相似,但当您定义抽象接口和底层实现时,我们称其为桥接。也就是说,您不是在适应某些遗留或第三方代码,而是所有代码的设计师,但需要能够交换不同的实现。

  • 外观是一个较高级别(更简单)的接口,用于一个或多个类的子系统。假设您拥有一个需要多个对象来表示的复杂概念。对该对象集进行更改非常令人困惑,因为您不总是知道哪个对象具有您需要调用的方法。那就是编写外观以提供针对所有可以对对象集执行的复杂操作的高级方法的时间。例如:学校部分的域模型,具有诸如countStudents()reportAttendance()assignSubstituteTeacher()等方法。


13
好的回答。也许值得加上一些你在实际应用中看到过的示例,例如Web服务中的代理类。我给你点赞。 - Rob Cooper
5
@Rob: 谢谢,但我更愿意让这个答案简洁明了。我鼓励你写另一个带有实际例子的答案! - Bill Karwin
8
装饰者模式也很适合避免类型层次结构失控。例如,假设你在GUI中有一个窗口,并且想要可选的滚动条。你可以创建 Window、VScrollWindow、HScrollWindow 和 VHScrollWindow 类,或者在 Window 上创建 VScroll 和 HScroll 装饰器。 - Eva
1
@RobertDailey,装饰器 就是 组合。 - Bill Karwin
1
如果您想要复制包装对象的接口,但又想添加一些额外的方法,那么这是一个装饰器还是适配器呢? - donquixote
显示剩余18条评论

235

正如Bill的回答所说,它们的用例不同

因此它们的结构也不同。

  • 代理模式(Proxy)装饰器模式(Decorator)的接口与它们封装的类型相同,但代理模式在底层创建一个实例,而装饰器模式在构造函数中接受一个实例。

  • 适配器模式(Adapter)外观模式(Facade)都具有不同于它们所包装的接口的接口。但是适配器模式派生自现有接口,而门面模式创建了一个新接口。

  • 桥接模式(Bridge)适配器模式(Adapter)都指向现有类型。但是桥接模式将指向抽象类型,而适配器模式可能会指向具体类型。桥接模式将允许您在运行时配对实现,而适配器模式通常不会。


42
你的回答和Bill的结合得非常好,很好地总结了《设计模式》这5章内容。可以称它们为该书的高级界面(即更简单易懂的界面)。 - Jonas Eicher
我认为Adapter通常依赖于一个接口(需要适应另一个接口 - 就像您所说的派生),但它仍然可以创建(引入)一个新的接口(从依赖接口适应而来)。 AdapterFacade之间的区别在于依赖关系的数量,Facade通常使用大量杂项接口(不仅仅是一个像Adapter),并将它们组合起来输出一个用于某些目的的通用接口。 - Hopeless

67

我对这个主题的看法。

所有四种模式都有很多共同点,有时都被非正式地称为包装器或包装器模式。它们都使用组合,将主题包装并在某个时候委托执行主题,将一个方法调用映射到另一个方法。它们使客户端不必构造不同的对象并复制所有相关数据。如果明智地使用,它们可以节省内存和处理器。

通过促进松散耦合,它们使曾经稳定的代码不那么容易受到不可避免的更改的影响,并且更易于其他开发人员阅读。

适配器

适配器将主题(被适配者)适应到不同的接口上。这样,我们可以将对象添加到名义上不同类型的集合中。

适配器仅向客户端公开相关方法,可以限制所有其他方法,显示特定上下文的使用意图,例如适应外部库,使其看起来不那么通用,而更专注于我们的应用程序需求。适配器增加了我们代码的可读性和自我描述能力。

适配器可以保护一个团队免受其他团队的易变代码的影响;处理离岸团队时,这是一个救命工具;-)

很少提到的目的是防止主题类过多地使用注释。随着越来越多基于注释的框架的出现,这变得比以往任何时候都更加重要。

适配器有助于解决Java只支持单继承的限制。它可以将多个被适配者组合在一个包装器下,给人一种多重继承的印象。
在代码方面,适配器是“轻量级”的。除了简单地调用被适配者方法和必要的数据转换之外,它不应该为被适配者类添加太多代码。
JDK或基本库中没有太多好的适配器示例。应用程序开发人员创建适配器来将库适应于应用程序特定的接口。
装饰器不仅仅是委托,也不仅仅是将一个方法映射到另一个方法,它们还能修改某些主题方法的行为,可以决定根本不调用主题方法,而是委托给不同的对象,即辅助对象。
装饰器通常会(透明地)向包装对象添加功能,如日志记录、加密、格式化或压缩到主题中。这些新功能可能带来很多新代码。因此,装饰器通常比适配器“更胖”。
装饰器必须是主题接口的子类。它们可以透明地代替其主题使用。请参见BufferedOutputStream,它仍然是OutputStream,并且可以像OutputStream一样使用。这是与适配器的一个主要技术区别。

整个装饰器家族的教科书示例都可以在JDK - Java IO中轻松找到。所有类,如BufferedOutputStreamFilterOutputStreamObjectOutputStream都是OutputStream的装饰器。它们可以像洋葱一样层层包裹,其中一个装饰器再次被装饰,添加更多功能。

代理

代理不是典型的包装器。代理主体-被包装对象,在代理创建时可能尚不存在。代理通常在内部创建它。它可能是按需创建的重量级对象,或者是不同JVM或不同网络节点中的远程对象,甚至是非Java对象,即本地代码中的组件。它不必必须完全包装或委托给另一个对象。

最典型的例子是远程代理、重量级对象初始化程序和访问代理。

  • 远程代理 - 主题位于远程服务器、不同的JVM甚至非Java系统上。代理将方法调用转换为RMI/REST/SOAP调用或其他所需技术,使客户端免受底层技术的影响。

  • 延迟加载代理 - 仅在第一次使用或首次强烈使用时完全初始化对象。

  • 访问代理 - 控制对主题的访问。

外观模式

外观模式与最小知识原则(德米特法则)密切相关。外观模式非常类似于适配器。它们都包装,它们都将一个对象映射到另一个对象,但它们的意图不同。外观模式展开了主题的复杂结构,复杂的对象图,简化了对复杂结构的访问。

外观模式包装了一个复杂的结构,为其提供了一个平面接口。这样可以防止客户端对象暴露在主题结构的内部关系中,从而促进了松散耦合。

桥接模式

适配器模式的更复杂的变体,其中不仅实现有所不同,而且抽象也有所不同。它增加了一层委托的间接性。额外的委托是桥梁。它甚至将适配器与适配接口分离开来。它比其他包装模式更复杂,因此需要谨慎应用。

构造函数的差异

当查看它们的构造函数时,模式之间的差异也很明显。

  • 代理模式没有包装现有对象。构造函数中没有主题。

  • 装饰器模式适配器模式确实包装了已经存在的对象,并且通常在构造函数中提供。

  • 外观模式的构造函数接受整个对象图的根元素,否则它看起来与适配器模式相同。

现实生活中的例子- JAXB Marshalling Adapter。该适配器的目的是将简单的平面类映射到外部所需的更复杂的结构,并防止过多注释“污染”主题类。


31

很多 GoF 设计模式之间存在很大的重叠性。它们都建立在多态性的优势上,有时只是意图上稍有不同(例如策略和状态)。

阅读《Head First设计模式》后,我的模式理解能力提高了100倍。

我强烈推荐!


17

专家已经清楚地解释了每个模式代表的含义,下面是精华摘要:

装饰者模式:

  1. 在运行时动态地为对象添加行为。继承是实现该功能的关键,这既是优点也是缺点。
  2. 它修改了接口的行为

例如(使用链接):java.io包中与InputStreamOutputStream接口相关的类

FileOutputStream fos1 = new FileOutputStream("data1.txt");  
ObjectOutputStream out1 = new ObjectOutputStream(fos1);

代理模式:

  1. 用于延迟初始化,通过缓存对象和控制对客户端/调用者的访问来提高性能。它可以提供替代行为或调用真实对象。在此过程中,它可能创建新对象。
  2. 与允许对象链接的装饰器不同,代理不允许链接

例如:java.rmi包中的类。

适配器模式:

  1. 它允许通过不同的对象使两个不相关的接口协同工作,可能扮演相同的角色。
  2. 它修改原始接口

例如:java.io.InputStreamReaderInputStream返回一个Reader)。

桥接模式:

  1. 它允许抽象和实现都可以独立变化
  2. 它使用组合而非继承

例如:java.util中的集合类。ListArrayList实现。

关键点:

  1. 适配器为主题提供不同的接口。 代理提供相同的接口。 装饰器提供增强的接口。
  2. 适配器更改对象的接口,装饰器增强对象的职责。
  3. 装饰器代理具有不同的目的,但具有类似的结构。
  4. 适配器在设计后使事物正常工作;桥接在它们工作之前就让它们正常工作。
  5. 桥接的设计是为了让抽象和实现可以独立变化。 适配器是为了让不相关的类一起工作而进行的后期改装。
  6. 装饰器旨在使您能够向对象添加职责,而无需子类化。

请查看优秀的SE问题/文章,了解各种设计模式的示例

何时使用装饰器模式?

何时使用桥接模式?它与适配器模式有何不同?

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


1
抱歉,我不明白你的意思。关于装饰器,你说,“继承是实现此功能的关键,这既是该模式的优点也是缺点”。同时,“装饰器旨在让您在不创建子类的情况下向对象添加职责”。在我看来,这两者相互矛盾。 - Andrey M. Stepanov

8
他们非常相似,它们之间的界限非常模糊。我建议您阅读c2 wiki中的代理模式装饰器模式条目。
那里的条目和讨论非常广泛,它们还链接到其他相关文章。顺便说一句,当想了解不同模式之间的细微差别时,c2 wiki是一个很好的选择。
总的来说,我会说装饰器添加/改变行为,但代理更多涉及访问控制(延迟实例化、远程访问、安全等)。但就像我说的那样,它们之间的界限是模糊的,我看到有关代理的引用可以很容易地被视为装饰器,反之亦然。

6
这是引用自《Head First设计模式》的内容。
定义来自书籍,示例来自我。 装饰器 - 不改变接口,但增加职责。假设你有一个汽车接口,当你为不同型号的汽车(s、sv、sl)实现该接口时,你可能需要为某些型号添加更多职责,例如拥有天窗、气囊等。 适配器 - 将一个接口转换为另一个接口。你有一个汽车接口,想让它像吉普车一样运行。因此,你将汽车进行修改并转换成吉普车,虽然它不是真正的吉普车,但表现得像吉普车。 外观 - 使接口更简单。假设你有汽车、飞机、船的接口,实际上你只需要一个类来将人们从一个地方送到另一个地方。你希望外观决定使用哪种交通工具。然后你将所有这些接口引用收集在一个大伞下,并让它决定/委托以保持简单。
Head First: “外观不仅简化了接口,还将客户端与组件子系统解耦。外观和适配器可以包装多个类,但外观的目的是简化,而适配器的目的是将接口转换为不同的东西。”

5
所有这四种模式都是将内部对象/类与外部对象/类组合起来,因此它们在结构上非常相似。我根据目的概述它们的差异:
- 代理(Proxy):封装了对内部对象的访问; - 装饰器(Decorator):使用外部对象修改或扩展内部对象的行为; - 适配器(Adapter):将内部对象的接口转换成外部对象的接口; - 桥接(Bridge):将行为的不变部分(外部对象)与可变或平台相关部分(内部对象)分离。
而内部和外部对象之间的接口变化如下:
- 在代理中,接口相同。 - 在装饰器中,接口相同。 - 在适配器中,接口形式上不同,但实现的是相同的目的。 - 在桥接中,接口从概念上来说是不同的。

1
我希望为Bill Karwing的回答添加一些示例(顺便说一句,他的回答非常好)。我还补充了一些实现上的关键区别,我觉得这些区别被遗漏了。
引用部分来自[https://dev59.com/l3RC5IYBdhLWcg3wSu57#350471](Bill Karwing)的回答:
代理、装饰器、适配器和桥接模式都是“包装”一个类的变体。但它们的用途不同。
- 代理模式可以在惰性实例化对象时使用,或者隐藏您调用远程服务的事实,或者控制对对象的访问。 - 被代理的ProxyClass和ObjectClass应该实现相同的接口,以便它们可以互换使用。 示例 - 代理昂贵的对象
class ProxyHumanGenome implements GenomeInterface  {
    private $humanGenome = NULL; 

    // humanGenome class is not instantiated at construct time
    function __construct() {
    }

    function getGenomeCount() {
        if (NULL == $this->humanGenome) {
            $this->instantiateGenomeClass(); 
        }
        return $this->humanGenome->getGenomeCount();
    }
} 
class HumanGenome implement GenomeInterface { ... }
  • 装饰器也称为“智能代理”。当您想要向对象添加功能,但不想通过扩展该对象的类型来实现时,可以使用它。这使您可以在运行时执行此操作。

DecoratorClass应该(可以)实现ObjectClass的扩展接口。因此,ObjectClass可以被DecoratorClass替换,但反之则不行。

示例-添加附加功能

class DecoratorHumanGenome implements CheckGenomeInterface  {

    // ... same code as previous example

    // added functionality
    public function isComplete() {
        $this->humanGenome->getCount >= 21000
    }
}

interface CheckGenomeInterface extends GenomeInterface {

    public function isComplete();

}

class HumanGenome implement GenomeInterface { ... }
  • 适配器用于当您有一个抽象接口,并且您想将该接口映射到具有类似功能角色但不同接口的另一个对象时。

实现差异:代理、装饰器、适配器

适配器为其主题提供了不同的接口。代理提供相同的接口。装饰器提供增强的接口。

  • Bridge非常类似于Adapter,但是当您定义抽象接口和底层实现时,我们称其为Bridge。 也就是说,您不是在适应某些遗留或第三方代码,而是您需要能够替换不同的实现, 您设计了所有的代码。

  • Facade是一个更高级别(即更简单)的接口,用于表示一个或多个类的子系统。 假设您有一个需要多个对象来表示的复杂概念。 对该对象集进行更改会很困惑,因为您并不总是知道哪个对象具有您需要调用的方法。 这就是编写Facade的时间,为所有可对对象集执行的复杂操作提供高级别方法。例如:学校部门的领域模型, 其中包括countStudents(),reportAttendance(),assignSubstituteTeacher()等方法。

这个答案中的大部分信息来自于https://sourcemaking.com/design_patterns,我强烈推荐它作为设计模式的一个优秀资源。


1

我相信代码会给你一个清晰的想法(也可以补充其他答案)。请看下面,重点关注类实现和包装的类型。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace TestConsole
{
    class Program
    {
        static void Main(string[] args)
        {
            /* Proxy */

            Console.WriteLine(Environment.NewLine);
            Console.WriteLine("PROXY");
            Console.WriteLine(Environment.NewLine);

            //instead of creating here create using a factory method, the facory method will return the proxy
            IReal realProxy = new RealProxy();
            Console.WriteLine("calling do work with the proxy object ");
            realProxy.DoWork();

            Console.WriteLine(Environment.NewLine);
            Console.WriteLine("ADAPTER");
            Console.WriteLine(Environment.NewLine);

            /*Adapter*/
            IInHand objectIHave = new InHand();
            Api myApi = new Api();
            //myApi.SomeApi(objectIHave); /*I cant do this, use a adapter then */
            IActual myAdaptedObject = new ActualAdapterForInHand(objectIHave);
            Console.WriteLine("calling api with  my adapted obj");
            myApi.SomeApi(myAdaptedObject);


            Console.WriteLine(Environment.NewLine);
            Console.WriteLine("DECORATOR");
            Console.WriteLine(Environment.NewLine);

            /*Decorator*/
            IReady maleReady = new Male();
            Console.WriteLine("now male is going to get ready himself");
            maleReady.GetReady();

            Console.WriteLine(Environment.NewLine);

            IReady femaleReady = new Female();
            Console.WriteLine("now female is going to get ready her self");
            femaleReady.GetReady();

            Console.WriteLine(Environment.NewLine);

            IReady maleReadyByBeautician = new Beautician(maleReady);
            Console.WriteLine("now male is going to get ready by beautician");
            maleReadyByBeautician.GetReady();

            Console.WriteLine(Environment.NewLine);

            IReady femaleReadyByBeautician = new Beautician(femaleReady);
            Console.WriteLine("now female is going to get ready by beautician");
            femaleReadyByBeautician.GetReady();

            Console.WriteLine(Environment.NewLine);

            Console.ReadLine();


        }
    }

    /*Proxy*/

    public interface IReal
    {
        void DoWork();
    }

    public class Real : IReal
    {
        public void DoWork()
        {
            Console.WriteLine("real is doing work ");
        }
    }


    public class RealProxy : IReal
    {
        IReal real = new Real();

        public void DoWork()
        {
            real.DoWork();
        }
    }

    /*Adapter*/

    public interface IActual
    {
        void DoWork();
    }

    public class Api
    {
        public void SomeApi(IActual actual)
        {
            actual.DoWork();
        }
    }

    public interface IInHand
    {
        void DoWorkDifferently();
    }

    public class InHand : IInHand
    {
        public void DoWorkDifferently()
        {
            Console.WriteLine("doing work slightly different ");
        }
    }

    public class ActualAdapterForInHand : IActual
    {
        IInHand hand = null;

        public ActualAdapterForInHand()
        {
            hand = new InHand();
        }

        public ActualAdapterForInHand(IInHand hnd)
        {
            hand = hnd;
        }

        public void DoWork()
        {
            hand.DoWorkDifferently();
        }
    }

    /*Decorator*/

    public interface IReady
    {
        void GetReady();
    }

    public class Male : IReady
    {
        public void GetReady()
        {
            Console.WriteLine("Taking bath.. ");
            Console.WriteLine("Dress up....");
        }
    }

    public class Female : IReady
    {
        public void GetReady()
        {
            Console.WriteLine("Taking bath.. ");
            Console.WriteLine("Dress up....");
            Console.WriteLine("Make up....");
        }
    }

    //this is a decorator
    public class Beautician : IReady
    {
        IReady ready = null;

        public Beautician(IReady rdy)
        {
            ready = rdy;
        }

        public void GetReady()
        {
            ready.GetReady();
            Console.WriteLine("Style hair ");

            if (ready is Female)
            {
                for (int i = 1; i <= 10; i++)
                {
                    Console.WriteLine("doing ready process " + i);
                }

            }
        }
    }

}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接