深入了解结构模式 :: 装饰器模式 :: 第二部分





5.00/5 (2投票s)
在我们之前的文章中,我们讨论了装饰者模式的作用,并对其进行了非常详细的阐述。此外,我们还讨论了它的设计变种,并解释了何时以及为何使用每种设计。
引言
在我们之前的文章中,我们讨论了装饰者模式的作用,并对其进行了非常详细的阐述。此外,我们还讨论了它的设计变种,并解释了何时以及为何使用每种设计。
今天,我们将继续讨论装饰者模式。我们将深入探讨现实世界中的不同案例,以加深您对该模式的理解。此外,我们还将讨论何时在现实世界中使用此模式。
我们将首先演示一个简单的示例,然后转向更复杂的示例和更多变种。
1. 示例:用例图工具
用例图是统一建模语言 (UML) 的基本图之一。我们使用用例图来描述用户在应用程序中可以交互的所有用例。
在这里,我们将开始构建一个简单的(确实非常简单,仅用于演示目的)用例图工具。
如果我们更仔细地查看在用例图中使用的主体形状,我们会发现它们是 3 种主要形状
- 参与者
- 用例
- 箭头(表示参与者之间、用例之间或参与者与用例之间的关系)
但仔细观察,我们会发现每种形状在不同情况下都有不同的外观。
如以下图所示,在绘制用例图的过程中,可以应用于任何形状的一些变体

