What
Decorator Pattern is used to extend existing functionality through composition instead of inheritance. Decorator Pattern can be used to intercept and modify the behavior of an existing class object. For example, say client code uses class object XYZ that implements an interface, ISomeInterface. Currently, the client code instantiates class object XYZ and uses it through its ISomeInterface. XYZ is almost perfect for us except a few behaviors we would like to tweak. We do not want to touch the code for XYZ because it has been tested and has been functioning well. Furthermore, we do not want to substantially change the client code that uses XYZ. Following the Decorator Pattern, we implement a new class KLM that implements ISomeInterface. KLM contains an instance of XYZ class object. KLM delegates the desired behavior to the contained XYZ class object but overrides undesired behavior where necessary. The only change that the client code needs to make is to instantiate KLM instead of XYZ and use it through its ISomeInterface as before.Decorator Pattern can be applied in a variety of modeling scenarios. Let's say we have an application that calculates the price of a car. There is the base price of the car and then there are additional costs for options such as sports suspension, all weather package and so on. The base price and the price of options may vary independent of each other. Further, the list of available options may change over time. Decorator Pattern is one way to model this kind of scenario.
One way to visualize how the Decorator Pattern works is to visualize an onion. An onion has layers. Let's say the cost of each layer in the onion is different. The cost of the onion is equal to the cumulative cost of all layers. This visualization of the onion costing is not all that different from the car pricing scenario described above. We start with the base model for the car and decorate or wrap it with options. To calculate the total price of the car, we ask the outermost layer what is your cost. The outermost layer asks the layer next to it: what is your cost and adds to its own cost. The inner layer in turn asks the layer next to it what is your cost and adds to its own cost and so on.
In the car pricing example, we can think of the base price and options as just car components. We wrap each component around another component until we have the car with the desired options. We ask the outermost car component to calculate its cost which in turn requests the cost from inner components to calculate the total cost of the car.
The salient features of the Decorator Design Pattern are as follows:
Common Super Type
All decorators used in a scenario derive from the same type or implement the same interface so that they can be used interchangeably. For the car pricing example, all car components implement the same interface in the code example below.Composition
Each concrete implementation of a decorator contains a reference to the class object that it wraps. The object that a decorator wraps has the same super type as the decorator. For the car pricing example, the innermost decorator would not be wrapping or decorating anything because it is the base price for the car.Delegation
Each decorator knows how to compute itself but delegates the computation of the decorator objects it wraps to the wrapped objects themselves.
Why
The decorator objects are independent of each other except that they share a common super type or interface. We can leave existing class implementations untouched while extending their behavior through composition and delegation. A decorator does not care about how many other decorators there are in its family. At runtime, a client can create a custom scenario or configuration by composing desired decorators. When new decorators are needed, they are simply implemented and used by the client following the same pattern as before. This is a perfect example of extension of functionality without modification to existing code (decorators). In object oriented terms, this is an application of the open-closed principle. The design is open for extension but closed to modification of existing decorator classes. In the car pricing example above, if we used inheritance instead of composition, the open-closed principle would be much harder to follow. By using the Decorator Pattern, we can implement additional options (decorators) for the car without having to touch code for existing options (decorators).
How
// Common interface all decorators in our car example implementinterface ICarComponent
{
decimal Cost();
}
// Implements common functionality for all Car Components
abstract class CarComponent : ICarComponent
{
protected decimal m_ThisComponentCost;
public CarComponent (decimal thisComponentCost)
{
this.m_ThisComponentCost = thisComponentCost;
}
public decimal Cost()
{
// m_ThisComponentCost is set by the constructor
decimal cost = m_ThisComponentCost;
// Cost of the car component this component decorates
if (m_CarComponent != null)
cost += m_CarComponent.Cost();
return cost;
}
}
// Base Price for the car
class AMWTurboBasePrice : CarComponent
{
public AMWTurboBasePrice ()
: base (33000m) // base price for the car
{
this.m_CarComponent = null;
}
}
// Pricing for Sports suspension
class SportsSuspension : CarComponent
{
public SportsSuspension (ICarComponent carComponent)
: base (4000m) // price for sports suspension
{
this.m_CarComponent = carComponent;
}
}
// What is the price of AMWTurbo with sports suspension
ICarComponent basePrice = new AMWTurboBasePrice();
ICarComponent basePriceWithSportsSuspension =
new SportsSuspension(basePrice);
Console.WriteLine(
String.Format("The cost of car is {0}", basePriceWithSportsSuspension.Cost());
// All Weather package just became available for our car
// We implement a new decorator
class AllWeatherPackage : CarComponent
{
public AllWeatherPackage (ICarComponent carComponent)
: base (2000) // cost of all-weather package
{
this.m_CarComponent = carComponent;
}
}
// What is the price of the AMWTurbo with
// sports suspension AND all weather package
ICarComponent basePriceWithSportsSuspensionAndAllWeatherPkg =
new AllWeatherPackage(basePriceWithSportsSuspension);
Console.WriteLine(
String.Format("The cost of car is {0}",
basePriceWithSportsSuspensionAndAllWeatherPkg.Cost());