Decorator Pattern: How to Extend Object Functionality Flexibly Without Inheritance
Decorator Pattern allows you to add new features to objects at runtime without modifying the original code or creating numerous subclasses. Discover how to apply this pattern effectively in real-world projects.
Table of Contents
No table of contents available for this article
1. What Is Decorator Pattern?
Decorator Pattern (also known as Wrapper) is one of the essential Structural Patterns in object-oriented programming. This pattern allows you to extend an object's functionality at runtime flexibly, without modifying the original code or creating a multitude of subclasses.
Key Characteristics:
Classification: Structural Pattern
Alias: Wrapper (because it "wraps" objects)
Purpose: Add new features to objects dynamically, as an alternative to complex inheritance
Usage Frequency: Medium - common in large frameworks and libraries
2. The Problem Decorator Pattern Solves
Real-World Scenario
Imagine you're developing a notification library for applications. Initially, the requirements are simple: just send email notifications.
You create a Notifier class with a send() method to send emails. Everything works fine until...
New requirements keep pouring in:
"Can we send SMS?"
"I want to receive notifications via Facebook!"
"What about Slack notifications?"
"I want to receive Email AND SMS AND Slack simultaneously!"
The Common Solution (And Its Problems)
Approach 1: Create multiple subclasses
Notifier
├── EmailNotifier
├── SMSNotifier
├── FacebookNotifier
├── SlackNotifier
├── EmailAndSMSNotifier
├── EmailAndFacebookNotifier
├── EmailAndSlackNotifier
├── SMSAndFacebookNotifier
├── ... (complexity explosion!)Problem: For each new combination, you need to create a new subclass. With 4 notification types, you could end up with 2⁴ = 16 subclasses! This makes code bulky, hard to maintain and violates the Open/Closed Principle.
Decorator Pattern to the Rescue
Instead of creating dozens of subclasses, Decorator Pattern allows you to "wrap" the original object with additional features like stacking layers of gift wrap.
The Idea:
Keep
EmailNotifieras the base classCreate separate decorators:
SMSDecorator,FacebookDecorator,SlackDecoratorWhen combining is needed, simply "wrap" the object sequentially
csharp
// Send Email + SMS + Slack
var notifier = new EmailNotifier();
notifier = new SMSDecorator(notifier);
notifier = new SlackDecorator(notifier);
notifier.send("Important message!");This way, you can freely combine features without creating new classes!
3. Decorator Pattern Architecture
Decorator Pattern consists of 4 main components:
Component (Interface/Abstract Class)
Defines the common interface for both concrete components and decorators. This is the "contract" that all participants must follow.
csharp
public interface ICar
{
ICar ManufactureCar();
}Concrete Component
The original class that needs to be extended. This is the actual object performing the main work.
csharp
public class BMWCar : ICar
{
private string CarName = "BMW";
public string CarBody { get; set; }
public string CarDoor { get; set; }
public string CarWheels { get; set; }
public string CarGlass { get; set; }
public string Engine { get; set; }
public ICar ManufactureCar()
{
CarBody = "Carbon fiber material";
CarDoor = "4 car doors";
CarWheels = "4 MRF wheels";
CarGlass = "6 car glasses";
return this;
}
}Decorator (Abstract Class)
An intermediate class containing a reference to the Component and implementing the Component interface. This is the foundation for concrete decorators.
csharp
public abstract class CarDecorator : ICar
{
protected ICar car;
public CarDecorator(ICar car)
{
this.car = car;
}
public virtual ICar ManufactureCar()
{
return car.ManufactureCar();
}
}Concrete Decorator
Classes extending from Decorator, adding specific functionality to the Component.
csharp
public class DieselCarDecorator : CarDecorator
{
public DieselCarDecorator(ICar car) : base(car) { }
public override ICar ManufactureCar()
{
car.ManufactureCar();
AddEngine(car);
return car;
}
public void AddEngine(ICar car)
{
if (car is BMWCar bmwCar)
{
bmwCar.Engine = "Diesel Engine";
Console.WriteLine($"Added Diesel Engine to: {car}");
}
}
}
```
**Flow Diagram:**
```
Client → Decorator 3 → Decorator 2 → Decorator 1 → Concrete Component
← Result ← Result ← Result ← Result ←4. Advantages and Disadvantages
✅ Advantages
1. Flexible extension without inheritance
You can add new features to objects without creating subclasses, adhering to the Open/Closed Principle.
2. Free feature combination
You can "stack" multiple different decorators to create rich feature combinations.
3. Add/remove features at runtime
Change object behavior while the program is running, without recompiling code.
4. Single Responsibility Principle
Each decorator is only responsible for adding one specific feature, making it easy to maintain and test.
Real-world example: In Java I/O, you can combine:
java
InputStream stream = new BufferedInputStream(
new GZipInputStream(
new FileInputStream("data.gz")
)
);❌ Disadvantages
1. Difficult to remove specific decorator
Once you've "wrapped" multiple layers, removing a decorator in the middle of the stack is very complex.
2. Order-dependent stack
Some decorators may behave differently depending on their position in the chain, making debugging difficult.
3. Increased number of objects
Each decorator is a separate object; with many decorators, you can create many small objects, affecting performance.
4. Difficult to debug
When errors occur, tracing through multiple decorator layers can be confusing.
5. When Should You Use Decorator Pattern?
✔️ Use when:
1. Need to add dynamic features to objects
When you want to add/remove object features at runtime without affecting the original code.
2. Cannot use inheritance
Class is marked
final(sealed)Inheritance would create too many subclasses
Need flexible feature combinations
3. Extending third-party library classes
When you cannot modify the original source code but still need to add features.
❌ Don't use when:
Only need simple extension → Use direct inheritance
Decorator order significantly affects logic → Reconsider design
Need high performance, avoid creating many objects → Consider other patterns
6. Real-World Example in C#
Problem: Car Manufacturing System
Suppose you're building a BMW car production management system. Initially, cars are manufactured with basic frame, doors, wheels, and glass. Then, depending on orders, cars will have Diesel or Petrol engines installed.
Instead of creating BMWCarWithDiesel, BMWCarWithPetrol, we use Decorator Pattern to "install" engines flexibly.
Complete Code
Step 1: Create Component Interface
csharp
public interface ICar
{
ICar ManufactureCar();
}Step 2: Create Concrete Component
csharp
public class BMWCar : ICar
{
private string CarName = "BMW";
public string CarBody { get; set; }
public string CarDoor { get; set; }
public string CarWheels { get; set; }
public string CarGlass { get; set; }
public string Engine { get; set; }
public override string ToString()
{
return $"BMWCar [Name={CarName}, Body={CarBody}, Door={CarDoor}, " +
$"Wheels={CarWheels}, Glass={CarGlass}, Engine={Engine}]";
}
public ICar ManufactureCar()
{
CarBody = "Carbon fiber material";
CarDoor = "4 doors";
CarWheels = "4 MRF wheels";
CarGlass = "6 glasses";
return this;
}
}Step 3: Create Base Decorator
csharp
public abstract class CarDecorator : ICar
{
protected ICar car;
public CarDecorator(ICar car)
{
this.car = car;
}
public virtual ICar ManufactureCar()
{
return car.ManufactureCar();
}
}Step 4: Create Concrete Decorators
csharp
// Decorator adding Diesel engine
public class DieselCarDecorator : CarDecorator
{
public DieselCarDecorator(ICar car) : base(car) { }
public override ICar ManufactureCar()
{
car.ManufactureCar();
AddEngine(car);
return car;
}
public void AddEngine(ICar car)
{
if (car is BMWCar bmwCar)
{
bmwCar.Engine = "Diesel Engine";
Console.WriteLine("✓ Diesel Engine installed in car: " + car);
}
}
}
// Decorator adding Petrol engine
public class PetrolCarDecorator : CarDecorator
{
public PetrolCarDecorator(ICar car) : base(car) { }
public override ICar ManufactureCar()
{
car.ManufactureCar();
AddEngine(car);
return car;
}
public void AddEngine(ICar car)
{
if (car is BMWCar bmwCar)
{
bmwCar.Engine = "Petrol Engine";
Console.WriteLine("✓ Petrol Engine installed in car: " + car);
}
}
}Step 5: Client Code
csharp
class Client
{
static void Main(string[] args)
{
// Basic car without engine
ICar bmwCar1 = new BMWCar();
bmwCar1.ManufactureCar();
Console.WriteLine("Initial car: " + bmwCar1 + "\n");
// Add Diesel engine
DieselCarDecorator carWithDiesel = new DieselCarDecorator(bmwCar1);
carWithDiesel.ManufactureCar();
Console.WriteLine();
// Second car with Petrol engine
ICar bmwCar2 = new BMWCar();
PetrolCarDecorator carWithPetrol = new PetrolCarDecorator(bmwCar2);
carWithPetrol.ManufactureCar();
Console.ReadKey();
}
}
```
**Output:**
```
Initial car: BMWCar [Name=BMW, Body=Carbon fiber material, Door=4 doors, Wheels=4 MRF wheels, Glass=6 glasses, Engine=]
✓ Diesel Engine installed in car: BMWCar [Name=BMW, Body=Carbon fiber material, Door=4 doors, Wheels=4 MRF wheels, Glass=6 glasses, Engine=Diesel Engine]
✓ Petrol Engine installed in car: BMWCar [Name=BMW, Body=Carbon fiber material, Door=4 doors, Wheels=4 MRF wheels, Glass=6 glasses, Engine=Petrol Engine]Further Extensions
You can easily add other decorators like:
TurboDecorator- add turbochargerSportPackageDecorator- sport packageLuxuryInteriorDecorator- luxury interior
And combine them freely:
csharp
ICar superCar = new BMWCar();
superCar = new DieselCarDecorator(superCar);
superCar = new TurboDecorator(superCar);
superCar = new SportPackageDecorator(superCar);7. Conclusion
Decorator Pattern is a powerful tool that helps you extend object functionality flexibly without bloating your codebase. This pattern is especially useful when:
You need to combine multiple dynamic features
Don't want to create numerous subclasses
Working with libraries/frameworks that can't be modified
However, consider carefully before applying, as it can increase debugging complexity and has issues with decorator ordering.
Advice: Start with simple design, only apply Decorator when truly necessary. Don't over-engineer!
If you found this article helpful, explore more in the Design Patterns Series to enhance your programming skills!
References
[1] Refactoring.Guru - https://refactoring.guru/design-patterns/decorator
[2] Design Patterns for Dummies, Steve Holzner, PhD
[3] Head First Design Patterns, Eric Freeman
[4] Gang of Four Design Patterns 4.0
[5] Dive into Design Patterns