这里的挑战在于,我们希望构建一个涵盖所有可用变体的应用程序,并且将来如果 UML 有任何更新,能够添加更多形状。
这就是装饰者模式的魔力所在。让我们开始吧
分析上图表明,我们有 3 种主要形状及其不同的变体。这里的变体意味着可以应用于不同形状的装饰器,以显示正确的最终形状。
进一步分析,我们注意到存在一些混合装饰器,例如带标签的点划线。实际上,它们是两个装饰器:标签装饰器和点划线装饰器。
让我们先从一种形状开始,然后进行详细说明。
IShape 接口和 Actor 类
IShape 接口
public interface IShape
{
Bitmap Draw();
}
Actor 类
public class Actor : IShape
{
public Bitmap Draw()
{
Bitmap image = new Bitmap(200, 200);
using (Graphics graphics = Graphics.FromImage(image))
{
using (Pen blackPen = new Pen(Brushes.Black))
{
// Drawing head
graphics.DrawEllipse(blackPen, 100, 20, 20, 20);
// Drawing backbone
graphics.DrawLine(blackPen, 110, 40, 110, 80);
// Drawing arms
graphics.DrawLine(blackPen, 110, 55, 130, 55);
graphics.DrawLine(blackPen, 110, 55, 90, 55);
// Drawing legs
graphics.DrawLine(blackPen, 110, 80, 130, 100);
graphics.DrawLine(blackPen, 110, 80, 90, 100);
}
}
return image;
}
}
如果我们现在尝试在 .NET 中将此类与 Windows Forms 应用程序一起使用,例如,在将 PictureBox 拖到窗体后,您将编写如下代码:
Actor actor = new Actor();
pictureBox1.Image = actor.Draw();
运行项目,您将看到
现在我们需要添加一些“糖”,所以让我们添加一个名为 TagDecorator
的装饰器,它将负责为任何形状添加标签(标题)。
这是类图(遵循装饰者模式的指导方针)
TagDecorator 类
public class TagDecorator : IShape
{
private IShape _shape;
private string _tag;
public TagDecorator(IShape shape, string tag)
{
_shape = shape;
_tag = tag;
}
public Bitmap Draw()
{
// Getting the actor shape without tags
Bitmap image = _shape.Draw();
// Writing the tag under the actor shape
using (Graphics grahpics = Graphics.FromImage(image))
{
grahpics.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAlias;
StringFormat stringFormat = new StringFormat
{
Alignment = StringAlignment.Center,
LineAlignment = StringAlignment.Center
};
grahpics.DrawString(_tag, new Font("Times New Roman", 12),
Brushes.Black, new RectangleF(10, 110, 200, 20), stringFormat);
}
return image;
}
}
让我们尝试将此装饰过的 Actor 绘制到窗体上。所以我们将编写这段代码:
Actor actor = new Actor();
TagDecorator actorTag = new TagDecorator(actor, "User");
pictureBox1.Image = actorTag.Draw();
并查看输出
现在让我们使事情变得更复杂,我将只给您概念,让您尝试实现它们。
如果我们想添加更多形状怎么办?很简单,我们将使用以下图:
那更多装饰器呢?!
去,尝试构建自己的绘图工具。
2. 现实世界中的示例
现在我们将展示一些我们可能每天都在使用但不知道的现实世界中的例子。
在 Rob Pierry 的一篇优秀文章中,他讨论了 .NET 中何时使用了装饰者模式。他说:
“任何有用的可执行程序都涉及读取输入、写入输出或两者兼有。无论正在读取或写入的数据源是什么,都可以将其抽象地视为一个字节序列。 .NET 使用 System.IO.Stream
类来表示此抽象。无论数据涉及文本文件中的字符、TCP/IP 网络流量,还是其他任何内容,您很可能可以通过 Stream
访问它。由于处理文件数据的类 (FileStream
) 和处理网络流量的类 (NetworkStream
) 都继承自 Stream
,因此您可以轻松地编写独立于其来源处理数据的代码。以下是从 Stream
中打印一些字节到控制台的方法:
public static void PrintBytes(Stream s)
{
int b;
while((b = fs.ReadByte()) >= 0)
{
Console.Write(b + " ");
}
}
一次读取一个字节通常不是访问 stream
的最有效方式。例如,硬盘能够(并经过优化)一次性从磁盘读取连续的数据块。如果您知道要读取几个字符,最好一次性从磁盘读取一个块,然后从内存中逐字节消耗该块。Framework 包含 BufferedStream
类来实现这一点。BufferedStream
的构造函数将您想要缓冲访问的任何 stream
作为参数。BufferedStream
重写了 Stream
的主要方法,如 Read
和 Write
,以提供更多功能。由于它仍然是 Stream
的子类,因此您可以使用它与任何其他 Stream
相同(请注意,FileStream
包含自己的缓冲功能)。同样,您可以使用 System.Security.Cryptography.CryptoStream
来实时加密和解密流,而无需应用程序的其余部分知道比它是一个 Stream
的事实。
这种通过组合透明地将新功能动态附加到对象的这种能力是装饰者模式的一个例子”。
据我所知,这对于 Java 开发人员来说在 java.io
命名空间中也是有效的。
让我们也展示一些更普遍的现实示例。
以下是装饰者模式在现实世界中的四种用法(摘自 C# 3.0 设计模式)
- 正如我们的小例子所示,装饰者模式非常适合图形世界。它同样适用于视频和音频;例如,视频流可以以不同的速率压缩,音频可以输入到实时翻译服务。
- 在更平凡的层面,如上所示,装饰器在 C# 的 I/O API 中随处可见。
- 在当今的移动设备、Web 浏览器和其他移动应用程序时代,装饰者模式得到了广泛应用。例如,它们可以创建适合较小屏幕的显示对象,这些对象包含滚动条并排除在桌面显示浏览器中会是标准的横幅。
- 装饰者模式非常有用,以至于 .NET 3.0 中现在有了实际的 Decorator 类。
System.Windows.Controls
中的类“为将效果应用于单个子元素或围绕单个子元素(如Border
或Viewbox
)的元素提供了基类。”
3. 何时使用?
当您拥有
- 一个现有的组件类,可能无法进行子类化
或者当您想
- 动态地为对象附加额外的状态或行为
- 更改某些类中的对象而不影响其他对象
- 避免子类化,因为可能导致过多的类
4. 最后
最后,我们完成了对该模式及其细节的演示。欢迎您的反馈。