Introduction
Let's begin with a conceptual diagram, the pictorial representation will help us to understand the flow.
Class DecoratorOne is injecting an IComponent interface. This allows you to inject any behaviour you're looking to add, for example, passing the object that you wish to decorate.
So DecoratorOne is acting as a wrapper around decorate. In the above diagram, you can see the Decorator class keeps the reference to the object being decorated. In this case, a component class.
Not only our DecoratorOne class IS-A IComponent but also it HAS-A IComponent. This is because the decorator class implements the same interface as the original Component class, so it now has a chance to intercept any method calls on the interface and inject some additional behaviour into those calls.
Real-life example
- Order a pizza: interface Ipizza
- Type of pizza (Farmhouse): class FarmHouse: Ipizza
- Add topping Mushrooms: class Mushroom: FarmHouse
- Add one more topping Sausage: class Sausage: Mushroom
In the end, we ended up in ordering a farmhouse pizza with mushroom & sausages. We kept injecting our new requirement and kept decorating old class to get us something additional.
So when you hear the word decorator, what you're really doing is designing a series of objects that can wrap around each other and inject behaviour as needed.
You can, in fact, define and use multiple decorator objects together so you can have a layered structure around your original object, much like an onion.
Each decorator class can define different behaviours, so you maintain that strong separation of concerns.
Since all of your decorator classes implement the same interface as your original object, you don't have to change any of your client code to add-in these various new behaviours.
Let's begin with code and implement the following design:
Let's add interface iMobile:
- public interface IMobile
- {
- double GetPrice();
- string GetModel();
- }
Let's add interface iAndroid:
- public interface IAndroid : IMobile
- {
- string GetAndroidOS();
- }
Now for our default implementation of Android class, let's return some strings & values through our functions.
- public class Android : IAndroid return
- { we
- public string GetAndroidOS()
- {
- return "New Android OS: ";
- }
-
- public string GetModel()
- {
- return " New Model: ";
- }
-
- public double GetPrice()
- {
- return 999;
- }
- }
At last, on our decorator class, let's add additional information to functions.
- public class OnePlus : IAndroid
- {
- #region Properties
- IAndroid Android;
- #endregion
-
- #region Constructor
- public OnePlus(IAndroid android)
- {
- this.Android = android;
- }
- #endregion
-
- #region overridden methods
-
- public string GetAndroidOS()
- {
- return Android.GetAndroidOS() + "Android 10";
- }
-
- public string GetModel()
- {
- return Android.GetModel() + "One Plus 8";
- }
-
- public double GetPrice()
- {
- return Android.GetPrice() + 54000;
- }
- #endregion
- }
Now the observer overridden methods are adding additional information to the injected class, hence an IS-A & HAS-A relationship.
Let's go ahead and call it through the program.
- class Program
- {
- static void Main(string[] args)
- {
- IAndroid android= new Android();
- OnePlus newPhone = new OnePlus(android);
- Console.WriteLine($"{newPhone.GetModel()}\n {newPhone.GetAndroidOS()} \n Price: {newPhone.GetPrice()}");
-
- }
- }
Run the app.
As you can see, it is a combination of both objects. One default implementation & another decorating default object.
Being able to layer objects together in this onion‑type structure and then intercept and modify method calls is a very powerful idea because it allows us to separate concerns and dynamically add new functionality when new needs come up. So, as you work with the decorator pattern, keep these ideas in mind. I hope you gained something out of this article and I wish you good luck.
Happy Coding!
Connect with me,