装饰器模式详解






4.67/5 (16投票s)
本文解释了装饰器模式及其用法。
引言
本文解释了装饰器模式,它的用法,并将它与实现相同结果的其他方式进行比较。
背景
定义是这样说的:“装饰器模式是一种设计模式,它允许动态地向单个对象添加行为,而不会影响来自同一类的其他对象的行为。”
它也遵守单一职责原则和开放-封闭原则。
让我们借助代码来理解这个定义。
使用代码
一家电信组织“xyz”雇佣了一位开发人员 **John**,来编写一个应用程序,以 pdf 格式为客户生成账单。John 创建了一个名为“Invoice”的类,其中包含 Generate() 方法。
public class Invoice
{
public PdfDocument Generate()
{
// Create a new PDF document
PdfDocument document = new PdfDocument();
// Create an empty page
PdfPage pdfPage = document.AddPage();
// Get an XGraphics object for drawing
XGraphics graph = XGraphics.FromPdfPage(pdfPage);
// Create a font
XFont font = new XFont("Verdana", 20, XFontStyle.Bold);
graph.DrawString("This is your bill....", font,
XBrushes.Black, new XRect(0, 0, pdfPage.Width.Point, pdfPage.Height.Point),
XStringFormats.Center);
return document;
}
}
现在,他的公司表示,如果客户没有支付上个月的账单,应用程序应该在每个账单页面中添加水印内容。水印内容可能类似于“四月和五月账单”或“三月、四月和五月账单”。 他修改了如下代码
1) 在此类中添加一个布尔属性“Watermark”。如果为 true,则添加水印内容,否则根本不添加水印。
2) 添加一个参数化构造函数来接受 Watermark 属性的值。
3) 修改 Generate 方法以基于此属性值添加水印逻辑。
public class Invoice { public bool Watermark { get; private set; } public Invoice(bool waterMark) { Watermark = waterMark; } public PdfDocument Generate() { // Create a new PDF document PdfDocument document = new PdfDocument(); // Create an empty page PdfPage pdfPage = document.AddPage(); // Get an XGraphics object for drawing XGraphics graph = XGraphics.FromPdfPage(pdfPage); // Create a font XFont font = new XFont("Verdana", 20, XFontStyle.Bold); graph.DrawString("This is your bill....", font, XBrushes.Black, new XRect(0, 0, pdfPage.Width.Point, pdfPage.Height.Point), XStringFormats.Center); if (Watermark) { string waterMarkText = "April and May bill."; // Get the size (in point) of the text XSize size = graph.MeasureString(waterMarkText, font); // Define a rotation transformation at the center of the page graph.TranslateTransform(pdfPage.Width / 2, pdfPage.Height / 2); graph.RotateTransform(-Math.Atan(pdfPage.Height / pdfPage.Width) * 180 / Math.PI); graph.TranslateTransform(-pdfPage.Width / 2, -pdfPage.Height / 2); // Create a string format XStringFormat format = new XStringFormat(); format.Alignment = XStringAlignment.Near; format.LineAlignment = XLineAlignment.Near; // Create a dimmed red brush XBrush brush = new XSolidBrush(XColor.FromArgb(128, 255, 0, 0)); // Draw the string graph.DrawString(waterMarkText, font, brush, new XPoint((pdfPage.Width - size.Width) / 2, (pdfPage.Height - size.Height) / 2), format); } return document; } }
到目前为止,一切都好吗? 没有? 哪里错了? 是的,John 在这里违反了开放-封闭原则。 他修改了现有代码,这可能会在现有功能中引入错误。
在咨询了他的技术架构师后,John 采取了一种子类方法。 他没有触及现有的 Invoice 类,而是创建了它的一个子类,并将其命名为 WatermarkedInvoice。
public class Invoice { //Other common members used by child classes as well. public virtual PdfDocument Generate() { // Create a new PDF document PdfDocument document = new PdfDocument(); // Create an empty page PdfPage pdfPage = document.AddPage(); // Get an XGraphics object for drawing XGraphics graph = XGraphics.FromPdfPage(pdfPage); // Create a font XFont font = new XFont("Verdana", 20, XFontStyle.Bold); graph.DrawString("This is your bill....", font, XBrushes.Black, new XRect(0, 0, pdfPage.Width.Point, pdfPage.Height.Point), XStringFormats.Center); return document; } }
public class WatermarkedInvoice: Invoice { public override PdfDocument Generate() { // Create a new PDF document PdfDocument document = new PdfDocument(); // Create an empty page PdfPage pdfPage = document.AddPage(); // Get an XGraphics object for drawing XGraphics graph = XGraphics.FromPdfPage(pdfPage); // Create a font XFont font = new XFont("Verdana", 20, XFontStyle.Bold); graph.DrawString("This is your bill....", font, XBrushes.Black, new XRect(0, 0, pdfPage.Width.Point, pdfPage.Height.Point), XStringFormats.Center); string waterMarkText = "April and May bill."; // Get the size (in point) of the text XSize size = graph.MeasureString(waterMarkText, font); // Define a rotation transformation at the center of the page graph.TranslateTransform(pdfPage.Width / 2, pdfPage.Height / 2); graph.RotateTransform(-Math.Atan(pdfPage.Height / pdfPage.Width) * 180 / Math.PI); graph.TranslateTransform(-pdfPage.Width / 2, -pdfPage.Height / 2); // Create a string format XStringFormat format = new XStringFormat(); format.Alignment = XStringAlignment.Near; format.LineAlignment = XLineAlignment.Near; // Create a dimmed red brush XBrush brush = new XSolidBrush(XColor.FromArgb(128, 255, 0, 0)); // Draw the string graph.DrawString(waterMarkText, font, brush, new XPoint((pdfPage.Width - size.Width) / 2, (pdfPage.Height - size.Height) / 2), format); return document; } }
现在,他的组织表示,如果客户的信用额度已经被超过,应用程序应该显示红色边框。 可能的情况有
1) 客户在一个月内就超过了信用额度。 因此,账单将只有红色边框,但没有水印内容。
2) 客户没有支付上个月的账单,并且在本月超过了信用额度。 账单将同时具有红色边框和水印内容。
3) 客户没有支付上个月的账单,但使用量仍在信用额度内。 账单将仅具有水印内容。
4) 客户已支付上个月的账单,并且本月的使用量在信用额度内。 账单既不会有水印内容,也不会有红色边框。
如果 John 继续使用子类方法,他最终会创建以下类
1) WatermarkedInvoice
2) RedColoredBorderInvoice
3) WatermarkAndRedColoredBorderInvoice
想象一下,如果组织添加更多彼此独立的业务逻辑,并且 John 需要基于此显示任何视觉标记,那么类的数量将会是多少。
那么,解决方案是什么? 装饰器模式,因为它 允许动态地向单个对象添加行为,而不会影响其他对象的行为。
装饰器模式首先创建/设计一个基类,然后根据需要对其进行装饰。 例如
1) 披萨,它有一个底,它的所有配料,如洋葱、蘑菇、辣椒、番茄都是装饰组件,即装饰器。 您可以使用任意数量的装饰组件来装饰披萨底。
2) 汉堡:一个面包(底)用奶酪、蔬菜、培根等(装饰器)装饰。
类似地,在我们当前的示例中,基本账单可以被认为是基类,而水印或红色边框是装饰器。
为了遵循这种设计模式,John 创建了以下项目
组件 (IInvoice):一个接口,由基类和装饰器实现。
//Component public interface IInvoice { PdfDocument Generate(); }
具体组件 (Invoice):需要装饰的基类。
//Concreate component public class Invoice : IInvoice { public PdfDocument Generate() { // Create a new PDF document PdfDocument document = new PdfDocument(); // Create an empty page PdfPage pdfPage = document.AddPage(); // Get an XGraphics object for drawing XGraphics graph = XGraphics.FromPdfPage(pdfPage); // Create a font XFont font = new XFont("Verdana", 20, XFontStyle.Bold); graph.DrawString("This is your bill....", font, XBrushes.Black, new XRect(0, 0, pdfPage.Width.Point, pdfPage.Height.Point), XStringFormats.Center); graph.Dispose(); return document; } }
装饰器 (InvoiceDecorator):所有装饰器的基类。
public abstract class InvoiceDecorator:IInvoice { protected IInvoice _invoice; public InvoiceDecorator(IInvoice invoice) { _invoice = invoice; } public virtual PdfDocument Generate() { PdfDocument invoice=null; if (_invoice != null) invoice = _invoice.Generate(); return invoice; } }
具体装饰器 A (DecoratedWithWatermark):一个装饰器。
public class DecoratedWithWatermark : InvoiceDecorator { public DecoratedWithWatermark(IInvoice invoice) : base(invoice) { _invoice = invoice; } public override PdfDocument Generate() { PdfDocument invoice = base.Generate(); invoice = AddWatermark(invoice); return invoice; } private PdfDocument AddWatermark(PdfDocument invoice) { PdfPage page = invoice.Pages[0]; string watermark = "April and May bill."; XFont font = new XFont("Verdana", 20, XFontStyle.Bold); // Get an XGraphics object for drawing beneath the existing content XGraphics gfx = XGraphics.FromPdfPage(page, XGraphicsPdfPageOptions.Prepend); // Get the size (in point) of the text XSize size = gfx.MeasureString(watermark, font); // Define a rotation transformation at the center of the page gfx.TranslateTransform(page.Width / 2, page.Height / 2); gfx.RotateTransform(-Math.Atan(page.Height / page.Width) * 180 / Math.PI); gfx.TranslateTransform(-page.Width / 2, -page.Height / 2); // Create a string format XStringFormat format = new XStringFormat(); format.Alignment = XStringAlignment.Near; format.LineAlignment = XLineAlignment.Near; // Create a dimmed red brush XBrush brush = new XSolidBrush(XColor.FromArgb(128, 255, 0, 0)); // Draw the string gfx.DrawString(watermark, font, brush, new XPoint((page.Width - size.Width) / 2, (page.Height - size.Height) / 2), format); gfx.Dispose(); return invoice; } }
具体装饰器 B (DecoratedWithRedColoredBorder):另一个装饰器
public class DecoratedWithRedColoredBorder : InvoiceDecorator { public DecoratedWithRedColoredBorder(IInvoice invoice) : base(invoice) { _invoice = invoice; } public override PdfDocument Generate() { PdfDocument invoice = base.Generate(); invoice = AddBorder(invoice); return invoice; } private PdfDocument AddBorder(PdfDocument report) { PdfPage page = report.Pages[0]; XFont font = new XFont("Verdana", 20, XFontStyle.Bold); // Get an XGraphics object for drawing beneath the existing content XGraphics gfx = XGraphics.FromPdfPage(page, XGraphicsPdfPageOptions.Prepend); // Adds a border XPen pen = new XPen(XColors.Red, Math.PI); gfx.DrawRectangle(pen, XBrushes.White, 10, 10, page.Width - 20, page.Height - 20); gfx.Dispose(); return report; } }
通过这样做,John 能够动态地添加行为,例如独立地添加水印和红色边框。
客户端类如下所示
首先创建 invoice 对象,然后根据用户输入将此对象注入到装饰后的对象中。
UI 有 2 个复选框,一个用于水印,另一个用于红色边框。 根据用户的选择,基本 invoice 将相应地进行装饰。
IInvoice invoice = new DecoratorPattern.UsingDecoratorPattern.Invoice(); foreach (var checkedItem in checkedListBox1.CheckedItems) { if (String.Compare(checkedItem.ToString(), "Watermark", true) == 0) { invoice = new DecoratedWithWatermark(invoice); } if (String.Compare(checkedItem.ToString(), "Border", true) == 0) { invoice = new DecoratedWithRedColoredBorder(invoice); } } PdfDocument document= invoice.Generate(); string pdfFilename = "DecoratedDesignPatternExplained.pdf"; string path = Path.Combine( System.IO.Path.GetDirectoryName( System.Reflection.Assembly.GetEntryAssembly().Location) , pdfFilename ); document.Save(path); this.textBox1.Text = String.Format("{0}: PDF generated at {1}", DateTime.Now.ToShortTimeString(), path);
我希望这个例子展示了装饰器模式在这种情况下有多强大。
欢迎您的评论/反馈。 让我们一起学习成长。
构建项目时,请确保已添加 pdfsharp 库的引用。
历史
First version.