What is the Decorator Design Pattern?
The Decorator design pattern is a structural pattern that lets you add new behavior to a class by “wrapping” it inside another class. This pattern is sometimes called the Wrapper Design Pattern.
You may use this pattern in Domain-Driven Design, where something like an Order class may have a different meaning to the different departments of a company – and require different properties.
Sample code description
This code will be for a pizza restaurant.
People can order a pizza to eat in the dining room, for picking up, or for delivery. For each of those different types of orders, the object needs to have different properties. We’ll use the Decorator pattern to add the additional required properties.
Non-pattern version
In the non-pattern version of the code, we have one PizzaOrder class that contains all the properties needed for each order type.
This works, but you end up with properties and functions that aren’t used. Also, it’s possible for a pizza order to be setup for delivery, pickup, and dining in – which doesn’t make sense.
namespace DesignPatternsCSharpNet6.Decorator.NonPattern; public class PizzaOrder { public decimal Price { get; private set; } public List<string> Ingredients { get; } = new List<string> { "Dough", "Tomato sauce", "Cheese" }; // Properties for delivery orders public decimal DeliveryFee { get; set; } public decimal TotalCharge => Price + DeliveryFee; public DateTime DeliveryDateTime { get; set; } public string? DeliveryDriverName { get; set; } // Properties for pick up orders public string? CustomerName { get; set; } public DateTime PickupDateTime { get; set; } // Properties for dining room orders public string? WaitStaffName { get; set; } public int TableNumber { get; set; } public PizzaOrder() { Price = 10.00M; } public void AddIngredient(string ingredientName, decimal price) { Ingredients.Add(ingredientName); Price += price; } }
Pattern version
For the pattern version, we’ll create this PizzaOrder class that holds the basic information about the pizza, but no properties for the delivery, pickup, or dining in information. Those properties will be available in the appropriate decorator classes.
namespace DesignPatternsCSharpNet6.Decorator.Pattern; public class PizzaOrder { public decimal Price { get; private set; } public List<string> Ingredients { get; } = new List<string> { "Dough", "Tomato sauce", "Cheese" }; public PizzaOrder() { Price = 10.00M; } public void AddIngredient(string ingredientName, decimal price) { Ingredients.Add(ingredientName); Price += price; } }
In each of the decorator class constructors, we pass in the PizzaOrder object, along with the additional parameters needed for the additional decorator class properties.
In PizzaOrderForDelivery, we add the DeliveryDataTime, DeliveryFee, and the DeliveryDriverName. This class also has the expression-bodied property TotalCharge, which combines the PizzaOrder.Price with its DeliveryFee.
namespace DesignPatternsCSharpNet6.Decorator.Pattern; public class PizzaOrderForDelivery { public PizzaOrder Pizza { get; } public DateTime DeliveryDateTime { get; } public decimal DeliveryFee { get; } public decimal TotalCharge => Pizza.Price + DeliveryFee; public string DeliveryDriverName { get; } public PizzaOrderForDelivery(PizzaOrder pizza, DateTime orderDateTime, decimal deliveryFee, string deliveryDriverName) { Pizza = pizza; DeliveryDateTime = orderDateTime.AddMinutes(30); DeliveryFee = deliveryFee; DeliveryDriverName = deliveryDriverName; } }
For PizzaOrderForPickup, we pass in the CustomerName and PickupDateTime values.
namespace DesignPatternsCSharpNet6.Decorator.Pattern; public class PizzaOrderForPickup { public PizzaOrder Pizza { get; } public string CustomerName { get; } public DateTime PickupDateTime { get; } public PizzaOrderForPickup(PizzaOrder pizza, string customerName, DateTime pickupDateTime) { Pizza = pizza; CustomerName = customerName; PickupDateTime = pickupDateTime; } }
For PizzaOrderForTable, we pass in the name of the waiter/waitress and the number of the table where the customer is sitting.
namespace DesignPatternsCSharpNet6.Decorator.Pattern; public class PizzaOrderForTable { public PizzaOrder Pizza { get; } public string WaitStaffName { get; set; } public int TableNumber { get; } public PizzaOrderForTable(PizzaOrder pizza, string waitStaffName, int tableNumber) { Pizza = pizza; WaitStaffName = waitStaffName; TableNumber = tableNumber; } }
Now, in our program that manages the pizza restaurant, we have a PizzaOrder object that can be shared by the different parts of the business.
The chefs can use PizzaOrder to make the pizza. That object has all the information they need. We can wrap PizzaOrder with the different decorators for the screens used by the people working at the counter (PizzaOrderForPickup), the waiters and waitresses (PizzaOrderForTable), and the delivery drivers (PizzaOrderForDelivery).
Summary
We could have done this with inheritance, instead of decorator classes.
In our example, we’ll probably never have a single order that is “for pickup” and “for delivery”. However, in a business application using inheritance, you may encounter situations where a class would need to inherit from multiple classes – which is not possible in C#. That’s where the decorator design pattern may be useful.
Also, in domain-driven design, decorators are a way to expose different properties to different users of your application.
Let’s say you’re writing a program that has an Employee object.
The Payroll department might have a PayrollEmployee decorator class that has a Salary property. However, you probably want to hide salary information in the “office phone directory” part of the program. That may only need Name, Department, and PhoneNumber properties. By having a PhoneDirectoryEmployee decorator class used in that part of the program, you can feel safer that salary data won’t be exposed in the phone directory.
The Decorator design pattern is similar to the Adapter design pattern. However, the Decorator pattern is generally used to make a class/object look differently in different parts of the same program. The Adapter pattern is more for convert an object into a different object, so it can be sent to a separate program.