Introduction
This article explains decorator pattern, its usage and compares it with other ways of achieving the same result.
Background
Well, the defintion says "The decorator pattern is a design pattern that allows behavior to be added to an individual object dynamically without affecting the behavior of other objects from the same class."
It also adhers to Single Responsibility and Open close principle.
Let's understand the definition with the help of code.
Using the code
A telecom organization 'xyz' hires a developer, John, to write an application to generate invoice for its customers in pdf format. John creates a class 'Invoice' with Generate() method in it.
public class Invoice
{
public PdfDocument Generate()
{
PdfDocument document = new PdfDocument();
PdfPage pdfPage = document.AddPage();
XGraphics graph = XGraphics.FromPdfPage(pdfPage);
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;
}
}
Now, his company says that application should add a water mark content in each page of invoice if customer has not paid the last month(s) bill. The watermark content may read something like this 'April & May bill' or 'March, April & May bill'. He changes the code as written below:
1) Adds a boolean property in this class 'Watermark'. If it is true, add the water mark content, else no water mark at all.
2) Add a parameterized construtor to accept the value of Watermark property.
3) Modifies the Generate method to add watermark logic based on this property value.
public class Invoice
{
public bool Watermark { get; private set; }
public Invoice(bool waterMark)
{
Watermark = waterMark;
}
public PdfDocument Generate()
{
PdfDocument document = new PdfDocument();
PdfPage pdfPage = document.AddPage();
XGraphics graph = XGraphics.FromPdfPage(pdfPage);
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.";
XSize size = graph.MeasureString(waterMarkText, font);
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);
XStringFormat format = new XStringFormat();
format.Alignment = XStringAlignment.Near;
format.LineAlignment = XLineAlignment.Near;
XBrush brush = new XSolidBrush(XColor.FromArgb(128, 255, 0, 0));
graph.DrawString(waterMarkText, font, brush,
new XPoint((pdfPage.Width - size.Width) / 2, (pdfPage.Height - size.Height) / 2),
format);
}
return document;
}
}
So far so good? No? what's wrong? Yes, John voilates the open close principle here. He modifes the existing code which may introduce bugs in existing functionality.
After consulting his technical architect, John takes a subclass approach. Instead of touching the existing Invoice class, he creates a child class of it, naming it as WatermarkedInvoice.
public class Invoice
{
public virtual PdfDocument Generate()
{
PdfDocument document = new PdfDocument();
PdfPage pdfPage = document.AddPage();
XGraphics graph = XGraphics.FromPdfPage(pdfPage);
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()
{
PdfDocument document = new PdfDocument();
PdfPage pdfPage = document.AddPage();
XGraphics graph = XGraphics.FromPdfPage(pdfPage);
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.";
XSize size = graph.MeasureString(waterMarkText, font);
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);
XStringFormat format = new XStringFormat();
format.Alignment = XStringAlignment.Near;
format.LineAlignment = XLineAlignment.Near;
XBrush brush = new XSolidBrush(XColor.FromArgb(128, 255, 0, 0));
graph.DrawString(waterMarkText, font, brush,
new XPoint((pdfPage.Width - size.Width) / 2, (pdfPage.Height - size.Height) / 2),
format);
return document;
}
}
Now, his organisation says that application should show the red colored border if customer's credit limit has already been crossed. The possible scenarios can be:
1) Customer crosses the credit limit in one month itself. So the invoice will have only red colored border but no water mark content.
2) Customer hasn't paid the last month bill and crosses the credit limit in current month. Invoice will have red colored border and water mark content both.
3) Customer hasn't paid the last month bill but usage is still under credit limit. Invoice will only have water mark content.
4) Customer has paid the last moth bill and current month usage is under credit limit. Invoice will neither have water mark content nor red colored border.
If John to continue with subclasses approach, he will end up creating following classes:
1) WatermarkedInvoice
2) RedColoredBorderInvoice
3) WatermarkAndRedColoredBorderInvoice
Imagine the number of classes if organisation adds more business logic which are independent of each other and based on that John needs to show any visual marks.
So, what's the solution? Decorator pattern as it allows behavior to be added to an individual object dynamically without affecting the behavior of other objects.
Decorator pattern creates/designs a base first and then decorate it accordingly. E.g.
1) Pizza, It has a base and all its toppings such as onion, mushroom, capsicum, tomato are decorating components i.e. Decorators. You may decorate pizza base with any number of decorating components.
2) Burger: A bun (base) decorated with Cheese, Veggies, Bacon etc. (decorators)
Similarly, in our current example, a basic invoice can be considered as base and watermark or red colored border are decorators.
In order to follow this design pattern, John creates following items:
Component (IInvoice): An interface implemented by both, base and decorators.
public interface IInvoice
{
PdfDocument Generate();
}
ConcreateComponent (Invoice): A base which needs to be decorated.
public class Invoice : IInvoice
{
public PdfDocument Generate()
{
PdfDocument document = new PdfDocument();
PdfPage pdfPage = document.AddPage();
XGraphics graph = XGraphics.FromPdfPage(pdfPage);
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;
}
}
Decorator(InvoiceDecorator): A base class for all decorators.
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;
}
}
ConcreateDecoratorA(DecoratedWithWatermark): A decorator.
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);
XGraphics gfx = XGraphics.FromPdfPage(page, XGraphicsPdfPageOptions.Prepend);
XSize size = gfx.MeasureString(watermark, font);
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);
XStringFormat format = new XStringFormat();
format.Alignment = XStringAlignment.Near;
format.LineAlignment = XLineAlignment.Near;
XBrush brush = new XSolidBrush(XColor.FromArgb(128, 255, 0, 0));
gfx.DrawString(watermark, font, brush,
new XPoint((page.Width - size.Width) / 2, (page.Height - size.Height) / 2),
format);
gfx.Dispose();
return invoice;
}
}
ConcreateDecoratorB (DecoratedWithRedColoredBorder): Another decorator
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);
XGraphics gfx = XGraphics.FromPdfPage(page, XGraphicsPdfPageOptions.Prepend);
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;
}
}
By doing this, John is able to add behavior dynamically e.g. adding watermark and RedColoredBorder indenpendently.
Client class looks like this:
Create the invoice object first and then injecting this object in decorated objects based on user inputs.
UI has 2 check boxes, one for water mark and other for red colored border. Depending on what the user chooses, base invoice will be decorated accordingly.
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);
I hope this examples shows how strong a decorator pattern can be in a situation like this.
Your comments/feedback are most welcome. Let's grow by learning together.
When you build the project, please make sure you have reference added for pdfsharp library.
History
First version.