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

装饰器模式(通俗易懂)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (70投票s)

2010年4月16日

CPOL

12分钟阅读

viewsIcon

82576

downloadIcon

529

本文采用基于场景的方法,让读者理解装饰器模式

编辑说明:内容未编辑,仅修复了链接。

引言

我的一个朋友曾经说过,学习任何编程学科最难的部分就是运行 “Hello World”。之后,一切就都简单了。多年后,我才意识到他说得多么正确。这是本系列设计模式文章的基本目标,旨在帮助初学者入门。它特别针对那些拥有扎实的 OOP 概念但仍在与设计模式作斗争,并且无法弄清楚如何在设计中应用它们的人。我不敢声称写了一篇关于不同设计模式的综合性参考文章,但我确实希望本系列能帮助您入门。设计模式不能与特定语言绑定。尽管我提供了大部分 C# 代码示例,但我会尽量避免使用 C# 特有的结构,从而面向尽可能多的读者,特别是那些从事受 C++ 对象模型影响的语言的读者。
装饰器模式允许我们动态地为对象添加额外的行为。就像在我之前的文章 策略模式入门 中一样,我们将首先经历一个场景。我们还将审视替代方法,这将有助于我们认识到这种设计模式的真正力量及其提供的灵活性。

思路过程

我们今天将要遵循的场景可能不太现实,有时甚至很奇怪,但它将帮助我们理解这种模式的概念和底层力量。目标是以文本文件的形式将来自不同来源的消息存储到硬盘上。所以,首先我们定义了意图(接口),然后实现了它。IMessageWriterIMessageReader 接口分别用于写入和读取消息,它们的实现如下所示。

interface IMessageWriter
{
string Message { set; }
void WriteMessage(string filePath);
}
    class MessageWriter : IMessageWriter
    {
        private string message;

        #region IMessageWriter Members

        public string Message
        {
            set { this.message = value; }
        }
        public virtual void WriteMessage(string filePath)
        {
            File.WriteAllText(filePath, this.message);
        }

        #endregion
    }

    interface IMessageReader
    {
        string ReadMessage(string filePath);
    }

    class MessageReader : IMessageReader
    {
        #region IMessageReader Members

        public virtual string ReadMessage(string filePath)
        {
            if (File.Exists(filePath))
                return File.ReadAllText(filePath);
            else return null;
        }

        #endregion
    }

消息将存储在 Message 属性中,而 MessageWriterWriteMessage 只是将消息写入传递给它的文件路径。同样,MessageReaderReadMessage() 函数将从文件中读取消息并将其作为文本 string 返回。假设您收到客户的新需求。

  1. 对于某些消息,我们需要在读取和写入文件之前验证用户。
  2. 对于某些消息,我们希望将它们加密保存,这样没有人能够从文件中读取它们。但我们需要将此加密消息保存为 base4 编码字符串。
  3. 对于某些消息,我们需要这两种功能。

很奇怪,对吧?首先,我们将分析不使用装饰器的不同解决方案。这将让我们认识到这种简单的设计模式有多强大。

经典解决方案

您决定使用继承,因为它允许您在基本行为的基础上进行构建。您选择在 MessageWriter 的派生类 EncryptedMessageWriter 中实现加密行为。

EncryptedMessageInheritence.jpg

同样,您从 EncryptedMessageWriter 派生了 SecureMessageWriter(用于用户验证的类)。

SecureMessageInherited.jpg

现在我们可以编写加密消息以及带用户验证的加密消息。那么,如何在不加密的情况下编写简单的文本消息,但带有用户验证呢?您可以使用一些技巧,在 EncryptedMessageWriter 中放置一些决策代码,使其在不需要时跳过加密。假设您仍然选择此选项。另一种操作顺序怎么样?也就是说,我们想先加密,然后验证,如果未验证,我们还会对 base64 编码的加密消息执行其他操作。显然,上述层次结构无法处理这种情况。谁能阻止客户提出更多功能呢?例如,某些消息应该进行数字签名,较大的消息应该根据需要进行压缩(是否加密),对于某些消息,在将它们写入磁盘后,您必须在 MessageQueue 中记录文件路径和时间戳,供其他应用程序读取,或者甚至写入数据库。等等,等等。

为了评估您所处情况的严重性和复杂性,让我们只关注验证,而忘记层次结构的细节。目前,我们已经实现了加密消息情况下的验证。现在,我们需要在许多其他情况下拥有相同的功能,例如,对于 CompressedMessageWriterDigitallySignedMessageWriter 等。您唯一的选择是实现 SecureCompressedMessageWriterSecureDigitallySignedMessageWriter 等。同样,对于大量其他组合,例如加密消息压缩、简单消息压缩等等。该死,你真的陷入了子类地狱

