Ever felt stuck trying to add features to an existing object without messing with its core code? I’ve been there. You want to extend functionality, not rewrite the whole thing. That’s where the Decorator Pattern comes in. It's like adding layers to a cake, each layer enhancing the original without changing it. It's a structural design pattern that allows behavior to be added to an individual object, either statically or dynamically, without affecting the behavior of other objects from the same class. Let's get into it.
The Decorator Pattern allows you to add responsibilities to objects dynamically. Think of it as wrapping an object with one or more decorators that add new functionality. This is a flexible alternative to subclassing because it lets you add responsibilities to individual objects without affecting other objects of the same class.
Use the Decorator Pattern when:
Let's look at a Java example. Suppose we have a simple Coffee interface and concrete implementations like SimpleCoffee. We can add decorators to enhance the coffee with milk, sugar, or chocolate.
java// Component Interface
interface Coffee {
String getDescription();
double getCost();
}
// Concrete Component
class SimpleCoffee implements Coffee {
@Override
public String getDescription() {
return "Simple Coffee";
}
@Override
public double getCost() {
return 1.0;
}
}
// Decorator
abstract class CoffeeDecorator implements Coffee {
protected Coffee decoratedCoffee;
public CoffeeDecorator(Coffee decoratedCoffee) {
this.decoratedCoffee = decoratedCoffee;
}
@Override
public String getDescription() {
return decoratedCoffee.getDescription();
}
@Override
public double getCost() {
return decoratedCoffee.getCost();
}
}
// Concrete Decorators
class Milk extends CoffeeDecorator {
public Milk(Coffee decoratedCoffee) {
super(decoratedCoffee);
}
@Override
public String getDescription() {
return decoratedCoffee.getDescription() + ", with Milk";
}
@Override
public double getCost() {
return decoratedCoffee.getCost() + 0.5;
}
}
class Sugar extends CoffeeDecorator {
public Sugar(Coffee decoratedCoffee) {
super(decoratedCoffee);
}
@Override
public String getDescription() {
return decoratedCoffee.getDescription() + ", with Sugar";
}
@Override
public double getCost() {
return decoratedCoffee.getCost() + 0.2;
}
}
// Client Code
public class DecoratorPatternExample {
public static void main(String[] args) {
Coffee coffee = new SimpleCoffee();
System.out.println("Cost: " + coffee.getCost() + ", Description: " + coffee.getDescription());
coffee = new Milk(coffee);
System.out.println("Cost: " + coffee.getCost() + ", Description: " + coffee.getDescription());
coffee = new Sugar(coffee);
System.out.println("Cost: " + coffee.getCost() + ", Description: " + coffee.getDescription());
}
}
In this example:
In Java’s java.io package, the Decorator Pattern is used extensively with streams. For example, you can wrap a FileInputStream with a BufferedInputStream to add buffering, and then wrap it with a DataInputStream to read primitive data types.
javaFileInputStream fileInputStream = new FileInputStream("test.txt");
BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
DataInputStream dataInputStream = new DataInputStream(bufferedInputStream);
int data = dataInputStream.readInt();
Here, BufferedInputStream and DataInputStream are decorators that add functionality to the base FileInputStream.
Here’s a React Flow UML diagram illustrating the structure of the Decorator Pattern:
Q: When should I use the Decorator Pattern instead of inheritance?
Use the Decorator Pattern when you need to add responsibilities to individual objects dynamically and avoid creating a large number of subclasses. Inheritance is static and applies to all instances of a class.
Q: Can I apply multiple decorators to an object?
Yes, you can apply multiple decorators to an object to add multiple responsibilities. The order in which you apply the decorators can affect the final behavior.
Q: Does the Decorator Pattern violate the Open/Closed Principle?
No, the Decorator Pattern adheres to the Open/Closed Principle because it allows you to add new functionality without modifying the existing code.
The Decorator Pattern is a valuable tool for enhancing low-level design flexibility. By adding responsibilities dynamically, you can create more maintainable and extensible code. It's all about layering functionality without altering the core. For hands-on practice with the Decorator Pattern and other design patterns, explore problems at Coudo AI, where practical exercises and AI-driven feedback can enhance your learning experience. Check out our related problems such as:
Next time you're thinking about how to extend an object's behavior, remember the Decorator Pattern—it might just be the perfect fit. \n\n