The decorator pattern has a base implementation at the very core, then it's wrapped by various decorators which extend the nested objects behavior.
the definition of the Decorator pattern is: The decorator attaches additional responsibilities to an object dynamically, decorators provide a flexible alternative to sub-classing for extending functionality.
what it does is lets us wrap objects and extend object members, it doesn't have to replace behavior, but can extend it, or just replace it depends on your needs. So for example if had some sort of object that made some sort of calculation, and you wanted to modify the result of that calculation, then you could wrap that original object with a decorator that would then modify the result in some fashion.
let's take a look at the UML for this
the big take away is when looking at the relationship between the IWidget and IDecorator interfaces, the IDecorator not only has a reference to an IWidget it also inherits from it, meaning that it has both "has as" and "is a" relationship with the IWidget class. this allows us to wrap a widget within multiple decorators each time extending or modifying it's behavior.
Let's take a look at a couple generic examples to wrap our heads around this concept, to get started take a look at the following interface
interface IWidget
{
string Name { get; set; }
double Price { get; set; }
}
interface IWidgetDecorator : IWidget
{
IWidget widget { get; }
}
class WidgetDecorator : IWidgetDecorator
{
public IWidget widget => throw new
NotImplementedException();
public string Name {
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public double Price {
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
}
class WidgetDecorator : IWidgetDecorator
{
public IWidget innerWidget{ get; private set; }
string name;
string name;
public string Name
{
get => $"{widget.Name} {this.name}";
set => this.name =
value;
}
double price;
double price;
public double Price
{
get => widget.Price + price;
set => this.price =
value;
}
public WidgetDecorator(IWidget innerWidget, string name, double price)
{
this.innerWidget = innerWidget;
this.Name = name;
this.Price = price;
}
}
Now here we can see that we have a constructor that takes in an inner widget and a name and price for the decorator, then in the decorators properties we modify the inner widgets properties with the decorators values.
Now let's put our contrived example together with some execution code.
using System;
namespace pav.decoratorPattern
{
interface IWidget
{
string Name { get; set; }
double Price { get; set; }
}
interface IWidgetDecorator : IWidget
{
IWidget innerWidget { get; }
}
class Widget : IWidget
{
public string Name { get; set; }
public double Price { get; set; }
public Widget(string name, double price)
{
this.Name = name;
this.Price = price;
}
}
class WidgetDecorator : IWidgetDecorator
{
public IWidget innerWidget { get; private set; }
string name;
string name;
public string Name
{
get => $"{innerWidget.Name} {this.name}";
set => this.name =
value;
}
double price;
double price;
public double Price
{
get => innerWidget.Price + price;
set => this.price =
value;
}
public WidgetDecorator(IWidget innerWidget, string name, double price)
{
this.innerWidget = innerWidget;
this.Name = name;
this.Price = price;
}
}
class Program
{
static void Main(string[] args)
{
var widget = new Widget("Base Widget", 1);
var d1 = new
WidgetDecorator(widget, "Decorator one", .25);
var d2 = new
WidgetDecorator(d1, "Decorator two", .45);
Console.WriteLine($"D1: {d1.Name} {d1.Price}");
Console.WriteLine($"D2: {d2.Name} {d2.Price}");
}
}
}
Now when we execute our application we see the following result
Just as our logic defines with each layered decorator we concat all the widget names and add up their prices.
Next let's look at another contrived example but this time instead of using an interface let's use an abstract class as our abstraction. We're going go build an application for a used car lot.
Let's get started by defining our abstract Car class
abstract class Car
{
public abstract string Name { get; set; }
public abstract string Make { get; set; }
public abstract int Cost { get; set; }
}
class Sedan : Car
{
public override string Name { get; set; }
public override string Make { get; set; }
public override int Cost { get; set; }
}
class Coupe : Car
{
public override string Name { get; set; }
public override string Make { get; set; }
public override int Cost { get; set; }
}
class Van : Car
{
public override string Name { get; set; }
public override string Make { get; set; }
public override int Cost { get; set; }
}
So to use what we have thus far we'd simply, instantiate our car and find out the cost by calling the cost property, now realistically we'd have some business logic that would take in a cost and return a price to sell for, but lets keep it simple.
class Program
{
static void Main(string[] args)
{
Car van = new Van { Make = "Ford", Name = "Windstar", Cost = 500 };
Console.WriteLine(van.Cost);
}
}
To drum up some more revenue what we want is to offer some "Options" to spruce up old used cars, for example offer a paint job, or maybe even let the customer purchase a warranty.
We want to add options to our cars but we don't want to modify our old Car class or any of its sub classes, so to do this we can create a "Decorator"
abstract class CarDecorator : Car
{
Car _car;
public CarDecorator(Car Car) => _car = Car;
public override string Name { get => _car.Name; set => _car.Name = value; }
public override string Make { get => _car.Make; set => _car.Make = value; }
public override int Cost { get => _car.Cost; set => _car.Cost = value; }
}
class WarentyOption : CarDecorator
{
public WarentyOption(Car Car) : base(Car) { }
public override int Cost
=> base.Cost + 500;
}
class PaintJobOption : CarDecorator
{
public PaintJobOption(Car Car) : base(Car) { }
public override int Cost
=> base.Cost + 250;
}
Now to use this we just have to pass our existing car class into our decorators to let
class Program
{
static void Main(string[] args)
{
Car van = new Van{ Make = "Ford", Name = "Windstar", Cost = 500 };
van = new WarentyOption (van);
van = new PaintJobOption (van);
Console.WriteLine(van.Cost);
Car sedan = new Sedan{ Make = "Ford", Name = "Escort", Cost = 250 };
sedan = new WarentyOption (sedan);
sedan = new PaintJobOption (sedan);
Console.WriteLine(sedan.Cost);
}
}
Both of the above examples convey the Decorator pattern in it's simplest form, however the decorator pattern is not meant to modify properties but more so behaviors, here's a shape example in which we start with a circle and then wrap it in various decorators to find the surface area of a cylinder and sphere.
using System;
namespace pav.decoratorPatternSurfaceArea
{
interface ICircle
{
double Arg { get; set; }
double GetSurfaceArea();
}
interface I3DShapeDecorator : ICircle
{
ICircle BaseShape { get; }
}
class Circle : ICircle
{
public double Arg { get; set; }
public Circle(double radius)
=> Arg = radius;
//A=πr2
public double
GetSurfaceArea() => Math.PI * Math.Pow(Arg, 2);
}
class Cylinder : I3DShapeDecorator
{
public ICircle BaseShape { get; private set; }
public double Arg { get; set; }
public Cylinder(Circle circle, double height)
{
BaseShape = circle;
Arg = height;
}
//A=πr2 * h
public double
GetSurfaceArea() => BaseShape.GetSurfaceArea() * Arg;
}
class Sphere : I3DShapeDecorator
{
public ICircle BaseShape { get; private set; }
public double Arg { get; set; }
public Sphere(Circle circle)
{
BaseShape = circle;
Arg = 4;
}
//A=πr2 * 4
public double
GetSurfaceArea() => BaseShape.GetSurfaceArea() * Arg;
}
class Program
{
static void Main(string[] args)
{
var circle = new
Circle(5);
var sphere = new
Sphere(circle);
var cylinder = new
Cylinder(circle, 2);
Console.WriteLine($"Circle SA:{circle.GetSurfaceArea()}");
Console.WriteLine($"Sphere SA:{sphere.GetSurfaceArea()}");
Console.WriteLine($"Cylinder SA:{cylinder.GetSurfaceArea()}");
}
}
}
An appropriate problem that comes to mind is parsing a CSV file, let's say that you create an application that parses values out of a csv line by line, but somewhere down the line the csv file evolves and some of the lines in it now contain more information well you could wrap your original parser in a decorator, the original parser would handle the original file and the decorator could deal with a newer version of the file and as more and more versions of the CSV schema came to light you would wrap your original parser in more and more decorators thus maintaining backwards compatibility and handling newer versions