第二种解决方案是编写一个相当庞大的 MessageReader,并随着需求的到来不断添加所有功能。随着时间的推移,增加了其复杂性,使其难以维护。最不推荐的方法。

第三种解决方案可能是上述两种的组合,这可能会缓解问题,但不会完全消除它。

装饰器模式登场

这正是装饰器模式所要解决的问题。如果您仔细观察上面涉及继承的解决方案,您会意识到问题的根源是继承引入的静态关系。这种关系嵌入到无法在运行时更改的类中。装饰器用包含(聚合)替换了这种关系,这是一种更灵活的、可以在运行时更新的对象关系。

首先,让我们看看装饰器模式到底是什么。下面是装饰器模式的类图。

image004.gif

四个参与者是

  1. Component:对象接口,可以动态地为其添加职责。在我们的例子中是 IMessageWriter IMessageReader
  2. ConcreteComponent:定义实现 Component 接口的对象。这是将被装饰的对象,但它对装饰一无所知。实现装饰器的人甚至可能无法访问源代码。在我们的例子中是 MessageWriterMessageReader
  3. Decorator:维护对 Component 对象的引用,并定义一个符合 Component 接口的接口。因此,它包含对基本行为的引用,并且还实现了相同的接口,因此可以被视为 Component 本身。期望 Component 的客户端代码将与 Decorator 打交道,而不会注意到区别。
  4. ConcreteDecorator:这实际上为组件添加了职责。从 Decorator 继承的类,并可能以新的 public 方法的形式添加一些额外的特定功能。

到目前为止,我们有了两个东西:Component,即基本行为,在我们的例子中是 IMessageWriter 和用于读取的 IMessageReader,以及 ConcreteComponent,即我们的写入和读取行为的实现,分别是 MessageWriterMessageReader

现在,这是我们对 SecureMessageWriterEncryptedMessageWriter 的实现。

SecureMessageWriter.jpg

EncryptedMessageWriter.jpg

您的 Decorator 到底在哪里???

我刚才说过这个模式有四个参与者,我已经向您展示了ComponentIMessageReaderIMessageWriter)、ConcreteComponentMessageReaderMessageWriter)和ConcreteDecoratorsSecureMessageWriterEncryptedMessageWriter)。但Decorator在哪里?在我们的例子中,我们只是在增加现有行为,并没有引入新行为。我们也没有改变层次结构。在这种情况下,我们可以忽略实现Decorator,遵循主要层次结构。我没有展示Reader 类,因为它们只是反向过程。

我们取得了什么成就

现在,当我需要一个简单的消息写入并带有用户验证时,我会这样做。

IMessageWriter msgWriter = new SecureMessageWriter(new MessageWriter());

我正在用 SecureMessageWriter 装饰 MessageWriter,它现在将在将消息写入磁盘之前验证用户。如果需要带验证的加密消息写入,我会这样做

IMessageWriter msgWriter = 
    new SecureMessageWriter(new EncryptionMessageWriter(new MessageWriter()));
  1. 装饰器避免了我们创建一个复杂的基类,其中包含大量代码,而这些代码在大多数情况下并不需要。
  2. 它们允许我们创建不同组合和序列的不同行为,这否则并非易事。
  3. 我们没有为每种不同的行为和组合实现不同的子类,而是单独实现了每种所需行为,可以根据需要添加。

回到现实世界

在本节中,我们将看到装饰器模式的一些实际应用示例。

同步包装器

使用过 .NET 中经典的集合类(如 QueueArrayList 等)的人可能仍然记得这些类公开的 synchronized(collection) 函数。它以集合实例本身作为参数,并返回一个同步的集合。集合类本身不是同步的,但此方法实际返回的是派生自集合类本身的装饰器。例如,在下面的代码中,当我们调用 Syncrhonized(al) 时,我们将收到一个 SyncArrayList 实例,这是一个派生自 ArrayList 的类。

ArrayList al = new ArrayList();
al = ArrayList.Synchronized(al);
//Now al can be treated just as ArrayList
//but actually it's an instance of SyncArrayList
//which is derived from ArrayList 

SyncArrayList 将在 _list 属性中存储传入的 ArrayList,并将覆盖 ArrayList 的不同实例方法。

 public override int Add(object value)
    {
        lock (this._root)
        {
            return this._list.Add(value);
        }
    }
注意事项

