Java
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented Java
-
Section 4: Collections Framework
-
Section 5: Exception Handling
-
Section 6: Generics
-
Section 7: Functional Java
-
Section 8: Concurrency
-
Section 9: I/O and NIO
-
Section 10: JVM Internals
-
Section 11: Modern Java Features
-
Section 12: Build Tools and Project Structure
-
Section 13: Testing
-
Section 14: Databases and Persistence
-
Section 15: Networking
-
Section 16: Design and Best Practices
-
Section 17: Reflection and Annotations
-
Section 18: Logging and Diagnostics
-
Section 19: Date, Time, and Internationalization
-
Section 20: Java Platform Module System
-
Section 21: Security in Java
-
Section 22: Advanced Collections and Data Structures
-
Section 23: More Concurrency Patterns
-
Section 24: Compression, Files, and System Integration
-
Section 25: GUI Programming
-
Section 26: Practical Projects
-
Section 27: Data Structures and Algorithms
-
Section 28: Interview and Algorithm Practice
-
Section 29: JSON and Data Interchange
-
Section 30: More Concurrency Utilities
-
Section 31: More Collections and Streams Practice
-
Section 32: More File and System Programming
-
Section 33: Standard Library Deep Dive
-
Section 34: More Practice and Drills
-
Section 35: More Testing and Quality
-
Section 36: More Design Patterns and Architecture
-
Section 37: Career and Ecosystem
-
Section 38: More OOP and Architecture Practice
-
Section 39: More Enterprise Concepts
-
Section 40: Advanced JavaFX
-
Section 41: More Interview Practice
152: The Decorator Pattern
You've probably encountered situations where you want to add functionality to an object, but you don't want to create a massive inheritance tree to do it. If you have a base class and ten different optional features, creating a subclass for every possible combination of those features is a nightmare. That's where the Decorator pattern comes in. It lets us "wrap" an object in another object that adds behavior, and we can stack these wrappers as deep as we want.
Avoiding the Subclass Explosion
Let's imagine we're building a system for a coffee shop. We have a basic coffee, but customers can add milk, sugar, whip cream, or caramel. If we used standard inheritance, we'd end up with classes like CoffeeWithMilk, CoffeeWithMilkAndSugar, and CoffeeWithMilkSugarAndWhip. It's a mess. Instead, we'll define a common interface that both the base coffee and the "decorators" implement.
public interface Beverage {
String getDescription();
double getCost();
}
Now, we create our simplest possible version of a beverage. This is the "concrete component" that we'll be decorating later.
public class SimpleCoffee implements Beverage {
@Override
public String getDescription() {
return "Simple Coffee";
}
@Override
public double getCost() {
return 2.00;
}
}
Wrapping Behavior with Decorators
To make the decorator work, we need a base decorator class. This class is a Beverage, but it also has a Beverage. This is the secret sauce: the decorator implements the interface it's decorating, which allows it to be passed around as if it were the original object.
public abstract class BeverageDecorator implements Beverage {
protected Beverage decoratedBeverage;
public BeverageDecorator(Beverage beverage) {
this.decoratedBeverage = beverage;
}
public String getDescription() {
return decoratedBeverage.getDescription();
}
public double getCost() {
return decoratedBeverage.getCost();
}
}
Now we can create specific additions, like milk. The MilkDecorator overrides the methods to add its own specific cost and description to whatever is already there.
public class MilkDecorator extends BeverageDecorator {
public MilkDecorator(Beverage beverage) {
super(beverage);
}
@Override
public String getDescription() {
return decoratedBeverage.getDescription() + ", Milk";
}
@Override
public double getCost() {
return decoratedBeverage.getCost() + 0.50;
}
}
Where I accidentally gave away free coffee
When I first implemented this in a project a few years back, I made a classic rookie mistake. I forgot that the decorator's job is to augment the existing value, not replace it. I wrote my SugarDecorator like this:
// DON'T DO THIS
public class SugarDecorator extends BeverageDecorator {
public SugarDecorator(Beverage beverage) { super(beverage); }
@Override
public double getCost() {
return 0.20; // I forgot to add decoratedBeverage.getCost()!
}
}
I spent twenty minutes wondering why my "Coffee with Milk and Sugar" only cost 20 cents. I had completely wiped out the cost of the coffee and the milk by returning only the price of the sugar. Always remember: a decorator should almost always call the wrapped object's method and then add its own logic to the result.
Stacking the Flavors
The real beauty of this pattern is how we use it at runtime. We don't need a CoffeeWithMilkAndSugar class because we can just wrap the objects in the order we want.
public class CoffeeShop {
public static void main(String[] args) {
// Start with a simple coffee
Beverage myCoffee = new SimpleCoffee();
// Wrap it in milk
myCoffee = new MilkDecorator(myCoffee);
// Wrap that in sugar
myCoffee = new SugarDecorator(myCoffee);
System.out.println("Order: " + myCoffee.getDescription());
System.out.println("Total: $" + myCoffee.getCost());
}
}
In this example, myCoffee is now a SugarDecorator wrapping a MilkDecorator wrapping a SimpleCoffee. When you call getCost(), it triggers a chain reaction: the sugar asks the milk for the price, the milk asks the coffee, the coffee returns 2.00, the milk adds 0.50, and the sugar adds 0.20. Clean, flexible, and no class explosion.
📋 Practical Task
Implement a Pizza Topping System
You are tasked with creating a customizable Pizza ordering system using the Decorator Pattern. Follow these requirements:
- Create a
Pizzainterface with methodsString getDescription()anddouble getCost(). - Implement a
PlainPizzaclass that costs $8.00. - Create an abstract
PizzaDecoratorclass that implementsPizzaand holds a reference to aPizzaobject. - Implement two concrete decorators:
CheeseDecorator(adds $1.50) andPepperoniDecorator(adds $2.00). - In a
Mainclass, simulate a customer ordering a pizza with both cheese and pepperoni, and print the final description and cost to the console.
There are no comments for now.