You’ve built a clean, simple class. It does one thing and does it well. But then, the business requirements start rolling in. “Can we add gift-wrapping?” “What about an optional extended warranty?” “We need to apply promotional discounts!”
Suddenly, your once-elegant class is bloated with boolean flags, if-else chains, and conditional logic. This violates the Open/Closed Principle, which states that software entities should be open for extension but closed for modification.
The Decorator Pattern is a structural design pattern that offers a clean solution. It allows you to attach new behaviors to objects dynamically by placing them inside special “wrapper” objects that contain the new functionality.
[!TIP] Think of it like dressing a doll. The doll is the core object. You can “decorate” it with a hat, a coat, or shoes. Each piece of clothing adds something new without changing the doll itself. You can also combine them in different orders.
The Problem: The Ever-Expanding Class
Let’s imagine we have a simple InvoiceItem class representing a product for sale.
// The initial, clean class
public class InvoiceItem {
public String name;
public float basePrice;
public InvoiceItem(String name, float basePrice) {
this.name = name;
this.basePrice = basePrice;
}
public float getPrice() {
return this.basePrice;
}
}
Now, the business asks for gift-wrapping and discounts. A common but flawed approach is to modify the class directly.
// The WRONG way: Modifying the class directly
public class InvoiceItem {
public String name;
public float basePrice;
// Feature 1: Gift Wrapping
public boolean hasGiftWrap;
public float giftWrapCost = 25.0f;
// Feature 2: Discount
public boolean hasDiscount;
public float discountRate = 0.1f; // 10%
public InvoiceItem(String name, float basePrice) {
this.name = name;
this.basePrice = basePrice;
}
public float getPrice() {
float finalPrice = this.basePrice;
if (hasGiftWrap) {
finalPrice += giftWrapCost;
}
if (hasDiscount) {
finalPrice *= (1 - discountRate);
}
return finalPrice;
}
}
This quickly becomes a maintenance nightmare. What if we add insurance? Or different types of discounts? The getPrice method will become a complex mess of conditionals, and the class will be tightly coupled to every new feature.
The Solution: The Decorator Pattern
The Decorator pattern elegantly solves this by creating a set of “decorator” classes that wrap the original object.
Here’s the structure we’ll build:
classDiagram
direction LR
class IInvoiceItem {
<<interface>>
+getDetails() String
+getPrice() float
}
class Product {
+String name
+float price
+getDetails() String
+getPrice() float
}
class BundleDecorator {
<<abstract>>
#IInvoiceItem wrappedItem
+getDetails() String
+getPrice() float
}
class GiftWrapDecorator {
+getDetails() String
+getPrice() float
}
class InsuranceDecorator {
+getDetails() String
+getPrice() float
}
class DiscountDecorator {
+getDetails() String
+getPrice() float
}
Product --|> IInvoiceItem
BundleDecorator --|> IInvoiceItem
BundleDecorator o-- "1" IInvoiceItem : wraps
GiftWrapDecorator --|> BundleDecorator
InsuranceDecorator --|> BundleDecorator
DiscountDecorator --|> BundleDecorator
IInvoiceItem(Component): An interface that defines the common methods for both the original object and the decorators.Product(Concrete Component): The base class we want to decorate.BundleDecorator(Abstract Decorator): An abstract class that implements theIInvoiceIteminterface and holds a reference to anIInvoiceItemobject. This is the “backbone” of our decorators.GiftWrapDecorator,InsuranceDecorator,DiscountDecorator(Concrete Decorators): These are the actual “wrappers.” Each one adds its own specific behavior before or after delegating the call to the wrapped object.
Step-by-Step Implementation
First, let’s set up our file structure.
decorator/
├── IInvoiceItem.java
├── Product.java
├── BundleDecorator.java
├── GiftWrapDecorator.java
├── InsuranceDecorator.java
└── DiscountDecorator.java
1. The Component Interface
This interface ensures that our decorators can be used interchangeably with the original object.
// decorator/IInvoiceItem.java
public interface IInvoiceItem {
String getDetails();
float getPrice();
}
2. The Concrete Component
This is our original, clean Product class.
// decorator/Product.java
public class Product implements IInvoiceItem {
private String name;
private float price;
public Product(String name, float price) {
this.name = name;
this.price = price;
}
@Override
public String getDetails() {
return String.format("%s (Price: $%.2f)", this.name, this.price);
}
@Override
public float getPrice() {
return this.price;
}
}
3. The Abstract Decorator
This class acts as a base for all concrete decorators. It holds a reference to the object it wraps and delegates calls to it. Making it abstract means we don’t have to implement any logic here; it’s just a pass-through.
// decorator/BundleDecorator.java
public abstract class BundleDecorator implements IInvoiceItem {
protected IInvoiceItem wrappedItem;
public BundleDecorator(IInvoiceItem item) {
this.wrappedItem = item;
}
@Override
public String getDetails() {
return wrappedItem.getDetails(); // Delegate to wrapped item
}
@Override
public float getPrice() {
return wrappedItem.getPrice(); // Delegate to wrapped item
}
}
4. The Concrete Decorators
Here’s where the magic happens. Each decorator overrides the methods to add its own behavior.
GiftWrapDecorator: Adds a fixed cost for gift wrapping.
// decorator/GiftWrapDecorator.java
public class GiftWrapDecorator extends BundleDecorator {
private float wrapCost;
public GiftWrapDecorator(IInvoiceItem item, float wrapCost) {
super(item);
this.wrapCost = wrapCost;
}
@Override
public String getDetails() {
return super.getDetails() + String.format("\n + Gift Wrap ($%.2f)", this.wrapCost);
}
@Override
public float getPrice() {
return super.getPrice() + this.wrapCost;
}
}
InsuranceDecorator: Adds a cost for insurance.
// decorator/InsuranceDecorator.java
public class InsuranceDecorator extends BundleDecorator {
private float insuranceCost;
public InsuranceDecorator(IInvoiceItem item, float insuranceCost) {
super(item);
this.insuranceCost = insuranceCost;
}
@Override
public String getDetails() {
return super.getDetails() + String.format("\n + Insurance ($%.2f)", this.insuranceCost);
}
@Override
public float getPrice() {
return super.getPrice() + this.insuranceCost;
}
}
DiscountDecorator: Applies a percentage-based discount.
[!NOTE] Notice how this decorator modifies the final price by a multiplier, demonstrating how decorators can fundamentally alter the output of the wrapped object.
// decorator/DiscountDecorator.java
public class DiscountDecorator extends BundleDecorator {
private float discountRate;
public DiscountDecorator(IInvoiceItem item, float discountRate) {
super(item);
this.discountRate = discountRate;
}
@Override
public String getDetails() {
return super.getDetails() + String.format("\n - Discount (%.0f%%)", this.discountRate * 100);
}
@Override
public float getPrice() {
return super.getPrice() * (1 - this.discountRate);
}
}
Putting It All Together
Now, we can dynamically “build” our final product by wrapping it in the decorators we need.
graph TD
subgraph Calculation Flow
A[Product: $1000] --> B(GiftWrapDecorator: +$25);
B --> C(InsuranceDecorator: +$75);
C --> D(DiscountDecorator: * 0.90);
D --> E[Final Price: $990];
end
Here’s the client code that demonstrates this stacking:
public class Main {
public static void main(String[] args) {
// Start with a base product
IInvoiceItem laptop = new Product("Gaming Laptop", 1000.0f);
// Now, let's decorate it!
// 1. Add gift wrapping
laptop = new GiftWrapDecorator(laptop, 25.0f);
// 2. Add insurance
laptop = new InsuranceDecorator(laptop, 75.0f);
// 3. Apply a promotional discount
laptop = new DiscountDecorator(laptop, 0.10f); // 10% off
// Display the final details and price
System.out.println("--- Final Invoice ---");
System.out.println(laptop.getDetails());
System.out.println("---------------------");
System.out.printf("Total Price: $%.2f\n", laptop.getPrice());
System.out.println("\n--- Example without discount ---");
IInvoiceItem simpleLaptop = new Product("Work Laptop", 800.0f);
simpleLaptop = new GiftWrapDecorator(simpleLaptop, 20.0f);
System.out.println(simpleLaptop.getDetails());
System.out.printf("Total Price: $%.2f\n", simpleLaptop.getPrice());
}
}
Output:
--- Final Invoice ---
Gaming Laptop (Price: $1000.00)
+ Gift Wrap ($25.00)
+ Insurance ($75.00)
- Discount (10%)
---------------------
Total Price: $990.00
--- Example without discount ---
Work Laptop (Price: $800.00)
+ Gift Wrap ($20.00)
Total Price: $820.00
As you can see, we can mix and match decorators as needed, creating complex objects with varied behaviors without ever touching the Product class. We have successfully extended its behavior while keeping it closed for modification.
Best Practices & Considerations
[!WARNING] Decorator order matters! Applying a percentage discount before adding a fixed fee will result in a different final price than applying it after. Always consider the chain of operations. For example, a 10% discount on a $100 item with $20 shipping is $108 (
100*0.9 + 20), not $108 ((100+20)*0.9).