What is a Decorator Design Pattern
The Decorator design pattern is a structural design pattern in object-oriented programming. It is used to dynamically add or extend functionality to objects at runtime without modifying their code. The pattern allows you to wrap an object in one or more decorators, effectively creating a chain of objects that add new behaviors or responsibilities to the original object.
The primary motivation behind using the Decorator pattern is to achieve the Open-Closed Principle, one of the SOLID principles, which states that classes should be open for extension but closed for modification. With the Decorator pattern, you can add new features to an object without altering its source code, making it easier to maintain and extend the application.
This pattern allows for the extension of an object’s behavior without modifying its original structure.
The intent of the Decorator Design Pattern is to attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to sub-classing for extending functionality. If we use sub-classing, this is done at compile time and it’s applicable to all the instances of the class. We can’t add any new functionality or remove any existing behavior at runtime – this is when Decorator pattern comes into picture.
Explanation
The Decorator Pattern is used to extend the functionality of an object dynamically without having to change the original class source or using inheritance. This is accomplished by creating an object wrapper referred to as a Decorator around the actual object.
The Decorator object is designed to have the same interface as the underlying object. This allows a client object to interact with the Decorator object in exactly the same manner as it would with the underlying actual object. The Decorator object contains a reference to the actual object. The Decorator object receives all requests (calls) from a client. In turn, it forwards these calls to the underlying object. The Decorator object adds some additional functionality before or after forwarding requests to the underlying object. This ensures that the additional functionality can be added to a given object externally at runtime without modifying its structure.
Key Features of the Decorator Design Pattern:
- Component: Represents the base object that needs to be decorated. It defines the interface or abstract class for both the component and its decorators.
// Component Interface
public interface Coffee {
double getCost();
}
- Decorator: Acts as the abstract base class for all decorators. It implements the component interface or abstract class and provides additional functionality by delegating to the wrapped component object.
// Decorator implements Component interface
abstract class CoffeeDecorator implements Coffee {
// wrapped Component object
protected Coffee coffee;
public CoffeeDecorator(Coffee coffee) {
this.coffee = coffee;
}
}
- Concrete Component: Represents the original object to be decorated. It provides the core functionality that can be enhanced or modified by decorators.
// Concrete Component
class SimpleCoffee implements Coffee {
@Override
public double getCost() {
return 5;
}
}
- Concrete Decorators: Extend the decorator base class and provide specific enhancements or modifications to the component’s behavior. They wrap the component object and add extra functionality.
// Concrete Decorators extends Base Decorator class
class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}
@Override
public double getCost() {
return coffee.getCost() + 2;
}
}
class SugarDecorator extends CoffeeDecorator {
public SugarDecorator(Coffee coffee) {
super(coffee);
}
@Override
public double getCost() {
return coffee.getCost() + 1;
}
}
Client code :
package com.rndayala.designpatterns.decorator;
//Usage
public class Demo {
public static void main(String[] args) {
Coffee simpleCoffee = new SimpleCoffee();
Coffee coffeeWithMilkAndSugar = new MilkDecorator(new SugarDecorator(simpleCoffee));
System.out.println("Cost of Simple Coffee: " + simpleCoffee.getCost()); // Output: Cost of Simple Coffee: 5
System.out.println("Cost of Coffee with Milk and Sugar: " + coffeeWithMilkAndSugar.getCost()); // Output: Cost of
// Coffee with Milk
// and Sugar: 8
}
}
Benefits
- Dynamic behavior extension: The Decorator pattern allows for the dynamic addition of new functionality to an object at runtime without affecting its original structure or the code that uses it.
- Single Responsibility Principle: Each decorator class has a specific responsibility, which promotes the separation of concerns and keeps the codebase modular and maintainable.
- Easy customization and flexibility: Decorators can be combined and composed in various ways to achieve different combinations of functionality, providing great flexibility in customizing object behavior.
- Open-Closed Principle: The Decorator pattern follows the Open-Closed Principle, as it allows for extension without modifying existing code. New decorators can be added without changing the component or existing decorators.
Decorator pattern is used a lot in Java IO classes, such as FileReader, BufferedReader
etc. The disadvantage of decorator pattern is that it uses a lot of similar kind of objects
(decorators).
Implementing the Decorator Design Pattern in Java
Let’s demonstrate the implementation of the Decorator design pattern using Java code. We’ll create an example of a text formatter that allows dynamic formatting options to be applied to text.
The TextFormatter interface defines the component interface, which represents the base object that needs to be decorated. It declares a format() method to format the text.
The PlainTextFormatter class is the concrete component that provides the core functionality of formatting text without any decorations.
// Component
interface TextFormatter {
String format(String text);
}
// Concrete Component - represents the original object to be decorated
// specifies core functionality without any decoration
class PlainTextFormatter implements TextFormatter {
@Override
public String format(String text) {
return text;
}
}
// Abstract Decorator class that implements the Component interface
// provides additional functionality by delegating to the wrapped component object
abstract class TextDecorator implements TextFormatter {
// wrapped component object
private TextFormatter textFormatter;
public TextDecorator(TextFormatter textFormatter) {
this.textFormatter = textFormatter;
}
@Override
public String format(String text) {
return textFormatter.format(text);
}
}
// Concrete Decorators
class BoldTextDecorator extends TextDecorator {
public BoldTextDecorator(TextFormatter textFormatter) {
super(textFormatter);
}
// adding decoration
@Override
public String format(String text) {
String formattedText = super.format(text);
return "<b>" + formattedText + "</b>";
}
}
class ItalicTextDecorator extends TextDecorator {
public ItalicTextDecorator(TextFormatter textFormatter) {
super(textFormatter);
}
@Override
public String format(String text) {
String formattedText = super.format(text);
return "<i>" + formattedText + "</i>";
}
}
public class Demo {
public static void main(String[] args) {
TextFormatter plainFormatter = new PlainTextFormatter();
TextFormatter boldFormatter = new BoldTextDecorator(plainFormatter);
TextFormatter boldItalicFormatter = new ItalicTextDecorator(boldFormatter);
String text = "Hello, world!";
String formattedText = boldItalicFormatter.format(text);
System.out.println(formattedText); // Output: <i><b>Hello, world!</b></i>
}
}
- The
TextDecoratorabstract class serves as the decorator base class. It implements theTextFormatterinterface and has a reference to the wrapped component object (textFormatter). It delegates theformat()method call to the component. - The
BoldTextDecoratorandItalicTextDecoratorclasses are concrete decorators. They extend theTextDecoratorclass and add specific decorations to the text by modifying the output of theformat()method.
In the above example, we create instances of the PlainTextFormatter, BoldTextDecorator, and ItalicTextDecorator classes. We pass the base formatter as an argument when creating the decorators, effectively wrapping them around each other. When we call the format() method on the boldItalicFormatter, the decorators’ behavior is applied to the text, resulting in a formatted output with both bold and italic styles.
Use cases
Use the Decorator pattern in the following cases:
- To add responsibilities to individual objects dynamically and transparently, that is,
without affecting other objects. - For responsibilities that can be withdrawn.
- When extension by sub-classing is impractical. Sometimes a large number of independent extensions are possible and would produce an explosion of subclasses to
support every combination. Or a class definition may be hidden or otherwise unavailable for sub-classing. - It’s easy to maintain and extend when the number of choices are more.
The Decorator design pattern offers a powerful mechanism for enhancing the functionality of objects at runtime while maintaining code flexibility and modularity. In Java, the Decorator pattern can be utilized in various scenarios where dynamic extension and customization of object behavior are required. By employing the Decorator pattern effectively, you can create software systems that are more adaptable, reusable, and maintainable, enabling you to add new features and variations without sacrificing the core structure of your code.
Another example of Decorator Pattern
// Component interface
public interface Coffee {
double getCost();
String getDescription();
}
// Concrete component implements the Component interface
public class BasicCoffee implements Coffee {
@Override
public double getCost() {
return 2.0;
}
@Override
public String getDescription() {
return "Basic Coffee";
}
}
// Decorator classes with various add-ons
public class MilkDecorator implements Coffee {
private Coffee coffee;
public MilkDecorator(Coffee coffee) {
this.coffee = coffee;
}
@Override
public double getCost() {
return coffee.getCost() + 1.0; // Additional cost for milk
}
@Override
public String getDescription() {
return coffee.getDescription() + ", Milk";
}
}
// Decorator classes with various add-ons
public class WhipDecorator implements Coffee {
private Coffee coffee;
public WhipDecorator(Coffee coffee) {
this.coffee = coffee;
}
@Override
public double getCost() {
return coffee.getCost() + 0.5; // Additional cost for whip
}
@Override
public String getDescription() {
return coffee.getDescription() + ", Whip";
}
}
// Client code
public class CoffeeShop {
public static void main(String[] args) {
// Order a basic coffee
Coffee basicCoffee = new BasicCoffee();
System.out.println("Cost: $" + basicCoffee.getCost());
System.out.println("Description: " + basicCoffee.getDescription());
// Add milk to the coffee
Coffee coffeeWithMilk = new MilkDecorator(basicCoffee);
System.out.println("Cost: $" + coffeeWithMilk.getCost());
System.out.println("Description: " + coffeeWithMilk.getDescription());
// Add whip to the coffee
Coffee coffeeWithWhip = new WhipDecorator(basicCoffee);
System.out.println("Cost: $" + coffeeWithWhip.getCost());
System.out.println("Description: " + coffeeWithWhip.getDescription());
// Add both milk and whip to the coffee
Coffee coffeeWithMilkAndWhip = new WhipDecorator(new MilkDecorator(basicCoffee));
System.out.println("Cost: $" + coffeeWithMilkAndWhip.getCost());
System.out.println("Description: " + coffeeWithMilkAndWhip.getDescription());
}
}
In this example, we’ve used the Decorator pattern to add optional enhancements (milk and whip) to the basic coffee at runtime. The Decorator pattern allows us to compose different combinations of add-ons dynamically without creating a separate class for each possible combination, making the code more flexible and maintainable.
Advanced Example : e-Commerce system
Let’s consider a complex order processing system where we want to apply different types of discounts to orders based on various criteria.
Example: Applying Dynamic Discounts in e-Commerce site
In an e-commerce system, customers may be eligible for different types of discounts, such as percentage-based discounts, fixed amount discounts, or special discounts for specific product categories. The Decorator pattern can be utilized to dynamically apply these discounts to orders without altering the core order processing classes.
// Component interface
public interface OrderProcessor {
void processOrder(Order order);
}
// Concrete component
public class BasicOrderProcessor implements OrderProcessor {
@Override
public void processOrder(Order order) {
// Basic order processing logic
double totalAmount = order.calculateTotalAmount();
System.out.println("Total amount : $" + totalAmount);
// Additional logic for processing the order (e.g., inventory update, shipping, etc.)
}
}
// Decorator classes for multiple features / add-ons
// Decorator classes wrap Component object
public class PercentageDiscountDecorator implements OrderProcessor {
private OrderProcessor orderProcessor;
private double discountPercentage;
public PercentageDiscountDecorator(OrderProcessor orderProcessor, double discountPercentage) {
this.orderProcessor = orderProcessor;
this.discountPercentage = discountPercentage;
}
@Override
public void processOrder(Order order) {
// Calculate the discount and apply it to the order
double discountAmount = order.calculateTotalAmount() * (discountPercentage / 100);
order.applyDiscount(discountAmount);
// Call the next processor in the chain or the core processor
orderProcessor.processOrder(order);
}
}
public class FixedAmountDiscountDecorator implements OrderProcessor {
private OrderProcessor orderProcessor;
private double fixedAmount;
public FixedAmountDiscountDecorator(OrderProcessor orderProcessor, double fixedAmount) {
this.orderProcessor = orderProcessor;
this.fixedAmount = fixedAmount;
}
@Override
public void processOrder(Order order) {
// Apply fixed amount discount to the order
order.applyDiscount(fixedAmount);
// Call the next processor in the chain or the core processor
orderProcessor.processOrder(order);
}
}
public class CategoryDiscountDecorator implements OrderProcessor {
private OrderProcessor orderProcessor;
private String discountedCategory;
private double categoryDiscountPercentage;
public CategoryDiscountDecorator(OrderProcessor orderProcessor, String discountedCategory, double categoryDiscountPercentage) {
this.orderProcessor = orderProcessor;
this.discountedCategory = discountedCategory;
this.categoryDiscountPercentage = categoryDiscountPercentage;
}
@Override
public void processOrder(Order order) {
// Calculate the total amount for the discounted category and apply the discount
double discountedAmount = order.calculateCategoryTotal(discountedCategory) * (categoryDiscountPercentage / 100);
order.applyDiscount(discountedAmount);
// Call the next processor in the chain or the core processor
orderProcessor.processOrder(order);
}
}
public class Order {
// ... Other order-related properties and methods ...
private double totalAmount;
public double calculateTotalAmount() {
// Calculate the total amount based on the order items and quantities
// Implementation not shown for simplicity
return totalAmount;
}
public double calculateCategoryTotal(String category) {
// Calculate the total amount for a specific product category
// Implementation not shown for simplicity
return categoryTotalAmount;
}
public void applyDiscount(double discountAmount) {
totalAmount -= discountAmount;
}
}
// Client code / Demo
public class OrderProcessingSystem {
public static void main(String[] args) {
// Create the basic order processor
OrderProcessor basicOrderProcessor = new BasicOrderProcessor();
// Add percentage-based discount decorator
OrderProcessor orderProcessorWithPercentageDiscount = new PercentageDiscountDecorator(basicOrderProcessor, 10);
// Add fixed amount discount decorator
OrderProcessor orderProcessorWithFixedAmountDiscount = new FixedAmountDiscountDecorator(orderProcessorWithPercentageDiscount, 5.0);
// Add category-based discount decorator
OrderProcessor finalOrderProcessor = new CategoryDiscountDecorator(orderProcessorWithFixedAmountDiscount, "Electronics", 15);
// Process the order with dynamic discounts
Order order = new Order(/* Order details */);
finalOrderProcessor.processOrder(order);
}
}
In this example, we’ve used the Decorator pattern to apply dynamic discounts to orders in an e-commerce system. The system allows you to compose different types of discounts and apply them to the order processing chain at runtime. This approach keeps the order processing logic separate from the discount logic, promoting modularity and flexibility in the system.