65.9K
CodeProject 正在变化。 阅读更多。
Home

理解装饰器模式

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.90/5 (44投票s)

2012年12月18日

CPOL

11分钟阅读

viewsIcon

95472

downloadIcon

552

在C# .NET中实现装饰器模式。

背景

在深入探讨理论和定义之前,让我们先看一个现实世界的问题。XYZ Organizers是一家新成立的活动管理公司,负责从头到尾处理婚礼活动。他们提供与婚礼活动相关的三项服务:

  • 摄影
  • 摄像
  • 餐饮

并且还要求客户支付一定的服务费。现在他们找到了我们ABC Technologies,为他们构建计费系统。在分析了他们的需求后,我们说没问题,开始吧。于是我们开始创建第一个类,WeddingService

public class WeddingService
{
    public decimal Cost
    {
        get
        {
            return 10000;   //Service charge for overall management
        }
    }
}

现在让我们扩展这个类以包含摄影费用。

public class WeddingServiceWithPhotography : WeddingService
{
    public decimal Cost
    {
        get
        {
            return base.Cost + 15000;
            //Wedding service charge + photography service charge
        }
    }
}

为了满足我们的需求,我们需要创建另外两个类,WeddingServiceWithPhotographyAndVideographyWeddingServiceWithPhotographyVideographyAndCatering,以包含摄像和餐饮的费用/服务费。

public class WeddingServiceWithPhotographyAndVideography : WeddingServiceWithPhotography
{
    public decimal Cost
    {
        get
        {
            return base.Cost + 15000;
            //Wedding service charge + photography service charge + videography service charge
        }
    }
}
public class WeddingServiceWithPhotographyVideographyAndCatering : 
       WeddingServiceWithPhotographyAndVideography
{
    public decimal Cost
    {
        get
        {
            return base.Cost + 50000;   //Wedding service charge + photography service charge + 
                                        //   videography service charge + catering service charge
        }
    }
}

WeddingServiceWithPhotographyVideographyAndCatering这个最终类将计算整个婚礼服务的费用。到目前为止,一切都按预期工作,客户很满意,我们也很满意。

请注意,我们在每一步都使用了继承来获取费用。另外,请注意,为了保持简单,我们返回了硬编码的金额。在实际实现中,金额将根据某些业务逻辑返回,例如摄影师/摄像师需要为活动工作的时间长度,或者餐饮服务费根据参加活动的人数等。

但是,随着时间的推移,企业会发生变化/增长,我们的客户XYZ Organizers也不例外。他们现在告诉我们,他们希望扩展他们的服务范围并保持灵活性。也就是说,他们的客户可以根据需要选择活动中的哪些服务。例如,客户只需要带有摄影和摄像的婚礼管理服务。我们微笑着回答说没问题,可以轻松完成(因为我们知道我们有一个类WeddingServiceWithPhotographyAndVideography,它计算带有摄影和摄像的婚礼服务费用)。但当他们听说客户还要增加两项服务:舞台装饰和场地装饰,并且客户可以要求其中一项或任意组合时,我们的脸色迅速变得苍白。这意味着我们必须为服务的每种组合创建类:

  • WeddingServiceWithStageDecoration
  • WeddingServiceWithHallDecoration
  • WeddingServiceWithStageAndHallDecoration
  • WeddingServiceWithStageAndHallDecorationAndCatering
  • WeddingServiceWithStageAndHallDecorationAndCateringAndPhotography
  • ...

这是成长的烦恼,我们意识到随着新服务的添加,我们的解决方案将会变得臃肿。

我们现在正在认真思考如何提供一个灵活的解决方案,能够根据客户的需求在活动中动态添加各项服务的费用。一个可能的解决方案是为每项服务创建一个类,并使用switch-case/if-else语句来根据需要为活动添加费用。但是,除了不是一个 proper OO 解决方案之外,当 if-else 语句随着新服务的添加而增长时,代码肯定会散发出“坏味道”(如果你懂我的意思的话)。那么,我们如何才能得到一个有意义且可管理的解决方案呢?我们自问。

看!装饰器模式!

装饰器模式向我们展示了如何解决上述问题。书本上的定义是:

