Decorator Design Pattern using C#
In this pattern, we would learn how an object can be decorated with additional functionality dynamically without subclassing. It is also known as Wrapper. The decorator pattern follows the Single Responsibility Principle, as it allows functionality to be distributed among classes with unique responsibility. Let’s take a deep dive into the pattern.
As per GoF, the decorator pattern is defined as shown below:
"Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality."
What we learn from the definition as stated above is that Decorator pattern is often used when we want to add behavior to an individual object not to an entire class at the run time, without affecting the behavior of other objects from the same class.
Example:
Let’s say we have a shop that sells phones of all type, iPhone, Android, Windows Phone, etc. Phones either fall in SmartPhone category or in normal Phone category. Every phone has its price, brand name and description along with it. Now shopkeeper wants to offer students discount on the phone prices. He has also got festival discounts on the phone.
Normal Approach
A bad design would lead you to change the behavior of the Phone class. But that is very inflexible as every time shopkeeper wants to offer a new discount based on festive season, he needs to change the Phone class. That means client can’t control how and when to decorate the object.
Design Pattern Approach
A more flexible approach is to enclose the object in another object that adds extra behavior to the object. Mapping to our example, if we have StudentOffer and ChristmasOffer classes, these classes enclose the main components i.e. Phone. The class which decorates the main object is called Decorator. Decorator maintains transparency to the decorated object’s client.
Base Design
Participants in the pattern are: For the sake of understanding, I have mapped my example to the participants in decorators’ pattern.
BaseComponent (IMobile)
Defines the interface for objects that can have responsibilities added to them dynamically. In our case, it is IMobile.
- public enum Brand
- {
- Samsung,
- Nokia,
- Apple,
- Xperia
- }
-
- public enum MobileType
- {
- SmartPhone,
- Normal
- }
-
- public abstract class IMobile
- {
- public abstract Brand Brand { get; }
- public abstract double Price { get; }
- public abstract MobileType MType { get; }
- }
ConcreteComponent (Apple, Samsung, Nokia) Defines an object to which additional responsibilities can be attached. Apple, Samsung are extensions of base class without any Offers.
Android
- public class Android : IMobile
- {
- private Brand brand;
- private double _price;
- private MobileType _mtype;
-
- public Android(Brand brand, double price, MobileType mtype)
- {
- this.brand = brand;
- _price = price;
- _mtype = mtype;
- }
-
- public override Brand Brand
- {
- get { return brand; }
- }
-
-
- public override double Price
- {
- get { return _price; }
-
- }
-
- public override MobileType MType
- {
- get { return _mtype; }
-
- }
- public override string ToString()
- {
-
- StringBuilder itemDesc = new StringBuilder();
- itemDesc.Append(Enum.GetName(typeof(Brand), brand));
- itemDesc.Append(" ");
- itemDesc.Append("||");
- itemDesc.Append(" ");
- itemDesc.Append(Price);
- itemDesc.Append(" ");
- itemDesc.Append("||");
- itemDesc.Append(" ");
- itemDesc.Append(Enum.GetName(typeof(MobileType), MType));
- return itemDesc.ToString();
- }
- }
iPhone.cs
- public class IPhone : IMobile
- {
- private Brand brand;
- private double _price;
- private MobileType _mtype;
-
- public IPhone(Brand brand, double price, MobileType mtype)
- {
- this.brand = brand;
- _price = price;
- _mtype = mtype;
- }
-
- public override Brand Brand
- {
- get { return brand; }
-
- }
-
- public override double Price
- {
- get { return _price; }
-
- }
-
- public override MobileType MType
- {
- get { return _mtype; }
-
- }
-
- public override string ToString()
- {
-
- StringBuilder itemDesc= new StringBuilder();
- itemDesc.Append(Enum.GetName(typeof(Brand),brand));
- itemDesc.Append(" ");
- itemDesc.Append("||");
- itemDesc.Append(" ");
- itemDesc.Append(Price);
- itemDesc.Append(" ");
- itemDesc.Append("||");
- itemDesc.Append(" ");
- itemDesc.Append(Enum.GetName(typeof(MobileType),MType));
- return itemDesc.ToString();
- }
- }
Decorator(Offers)
Maintains a reference to component object and defines an interface that conforms to component’s interface. Offer is a abstract decorator class - note that it encapsulates IMobile.
- public abstract class Offer
- {
- private readonly IMobile _mobile;
-
- protected Offer(IMobile mobile)
- {
- this._mobile = mobile;
- }
-
- public Brand Brand
- {
- get { return _mobile.Brand; }
- }
-
- public double Price
- {
- get { return _mobile.Price; }
- }
-
- public MobileType MType
- {
- get { return _mobile.MType; }
- }
- }
ConcreteDecorators (Christmas Offer, Student Offer): Adds responsibilities to the component e.g. the first concrete decorator which adds
ChristmasOffer functionality.
- public sealed class ChristmasOffer:Offer
- {
- public int DiscountPercentage { get; set; }
-
- public ChristmasOffer(IMobile mobile) : base(mobile){
-
- }
-
- public double CalculatedPrice
- {
- get
- {
- double price = base.Price;
- int percentage = 100 - DiscountPercentage;
- return Math.Round((price * percentage) / 100, 2);
- }
- }
-
- }
Client
- private static void Main(string[] args)
- {
- int phoneType;
-
- OfferIteratorImp offerCollection = new OfferIteratorImp(GetMeCollection.GetMeOfferList());
-
- Console.WriteLine("------------- Prepare Quotation for the mobile -------------");
-
- Console.WriteLine("Select the Mobile type");
-
- int i = 1;
- foreach (var mtype in Enum.GetNames(typeof (MobileType)))
- {
- Console.WriteLine(i + " : " + mtype);
- i++;
- }
-
- string userInput = Console.ReadLine();
-
- int.TryParse(userInput, out phoneType);
-
-
- var mType = phoneType == 1 ? MobileType.SmartPhone : MobileType.Normal;
-
- Console.WriteLine("Choose the brand out of shown ones?");
-
- Console.WriteLine("\n");
- Console.WriteLine("Brand || Price || Type ");
- Console.WriteLine("----------------------------");
-
-
- foreach (var mobile in GetMeCollection.GetMeListOfPhones())
- {
- if (mobile.MType == mType)
- {
- Console.WriteLine(mobile);
- }
- }
-
-
- Console.WriteLine("\n");
- Console.WriteLine("\nAre you a student?");
-
- int custType;
-
- int.TryParse(Console.ReadLine(), out custType);
- bool isStudent = custType == 1 ? true : false;
-
- if (isStudent)
- {
- Console.WriteLine("\n");
- Console.WriteLine("Student Prices");
- Console.WriteLine("\n");
- foreach (var listOffer in offerCollection)
- {
- if (listOffer.GetType() != typeof (StudentOffer)) continue;
- Console.WriteLine(listOffer);
- Console.WriteLine("\n");
- }
- }
-
- Console.ReadKey();
- }
As you can see, the Base Component (IMobile) defines the interface for objects that can be assigned new responsibilities dynamically, and the ConcreteComponent (IPhone) is simply an implementation of this interface. The Decorator (Offer) has a reference (composition) to a Component, and also conforms to the Component interface (IMobile). This is an important thing to remember, as the Decorator is essentially wrapping the Component. The ConcreteDecorator (ChristmasOffer and StudentOffer) adds extra responsibilities to the original Component.