Table of Contents
What is the Decorator Design Pattern? 🤔
The Decorator Design Pattern is a structural design pattern that allows us to add new functionality to an existing object without altering its structure. This is achieved by wrapping the original object with an object of a decorator class which provides the additional functionality.
It allows us to dynamically add new behaviours or responsibilities to an object at runtime without modifying its core implementation. By embracing the Decorator Pattern, developers can create highly adaptable and maintainable code, adhering to the Open/Closed Principle. 🔓
Why the Decorator Pattern is considered a Structural Design Pattern?
To understand why it falls under the Structural Design Pattern category, let’s first define what a Structural Design Pattern is:
Structural Design Patterns are concerned with how classes and objects are composed to form larger structures. They help in understanding and facilitating the design by identifying ways to realize relationships between entities. In other words, these patterns focus on the composition of classes and objects to create complex structures that work together to achieve desired functionalities.
The basic definition of a Structural Design Pattern is:
“A Structural Design Pattern is a way of composing classes and objects to form larger structures while keeping these structures flexible and efficient.”
Now, let’s understand why the Decorator Pattern is considered a Structural Design Pattern:
The Decorator Pattern allows you to dynamically attach additional responsibilities or behaviours to an object by wrapping it with one or more decorator objects. This is done by creating a chain of decorator objects that encapsulate the original object, where each decorator object extends the functionality of the previous one.
In essence, the Decorator Pattern is all about creating a flexible and extensible structure of objects by composing them together. It achieves this by:
- Object Composition: The Decorator Pattern uses object composition to add or remove responsibilities dynamically. The decorator objects wrap the original object, creating a chain of objects that work together to provide the desired functionality.
- Flexibility and Extensibility: By using composition instead of inheritance, the Decorator Pattern allows for more flexibility and extensibility. New decorators can be added or removed without modifying the original object or existing decorators, following the Open/Closed Principle.
- Structural Changes at Runtime: The Decorator Pattern enables structural changes to objects at runtime by dynamically adding or removing decorators, effectively changing the object’s behaviour and responsibilities.
These characteristics align with the fundamental goal of Structural Design Patterns, which is to provide ways to compose objects into larger structures while maintaining flexibility and efficiency.
In summary, the Decorator Design Pattern is considered a Structural Design Pattern because it focuses on the composition and organization of objects to create larger, more complex structures. It provides a flexible and extensible way to add or remove responsibilities from objects by wrapping them with decorator objects, allowing for dynamic structural changes at runtime.
Explain Decorator Design Pattern to a 5-year-old kid 🧒🎈📚:
Image from https://limewire.com/
Explaining the Decorator Design Pattern to a 5-year-old can be a bit challenging, but let’s try to make it as simple and relatable as possible. 🤗
Imagine you have a toy robot, and it can walk and make sounds. 🚶♂️ That’s the basic functionality of the robot, right? But what if you want to add more features to your robot, like making it dance or shoot little foam balls? 💃🎉
Instead of creating a completely new robot with all those additional features, you can use “decorators” to enhance your existing robot. 🎁
Decorators are like special accessories or costumes that you can put on your robot to give it new abilities. For example, you could put on a “dancing decorator” costume that makes your robot dance whenever you press a button. 💃
Or, you could add a “shooter decorator” accessory that allows your robot to shoot foam balls. 🔫
The best part is that you can mix and match these decorators however you want! 🤖👯♂️ You can put on the dancing decorator and the shooter decorator at the same time, making your robot both dance and shoot foam balls.
And if you ever want to remove a decorator, you can simply take it off without affecting the robot’s original walking and sound-making abilities.
👍
Just like how you can customize your toy robot by adding or removing decorators, software developers use the Decorator Design Pattern to add or remove features from objects in their programs without modifying the original code. 💻
This way, they can create flexible and extensible software that can easily adapt to new requirements or changes, just like how you can keep customizing your toy robot with different decorators. 🙌
The Building Blocks: 🧱
- Component Interface: The foundation of the Decorator Pattern lies in the
Component
interface (or abstract class). This interface defines the common methods that both concrete components and decorators must implement. It serves as a contract, ensuring that all objects implementing this interface can be treated uniformly. 🤝
public interface Robot {
void walk();
void makeSound();
}
Here The Robot
interface defines the common methods that both the concrete robot and the decorators must implement.
- Concrete Component: The
ConcreteComponent
class is a direct implementation of theComponent
interface. It represents the core functionality that we want to extend or enhance using decorators. This class serves as the base upon which we’ll build our decorated objects. 💎
public class BasicRobot implements Robot {
@Override
public void walk() {
System.out.println("The robot is walking.");
}
@Override
public void makeSound() {
System.out.println("The robot is making a sound.");
}
}
The BasicRobot
class is a concrete implementation of the Robot
interface, representing the core functionality of walking and making sounds.
- Decorator Base Class: The
Decorator
class is an abstract class (or interface) that implements theComponent
interface. 🎭 It holds a reference to aComponent
object, which can be either a concrete component or another decorator. This reference allows the decorator to delegate method calls to the wrapped component. TheDecorator
class also provides a default implementation for the methods defined in theComponent
interface.
public abstract class RobotDecorator implements Robot {
protected Robot robot;
public RobotDecorator(Robot robot) {
this.robot = robot;
}
@Override
public void walk() {
robot.walk();
}
@Override
public void makeSound() {
robot.makeSound();
}
}
The RobotDecorator
class is an abstract class that implements the Robot
interface and holds a reference to a Robot
object, which can be either a concrete robot or another decorator. It delegates the walk()
and makeSound()
methods to the wrapped Robot
object.
The Decorator
class is marked as abstract
because we typically don’t want to instantiate it directly. Instead, we create concrete subclasses that extend this class and provide the desired additional behaviour. 🚫
- Concrete Decorators: Concrete decorators are subclasses of the
Decorator
class. They implement the specific behaviour that we want to add to the component. Each concrete decorator overrides the methods defined in theComponent
interface, calling the superclass’s method (super.operation()
) and then performing additional operations before or after the superclass’s method. 💫
public class DancingDecorator extends RobotDecorator {
public DancingDecorator(Robot robot) {
super(robot);
}
@Override
public void walk() {
super.walk();
System.out.println("The robot is dancing while walking!");
}
public void dance() {
System.out.println("The robot is dancing!");
}
}
public class ShooterDecorator extends RobotDecorator {
public ShooterDecorator(Robot robot) {
super(robot);
}
@Override
public void makeSound() {
super.makeSound();
System.out.println("The robot is shooting foam balls!");
}
public void shoot() {
System.out.println("Pew! Pew! The robot is shooting foam balls!");
}
}
The DancingDecorator
and ShooterDecorator
classes are concrete decorators that extend the RobotDecorator
class. They override the walk()
and makeSound()
methods to add their behaviour before or after calling the superclass’s method. They also provide additional methods dance()
and shoot()
to add new functionality to the robot.
The super(component)
call in the constructor is crucial. 🔑 It passes the Component
object that we want to decorate to the superclass constructor (Decorator
). This allows the decorator to hold a reference to the component it is decorating, enabling the decorator to call the component’s methods and add its own behavior before or after those method calls.
- Usage and Power of Composition: 🧮 The true power of the Decorator Pattern lies in its ability to compose objects dynamically at runtime. By combining multiple decorators with a concrete component, we can create complex objects with rich functionality. 🌈
public class RobotDemo {
public static void main(String[] args) {
Robot basicRobot = new BasicRobot();
basicRobot.walk();
basicRobot.makeSound();
System.out.println();
Robot dancingRobot = new DancingDecorator(basicRobot);
dancingRobot.walk();
dancingRobot.makeSound();
((DancingDecorator) dancingRobot).dance();
System.out.println();
Robot shootingRobot = new ShooterDecorator(basicRobot);
shootingRobot.walk();
shootingRobot.makeSound();
((ShooterDecorator) shootingRobot).shoot();
System.out.println();
Robot dancingShootingRobot = new DancingDecorator(new ShooterDecorator(basicRobot));
dancingShootingRobot.walk();
dancingShootingRobot.makeSound();
((DancingDecorator) dancingShootingRobot).dance();
((ShooterDecorator) dancingShootingRobot).shoot();
}
}
In the main
method, we create instances of the BasicRobot
, DancingDecorator
, ShooterDecorator
, and a combination of both decorators. We demonstrate how the decorators add new behaviours to the robot without modifying its core implementation.
Real-World Scenarios: 🌍
The Decorator Pattern finds applications in various domains, enabling developers to enhance the functionality of objects in a modular and extensible manner. Here are a few real-world scenarios where the Decorator Pattern shines:
- GUI Components: In graphical user interface (GUI) libraries, decorators can be used to add visual or behavioural features to GUI components like windows, buttons, menus, and more. For example, you could have a
BorderDecorator
that adds a border to a window, aScrollDecorator
that adds scrolling behavior to a text area, or aToolTipDecorator
that adds tooltip functionality to a button. 🖥️ - Logging and Instrumentation: Decorators can be employed to add logging or instrumentation capabilities to existing classes without modifying their code. For instance, you could have a
LoggingDecorator
that logs method calls and parameters, or aPerformanceDecorator
that measures the execution time of method invocations. 🔍 - Data Compression and Encryption: The Decorator Pattern can be used to add compression or encryption functionality to data streams or network communication. For example, you could have a
CompressionDecorator
that compresses data before sending it over the network, or anEncryptionDecorator
that encrypts sensitive data before storing it. 🔒 - Caching: Decorators can be utilized to add caching mechanisms to existing classes. You could have a
CachingDecorator
that caches the results of expensive computations or database queries, improving performance by returning cached results whenever possible. ⚡ - E-commerce Pricing: In an e-commerce application, decorators can be used to dynamically add or alter pricing features to a product. For example, you could have a
TaxDecorator
that adds tax to the base price of a product, or aDiscountDecorator
that applies a discount to the price. 💰
Simple Example
Let’s start with a straightforward example everyone can understand – customizing a coffee order!
Java Code:
// The Component
public interface Coffee {
String getDescription();
double cost();
}
// Concrete Component
public class BasicCoffee implements Coffee {
public String getDescription() {
return "Basic Coffee";
}
public double cost() {
return 1.00;
}
}
// Decorator
public abstract class CoffeeDecorator implements Coffee {
protected Coffee decoratedCoffee;
public CoffeeDecorator(Coffee coffee) {
this.decoratedCoffee = coffee;
}
public String getDescription() {
return decoratedCoffee.getDescription();
}
public double cost() {
return decoratedCoffee.cost();
}
}
// Concrete Decorators
public class WithMilk extends CoffeeDecorator {
public WithMilk(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return decoratedCoffee.getDescription() + ", with milk";
}
@Override
public double cost() {
return decoratedCoffee.cost() + 0.5;
}
}
public class WithSugar extends CoffeeDecorator {
public WithSugar(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return decoratedCoffee.getDescription() + ", with sugar";
}
@Override
public double cost() {
return decoratedCoffee.cost() + 0.2;
}
}
Using the Decorators:
public class CoffeeShop {
public static void main(String[] args) {
Coffee myOrder = new BasicCoffee();
System.out.println(myOrder.getDescription()
+ " $" + myOrder.cost());
myOrder = new WithMilk(myOrder);
System.out.println(myOrder.getDescription()
+ " $" + myOrder.cost());
myOrder = new WithSugar(myOrder);
System.out.println(myOrder.getDescription()
+ " $" + myOrder.cost());
}
}
Output:
Basic Coffee $1.0
Basic Coffee, with milk $1.5
Basic Coffee, with milk, with sugar $1.7
In this example, we have a BasicCoffee
object to which we dynamically add “milk” and “sugar” enhancements. Each addition is implemented as a CoffeeDecorator
that wraps around the BasicCoffee
.
Complex Real-World Example
Now, let’s look at a more complex example, say, in a GUI application where you might want to add features like borders, scrolling, or colour changes to certain elements without changing the original classes of those elements.
Java Code:
// The GUI component we want to decorate
public interface GUIComponent {
void draw();
}
// A concrete component
public class Window implements GUIComponent {
public void draw() {
// Draw the window
}
}
// Decorator base class
public abstract class ComponentDecorator implements GUIComponent {
protected GUIComponent component;
public ComponentDecorator(GUIComponent component) {
this.component = component;
}
public void draw() {
component.draw(); // Delegating the task
}
}
// Concrete Decorators
public class BorderDecorator extends ComponentDecorator {
public BorderDecorator(GUIComponent component) {
super(component);
}
@Override
public void draw() {
super.draw();
drawBorder();
}
private void drawBorder() {
// Draw the border around the component
}
}
public class ScrollDecorator extends ComponentDecorator {
public ScrollDecorator(GUIComponent component) {
super(component);
}
@Override
public void draw() {
super.draw();
addScroll();
}
private void addScroll() {
// Add scroll functionality
}
}
Using the GUI Decorators:
public class GUI {
public static void main(String[] args) {
GUIComponent window = new Window();
window.draw(); // Draw basic window
window = new BorderDecorator(window);
window.draw(); // Draw window with border
window = new ScrollDecorator(window);
window.draw(); // Draw window with border and scroll
}
}
Conclusion
The Decorator pattern offers a robust alternative to subclassing for extending behaviour.