以这种方式创建自己的同步包装器时,请注意所谓的自死锁现象,即已经拥有锁的线程将进入另一个方法(或在递归的情况下进入同一方法),在那里它将尝试再次获取同一对象的锁。在 Windows 中,无论您是使用 .NET 的 Monitors 实现,还是内核级别的命名或匿名互斥体,所有这些都是可重入的(即递归的)。因此,您不会遇到此问题,但在 Linux 等其他环境中编程时,其中默认的互斥体类型是 Fast mutex(非递归互斥体),您的代码可能会成为自死锁的受害者。在使用信号量(即使在 Windows 上)时,它也没有所有权的概念,如果您不小心,它也会给您带来这个问题。当然,对于二元信号量,即 n=1,只有在第二次尝试时您才会陷入自死锁。

同样,您可以为集合类实现一个只读包装器。与我们到目前为止所见的不同,它们不是在类中添加功能,而是移除一些功能。例如,在重写的 Add() 方法中,可能会抛出 OperationNotSupportedException。.NET 提供了 ReadonlyCollection<T>,它是通用列表的包装器。Java 提供了不可修改的包装器,例如 unmodifiableCollectionunmodifiableSet 等。
在 Java 中,您还可以为许多集合类型获取同步包装器,例如

List<t> list =
    Collections.synchronizedList(new ArrayList<t>());
Java 和 .NET 中的 IO

Java 的 java.io 包和 .NET 的 Stream 类都采用了这种模式。我将不详细介绍它们的底层实现。在 .NET 中,Stream 是一个 abstract 类,它提供了基本行为(我们的 Component),而像 FileStreamMemoryStream 这样的类是 ConcreteCompents,而像 BufferedStreamCryptoStream 等类是 Concrete Decorators,它们像 FilestreamMemoryStream 一样派生自抽象的 Stream 类。您可以清楚地注意到,它们的实现也缺少 Decorator。

同样,在 Java 中,BufferedReaderFilterReaderReader 的装饰器,而 BufferedReader 进一步被 LineNumberReader 装饰,FilterReaderPushbackReader 装饰。

有什么问题

这种模式允许我们在实现中提供可扩展性点。正如您可能注意到的,在实现我的装饰器时,我甚至从未触及过我的组件类。因此,即使一个人不拥有一个类,也可以对其进行装饰以动态甚至递归地添加新行为。这种附加行为可能在基本行为之前或之后添加,或者这种行为可能阻止它。装饰器可以提供新的方法,甚至新的属性,正如您在类图中看到的。但是,装饰器存在一些问题。

  1. 将要使用装饰器的程序员必须理解其意图;否则,他可能会最终使用一些无意义的组合或序列。例如,在我们的场景中,如果程序员这样定义顺序:消息将被压缩,然后加密,然后验证,这将有点无意义。在实际场景中,对于某些组合或序列,结果可能会是灾难性的。
  2. 测试装饰器类需要提供一个被装饰类的模拟。由于我们还没有介绍测试,所以我不会讨论它
  3. 此外,此模式对基本行为设计者施加了一些责任。虽然在装饰器中添加新属性或 public 方法是完全合法的,但这样它将失去通过基本引用处理的灵活性,并且您将需要使用具体实例

现在一些澄清

我感谢所有在我上一篇关于策略模式的文章中提供反馈的人。这指导了我写这篇。 很多人以这样或那样的方式指责我过于简化。嗯,我断言我并没有过度简化任何东西。 这些词可能有点误导。我想说的是,他们的反馈是正确的,因为简化是我的目标。但过度简化有点苛刻。那些人并非完全错误,因为我避免将设计模式与编程习语混淆。我在这系列文章中展示的是基本原理以及在何处应用模式的指南。基本原理很简单;是问题让你头疼。识别适合设计模式的问题部分可能很棘手,需要经验。有时,直到经过多个重构周期,您才会意识到某段代码是应用某些设计模式的好候选。这为我们提供了分析问题并识别不同设计模式的另一个理由,以避免将来进行大量重构,因为它们已经是大量重构的结果。然而,该反馈对我帮助很大,将来我也会尝试涵盖特定于语言的实现(前提是我有足够的时间)。下一篇文章将介绍创建型模式,涵盖简单工厂、工厂方法、抽象工厂和生成器。再次,您的反馈非常受欢迎,请指出我未能清晰解释的领域,或改进的要点,谢谢。

关于源代码

当我开始写这篇文章时,我计划为 Java 和 .Net 发布代码。但由于最近日程很紧,我只能提供 .Net 的代码。这是一个 Visual Studio 2008 解决方案文件。我忘记了在解决方案中包含自述文件,我现在将其附加。

© . All rights reserved.