装饰器模式在运行时动态地为一个对象附加额外的职责。装饰器为扩展功能提供了比子类化更灵活的替代方案。
(Head First Design Patterns, O'Reilly)

装饰器模式可用于在运行时动态地扩展(装饰)某个对象的功能,并且独立于同一类的其他实例,前提是设计时已完成一些基础工作。这是通过设计一个新的装饰器类来包装原始类来实现的。
(维基百科,http://en.wikipedia.org/wiki/Decorator_pattern)

现在,让我们再次看看这些定义,但这次是在我们的问题上下文中:

装饰器模式动态地为对象(婚礼活动)附加额外的职责(服务)。

装饰器模式可用于在运行时动态地扩展(装饰)某个对象(婚礼活动)的功能(服务),并且独立于同一类的其他实例(其他婚礼活动)。

开始有点明白了,不是吗?

对我来说,装饰器模式更像是一种继承和组合的优雅融合。我们稍后会看到这一点。

那么,装饰器模式是如何解决这些问题的呢?

根据定义,在装饰器模式设计的解决方案中,有一个组件类,然后有一个装饰器类。装饰器类持有组件类型的一个对象的引用,并且本身派生自组件类。

什么意思?

是的,理论上,装饰器类围绕组件类被链接在一起,并使用委托来赋予组件类新的职责。

听起来很吓人,对吧?是啊!够了那些废话,让我们开始工作吧。首先,让我们来看一下装饰器模式的UML类图的快照。

UML class diagram of the decorator pattern

嗯,UML图似乎与定义一致。Decorator类派生自Component,并持有一个Component对象的引用。但是ConcreteComponent和ConcreteDecorator类是怎么回事呢?在我们的上下文中,WeddingService类将是组件对象,而PhotographyServiceVideographyServiceCateringService等将是装饰器类(基本上,那些行为要在运行时动态添加的对象被制成装饰器)。装饰器模式要求装饰器和组件派生自同一个基组件(我们将在本文后面看到原因)。这就是为什么UML图显示了一个通用的Component类,所有其他具体组件和装饰器都派生自它。

好的。但是ConcreteDecorator类呢?它似乎派生自抽象的Decorator类。为什么?

我们稍后会找到答案。所以,根据模式,让我们先创建我们的组件接口。

public interface IEventService
{
    decimal Cost { get; }
}

然后是抽象基组件类EventService,它实现了IEventService

public abstract class EventService : IEventService
{
    public abstract decimal Cost { get; }
}

接下来要创建的是具体组件类WeddingService,它将派生自EventService

public class WeddingService : EventService
{
    public override decimal Cost
    {
        get
        {
            return 10000;   //Service charge for overall management
        }
    }
}

…以及抽象的Decorator类,我们将其命名为EventServiceDecorator

public abstract class EventServiceDecorator : EventService
{
    public IEventService EventServiceObj;
    public override decimal Cost
    {
        get
        {
            return EventServiceObj.Cost;
        }
    }
 
    public EventServiceDecorator(IEventService eventService)
    {
        this.EventServiceObj = eventService;
    }
}

这里需要注意的一点是,我们不一定必须创建一个抽象组件类,而是可以直接创建我们的具体组件和抽象装饰器类,只需实现IEventService接口即可。这完全取决于我们问题的上下文。

我们可以看到,EventServiceDecorator抽象类的构造函数接受一个IEventService对象作为参数,并将其存储在IEventService类型的实例变量中。这就是组合发生的地方。另外请注意,EventServiceDecorator抽象类重写了其基类EventServiceCost属性。这个被重写的Cost属性简单地返回EventServiceObj实例变量所代表的任何服务的成本。

让我们看看我们的具体装饰器类,即PhotographyService类。

public class PhotographyService : EventServiceDecorator
{
    public PhotographyService(IEventService eventService)
        : base(eventService)
    {
 
    }
 
    public override decimal Cost
    {
        get
        {
            return base.Cost + 15000;
            //Cost of whatever event service represented by EventService + Photography service
        }
    }
}

我们可以看到,我们的具体PhotographyService装饰器类的构造函数只是调用其基类版本,传递IEventService参数。那么,这个传递给构造函数的IEventService参数是什么呢?确实,它正是PhotographyService将要装饰的IEventService。看看被重写的Cost属性的get访问器。它首先通过访问其基类上的Cost属性来检索传递给构造函数的IEventService对象的成本,然后加上PhotographyService自己的成本。这就是PhotographyService的行为如何添加到传递的IEventService对象中。在这种情况下,它将摄影成本添加到EventServiceObj所代表的任何服务的成本中,无论是WeddingServiceSeminarService等。因此,我们可以看到如何通过PhotographyService装饰器与具体服务对象(婚礼服务)或其他装饰器对象进行组合,将新职责(在本例中是新服务,摄影)添加到现有对象(婚礼服务)中。等等,另一个装饰器对象?怎么可能?我们稍后会看到。

我认为我们现在对抽象装饰器类的用途已经相当清楚了。但同时,您也有权说,我们可以直接从EventService (IEventService)派生PhotographyService类,将EventService (IEventService)实例变量放在具体PhotographyService类中,而不是放在抽象装饰器类中,并在PhotographyService被重写的Cost get访问器中直接调用它,然后加上摄影本身的成本,从而在过程中消除了抽象装饰器类的需要。是的,我们可以做到。但那样的话,我们就必须在所有具体装饰器类中放置相同的IEventService实例变量,并在所有重写的Cost get访问器中一遍又一遍地编写相同的代码。此外,我想将操作被装饰的IEventService对象(无论其多么微小)的责任转移到抽象装饰器类。将来可能会出现一些常见的逻辑,取决于业务,在装饰之前应用于该对象。然后,有一种使用抽象装饰器类来实现装饰器模式的传统Wink | <img src=

所以还剩一个问题。为什么我们必须从抽象组件类/组件接口(IEventService)派生装饰器类,从而有效地使其成为一个组件?准确地说,是为了使其成为一个组件,允许它与其他组件互换。还记得我之前谈到链接装饰器吗?现在我们将看到这是如何展开的。但首先,让我们创建另一个服务装饰器。

public class CateringService : EventServiceDecorator
{
    public CateringService(IEventService eventService)
        : base(eventService)
    {
 
    }
 
    public override decimal Cost
    {
        get
        {
            return base.Cost + 50000;
            //Cost of whatever event service represented by EventService + Catering service
        }
    }
}

并且还要创建一个具体的事件服务类,SeminarService

public class SeminarService : EventService
{
    public override decimal Cost
    {
        get
        {
            return 5000;   //Service charge for overall management
        }
    }
}

假设有一个研讨会活动需要管理,而客户只需要摄影和餐饮服务。我们可以通过以下方式计算所有请求服务的账单:

IEventService seminarService = new SeminarService();
PhotographyService photographyService = new PhotographyService(seminarService);
CateringService cateringService = new CateringService(photographyService);
Console.WriteLine(cateringService.Cost);

首先,我们创建了原始事件服务,即SeminarService具体类,也就是具体组件。然后添加了摄影服务,最后添加了餐饮服务。上面代码的前两行似乎很直接,但您可能会关注这里的第3行。是的,我们将接受SeminarService对象的装饰器作为参数传递给了另一个装饰器。还记得我之前谈到的链接装饰器吗?这就是继承发挥作用的地方,并确切地展示了为什么我们从抽象组件类EventService(一种IEventService)派生我们的抽象装饰器类EventServiceDecorator,以使EventServiceDecorator成为另一种类型的EventService (IEventService)。简单地说,装饰器就是它将要装饰(包装)的同一类型的对象。这就是为什么在装饰器的情况下,我们通过继承来符合与它将要装饰(包装)的对象相同的接口,以实现类型匹配。因为这个装饰器又可以被传递给其他装饰器,以使用其他行为进行装饰。如果我们不保持类型相同,我们如何实现这一点?

所以,让我们在脑海中运行上面的示例程序。

首先调用cateringService.Cost,它将自己的成本加到它组合的组件的成本上,即PhotographyService对象。反过来,PhotographyService将自己的成本加到它组合的组件(SeminarService)的成本上,并将总金额返回给调用者,即cateringService.Cost的get访问器。这导致计算研讨会服务加上摄影和餐饮服务的最终成本。在此过程中,我们将两个装饰器链接到一个具体组件对象周围。

结论

我们通过将装饰器与该组件对象或另一个装饰器(实际上是一个组件)组合,动态地(在运行时)为对象(组件)添加新行为。装饰器模式用于为特定的“对象”提供附加功能,而不是为“类”的对象提供功能。因此,我们通过组合而不是继承来实现新行为,从而在运行时灵活地混合不同类型的装饰器。继承仅用于获取正确的蓝图。如果我们仅依赖继承来实现新行为,那么我们将像在本文开头所见的那样,在编译时静态地定义行为,从而导致解决方案膨胀,类几乎无限多。 

装饰器模式还很好地遵循了开放/封闭原则,即装饰器类对扩展开放,对修改关闭。在此基础上,以下是John Brett的评论:

“还值得一提的是,这种模式非常适合单元测试。每个装饰器由于需要,都与其他系统解耦,使得可以很容易地在构造函数中提供一个模拟对象,并全面地测试接口。”

最后,我敢打赌您可能已经使用过包含装饰器模式的.NET框架类。我把发现的任务留给您。好吧,提示:看看Stream类Wink | <img src=

© . All rights reserved.