The Problem: Inheritance Explosion
Normally, we achieve runtime behavior modification using inheritance and polymorphism. At the time of object creation, we decide – based on some condition – whether to create an object of the base class or one of its derived classes. Since all objects share a common interface, we can operate on them uniformly, while the actual behavior depends on which specific subclass was instantiated at runtime.
Imagine you’re building a text editor.
You start with a simple Text
class.
class Text {
public:
virtual void render() {
cout << "Simple Text";
}
virtual ~Text() {}
};
Now, you want to support different formatting:
- Bold
- Italic
- Underlined
Normally, we achieve this as follows:
- We define a base class.
- We create derived classes for different variations.
- At runtime, we choose which subclass to instantiate based on some condition.
class BoldText : public Text {
public:
void render() override {
cout << "<b>Simple Text</b>";
}
};
class ItalicText : public Text {
public:
void render() override {
cout << "<i>Simple Text</i>";
}
};
class UnderlinedText : public Text {
public:
void render() override {
cout << "<u>Simple Text</u>";
}
};
Now based on it we can choose subclass at runtime (during object creation).
int main() {
// Let's say the user selects "Bold"
string style = "Bold";
Text* text;
if (style == "Bold") {
text = new BoldText();
} else if (style == "Italic") {
text = new ItalicText();
} else if (style == "Underline") {
text = new UnderlinedText();
} else {
text = new Text();
}
text->render(); // Output: <b>Simple Text</b>
delete text;
}
Pretty awesome right; But what if the user want Bold + Italic?
We would need another subclass:
class BoldItalicText : public Text {
public:
void render() override {
cout << "<b><i>Simple Text</i></b>";
}
};
Now in the main
code:
else if (style == "BoldItalic") {
text = new BoldItalicText();
}
If later we add Underline
, we would need:
BoldUnderlinedText
ItalicUnderlineText
BoldItalicUnderlinedText
If you add more (like color, font size, background), the number of subclasses grows exponentially.
This is called the inheritance explosion problem — too many rigid subclasses, hard to extend and maintain. With n formatting features
, you end up needing 2^n
subclasses to cover every combination. This technique would become impractical because we would have to create a separate subclass for every combination.
[ Text ]
|
+---------------+---------------+
| | |
[BoldText] [ItalicText] [UnderlineText]
|
more subclasses for combinations...
|
[BoldItalicText], [BoldUnderlineText], [ItalicUnderlineText]
|
[BoldItalicUnderlineText]
Why Inheritance Fails here
- Rigid: You have to decide all combinations ahead of time.
- Not flexible: You can’t easily enable/disable features at runtime without creating new classes.
- Hard to maintain: Adding just one new feature means updating many subclasses.
The Need
We need a way to:
- Add features to objects dynamically at runtime.
- Avoid creating endless subclasses.
- Keep the system flexible and extensible.
The Solution: Decorator Pattern
The Decorator Pattern is a structural design pattern used to add new behaviors or responsibilities to an object dynamically without affecting the behavior of other objects of the same class.
Instead of using subclassing to extend functionality, the Decorator Pattern uses composition and allows you to "wrap" an object with additional functionality.
The Decorator Pattern avoids subclass explosion by shifting from inheritance to composition.
Components of the Decorator Pattern
1 Interface / Abstract Class
Defines the common interface that both concrete objects and decorators must implement.
Example:
class Text {
public:
virtual void render() = 0;
virtual ~Text() {}
};
2 Concrete Component
The original object to which we want to add responsibilities dynamically. Implements the Component
interface.
Example:
class SimpleText : public Text {
public:
void render() override {
cout << "Hello World";
}
};
3 Decorator (Abstract Decorator Class)
- Maintains a reference to a
Component
object (usually via pointer). - Implements the
Component
interface, so it can stand in for real object. - Delegates work to the wrapped object, while allowing subclasses to add extra behavior.
Example:
class TextDecorator : public Text {
protected:
Text* text; // holds the wrapped component
public:
TextDecorator(Text* t) : text(t) {}
};
4 Concrete Decorators
- Extend the functionality of the object by wrapping it and adding new behavior before/after delegating to the wrapped component.
Example:
class Bold : public TextDecorator {
public:
Bold(Text* t) : TextDecorator(t) {}
void render() override {
cout << "<b>";
text->render(); // delegate
cout << "</b>";
}
};
class Italic : public TextDecorator {
public:
Italic(Text* t) : TextDecorator(t) {}
void render() override {
cout << "<i>";
text->render(); // delegate
cout << "</i>";
}
};
Standard UML Diagram
┌─────────────────────┐
│ Component │◄───────────────┐
│---------------------│ │
│ + operation() │ │
└─────────────────────┘ │
▲ │
┌───────────┴───────────┐ │
│ │ │
┌───────────────────┐ ┌───────────────────┐ │
│ ConcreteComponent │ │ Decorator │───┘
│-------------------│ │-------------------│
│ + operation() │ │ - component:Comp. │
└───────────────────┘ │ + operation() │
└───────────────────┘
▲
┌───────────┴───────────┐
│ │
┌─────────────────────┐ ┌─────────────────────┐
│ ConcreteDecoratorA │ │ ConcreteDecoratorB │
│---------------------│ │---------------------│
│ + addedBehavior() │ │ + addedBehavior() │
│ + operation() │ │ + operation() │
└─────────────────────┘ └─────────────────────┘
Example:
#include <iostream>
#include <string>
using namespace std;
// Component Interface: defines a common interface for Mario and all power-up decorators.
class Character {
public:
virtual string getAbilities() const = 0;
virtual ~Character() {} // Virtual destructor
};
// Concrete Component: Basic Mario character with no power-ups.
class Mario : public Character {
public:
string getAbilities() const override {
return "Mario";
}
};
// Abstract Decorator: CharacterDecorator "is-a" Charatcer and "has-a" Character.
class CharacterDecorator : public Character {
protected:
Character* character; // Wrapped component
public:
CharacterDecorator(Character* c){
this->character = c;
}
};
// Concrete Decorator: Height-Increasing Power-Up.
class HeightUp : public CharacterDecorator {
public:
HeightUp(Character* c) : CharacterDecorator(c) { }
string getAbilities() const override {
return character->getAbilities() + " with HeightUp";
}
};
// Concrete Decorator: Gun Shooting Power-Up.
class GunPowerUp : public CharacterDecorator {
public:
GunPowerUp(Character* c) : CharacterDecorator(c) { }
string getAbilities() const override {
return character->getAbilities() + " with Gun";
}
};
// Concrete Decorator: Star Power-Up (temporary ability).
class StarPowerUp : public CharacterDecorator {
public:
StarPowerUp(Character* c) : CharacterDecorator(c) { }
string getAbilities() const override {
return character->getAbilities() + " with Star Power (Limited Time)";
}
~StarPowerUp() {
cout << "Destroying StarPowerUp Decorator" << endl;
}
};
int main() {
// Create a basic Mario character.
Character* mario = new Mario();
cout << "Basic Character: " << mario->getAbilities() << endl;
// Decorate Mario with a HeightUp power-up.
mario = new HeightUp(mario);
cout << "After HeightUp: " << mario->getAbilities() << endl;
// Decorate Mario further with a GunPowerUp.
mario = new GunPowerUp(mario);
cout << "After GunPowerUp: " << mario->getAbilities() << endl;
// Finally, add a StarPowerUp decoration.
mario = new StarPowerUp(mario);
cout << "After StarPowerUp: " << mario->getAbilities() << endl;
delete mario;
return 0;
}
Leave a comment
Your email address will not be published. Required fields are marked *