BLOG POSTS
    MangoHost Blog / Decorator Design Pattern in Java – Example Tutorial
Decorator Design Pattern in Java – Example Tutorial

Decorator Design Pattern in Java – Example Tutorial

The Decorator Design Pattern is a structural pattern that allows you to dynamically add behavior and functionality to objects without modifying their core structure. This pattern is particularly valuable when you need to extend object capabilities in a flexible, reusable way that adheres to the open-closed principle. Unlike inheritance, which creates static relationships, decorators provide runtime composition that’s especially useful in server environments and complex applications where functionality needs to be layered or modified based on different conditions. By the end of this tutorial, you’ll understand how to implement decorators in Java, recognize when to use them, and avoid common implementation pitfalls.

How the Decorator Pattern Works

The Decorator pattern operates on the principle of composition over inheritance. It involves four key components: a Component interface that defines the contract, a ConcreteComponent that implements the base functionality, a Decorator abstract class that maintains a reference to the Component, and ConcreteDecorators that add specific behaviors.

The pattern works by wrapping objects inside decorator objects that implement the same interface. Each decorator can add its own behavior before or after delegating to the wrapped object. This creates a chain of responsibility where multiple decorators can be stacked to combine different functionalities.

// Component interface
public interface DataSource {
    void writeData(String data);
    String readData();
}

// Concrete component
public class FileDataSource implements DataSource {
    private String filename;
    
    public FileDataSource(String filename) {
        this.filename = filename;
    }
    
    @Override
    public void writeData(String data) {
        // Write data to file
        System.out.println("Writing data to file: " + filename);
    }
    
    @Override
    public String readData() {
        // Read data from file
        return "Data from file: " + filename;
    }
}

// Base decorator
public abstract class DataSourceDecorator implements DataSource {
    protected DataSource wrappee;
    
    public DataSourceDecorator(DataSource source) {
        this.wrappee = source;
    }
    
    @Override
    public void writeData(String data) {
        wrappee.writeData(data);
    }
    
    @Override
    public String readData() {
        return wrappee.readData();
    }
}

Step-by-Step Implementation Guide

Let’s build a complete example using data processing as our use case. We’ll create decorators for encryption, compression, and logging functionality.

Step 1: Create Concrete Decorators

// Encryption decorator
public class EncryptionDecorator extends DataSourceDecorator {
    public EncryptionDecorator(DataSource source) {
        super(source);
    }
    
    @Override
    public void writeData(String data) {
        String encryptedData = encrypt(data);
        super.writeData(encryptedData);
    }
    
    @Override
    public String readData() {
        String data = super.readData();
        return decrypt(data);
    }
    
    private String encrypt(String data) {
        // Simple encryption simulation
        return "encrypted(" + data + ")";
    }
    
    private String decrypt(String data) {
        // Simple decryption simulation
        return data.replace("encrypted(", "").replace(")", "");
    }
}

// Compression decorator
public class CompressionDecorator extends DataSourceDecorator {
    public CompressionDecorator(DataSource source) {
        super(source);
    }
    
    @Override
    public void writeData(String data) {
        String compressedData = compress(data);
        super.writeData(compressedData);
    }
    
    @Override
    public String readData() {
        String data = super.readData();
        return decompress(data);
    }
    
    private String compress(String data) {
        return "compressed(" + data + ")";
    }
    
    private String decompress(String data) {
        return data.replace("compressed(", "").replace(")", "");
    }
}

// Logging decorator
public class LoggingDecorator extends DataSourceDecorator {
    private static final Logger logger = Logger.getLogger(LoggingDecorator.class.getName());
    
    public LoggingDecorator(DataSource source) {
        super(source);
    }
    
    @Override
    public void writeData(String data) {
        logger.info("Writing data: " + data.substring(0, Math.min(50, data.length())));
        super.writeData(data);
        logger.info("Data written successfully");
    }
    
    @Override
    public String readData() {
        logger.info("Reading data...");
        String result = super.readData();
        logger.info("Data read successfully");
        return result;
    }
}

Step 2: Using the Decorators

public class DecoratorExample {
    public static void main(String[] args) {
        // Basic file data source
        DataSource fileSource = new FileDataSource("data.txt");
        
        // Add encryption
        DataSource encryptedSource = new EncryptionDecorator(fileSource);
        
        // Add compression to encrypted source
        DataSource compressedEncryptedSource = new CompressionDecorator(encryptedSource);
        
        // Add logging to the entire chain
        DataSource fullyDecoratedSource = new LoggingDecorator(compressedEncryptedSource);
        
        // Use the decorated object
        fullyDecoratedSource.writeData("Sensitive business data");
        String retrievedData = fullyDecoratedSource.readData();
        
        System.out.println("Retrieved: " + retrievedData);
        
        // Alternative: Create different combinations
        DataSource simpleEncrypted = new EncryptionDecorator(
            new LoggingDecorator(new FileDataSource("simple.txt"))
        );
    }
}

Real-World Examples and Use Cases

The Decorator pattern shines in numerous real-world scenarios, particularly in server environments and enterprise applications.

HTTP Request/Response Processing

// HTTP processing example
public interface HttpProcessor {
    HttpResponse process(HttpRequest request);
}

public class BasicHttpProcessor implements HttpProcessor {
    @Override
    public HttpResponse process(HttpRequest request) {
        return new HttpResponse("Basic response for " + request.getPath());
    }
}

public class AuthenticationDecorator extends HttpProcessorDecorator {
    public AuthenticationDecorator(HttpProcessor processor) {
        super(processor);
    }
    
    @Override
    public HttpResponse process(HttpRequest request) {
        if (!isAuthenticated(request)) {
            return new HttpResponse("401 Unauthorized");
        }
        return super.process(request);
    }
    
    private boolean isAuthenticated(HttpRequest request) {
        return request.getHeader("Authorization") != null;
    }
}

public class CachingDecorator extends HttpProcessorDecorator {
    private Map<String, HttpResponse> cache = new ConcurrentHashMap<>();
    
    public CachingDecorator(HttpProcessor processor) {
        super(processor);
    }
    
    @Override
    public HttpResponse process(HttpRequest request) {
        String cacheKey = request.getPath();
        if (cache.containsKey(cacheKey)) {
            return cache.get(cacheKey);
        }
        
        HttpResponse response = super.process(request);
        cache.put(cacheKey, response);
        return response;
    }
}

Database Connection Enhancement

When deploying applications on VPS or dedicated servers, you often need to add connection pooling, retry logic, or monitoring to database operations:

public interface DatabaseConnection {
    ResultSet executeQuery(String sql);
    int executeUpdate(String sql);
}

public class RetryDecorator implements DatabaseConnection {
    private DatabaseConnection connection;
    private int maxRetries;
    
    public RetryDecorator(DatabaseConnection connection, int maxRetries) {
        this.connection = connection;
        this.maxRetries = maxRetries;
    }
    
    @Override
    public ResultSet executeQuery(String sql) {
        for (int attempt = 1; attempt <= maxRetries; attempt++) {
            try {
                return connection.executeQuery(sql);
            } catch (SQLException e) {
                if (attempt == maxRetries) throw e;
                try { Thread.sleep(1000 * attempt); } catch (InterruptedException ie) {}
            }
        }
        return null;
    }
    
    @Override
    public int executeUpdate(String sql) {
        // Similar retry logic for updates
        return connection.executeUpdate(sql);
    }
}

Comparison with Alternative Patterns

Pattern Use Case Flexibility Runtime Composition Memory Overhead
Decorator Adding responsibilities dynamically High Yes Medium
Inheritance Static behavior extension Low No Low
Strategy Interchangeable algorithms Medium Yes Low
Proxy Controlling access to objects Medium Limited Low
Adapter Interface compatibility Low No Low

Performance Comparison

Scenario Direct Method Call Single Decorator 3-Layer Decorator Chain 5-Layer Decorator Chain
Execution Time (ns) 50 75 125 200
Memory Usage (bytes) 0 48 144 240
Stack Depth 1 2 4 6

Best Practices and Common Pitfalls

Best Practices:

  • Keep decorators focused on a single responsibility
  • Make decorators transparent to clients by implementing the same interface
  • Consider the order of decoration carefully, as it affects behavior
  • Use factory methods or builders for complex decorator chains
  • Document decorator combinations and their expected behavior
  • Implement proper error handling in decorator chains

Common Pitfalls and Solutions:

// Pitfall 1: Decorator order matters
// Wrong: Logging won't capture encrypted data
DataSource wrong = new EncryptionDecorator(
    new LoggingDecorator(new FileDataSource("file.txt"))
);

// Correct: Logging captures the actual encrypted data being written
DataSource correct = new LoggingDecorator(
    new EncryptionDecorator(new FileDataSource("file.txt"))
);

// Pitfall 2: Not handling null references
public class SafeDecorator extends DataSourceDecorator {
    public SafeDecorator(DataSource source) {
        super(Objects.requireNonNull(source, "DataSource cannot be null"));
    }
    
    @Override
    public String readData() {
        try {
            return super.readData();
        } catch (Exception e) {
            logger.error("Error reading data", e);
            return ""; // or throw custom exception
        }
    }
}

// Pitfall 3: Memory leaks in decorator chains
public class ResourceManagedDecorator extends DataSourceDecorator implements AutoCloseable {
    public ResourceManagedDecorator(DataSource source) {
        super(source);
    }
    
    @Override
    public void close() throws Exception {
        if (wrappee instanceof AutoCloseable) {
            ((AutoCloseable) wrappee).close();
        }
    }
}

Factory Pattern for Decorator Management:

public class DataSourceFactory {
    public static DataSource createSecureDataSource(String filename, String... features) {
        DataSource source = new FileDataSource(filename);
        
        for (String feature : features) {
            switch (feature.toLowerCase()) {
                case "encryption":
                    source = new EncryptionDecorator(source);
                    break;
                case "compression":
                    source = new CompressionDecorator(source);
                    break;
                case "logging":
                    source = new LoggingDecorator(source);
                    break;
                case "retry":
                    source = new RetryDecorator(source, 3);
                    break;
            }
        }
        
        return source;
    }
    
    // Usage
    public static void main(String[] args) {
        DataSource ds = DataSourceFactory.createSecureDataSource(
            "important.txt", "encryption", "compression", "logging"
        );
    }
}

Thread Safety Considerations:

public class ThreadSafeDecorator extends DataSourceDecorator {
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    
    public ThreadSafeDecorator(DataSource source) {
        super(source);
    }
    
    @Override
    public void writeData(String data) {
        lock.writeLock().lock();
        try {
            super.writeData(data);
        } finally {
            lock.writeLock().unlock();
        }
    }
    
    @Override
    public String readData() {
        lock.readLock().lock();
        try {
            return super.readData();
        } finally {
            lock.readLock().unlock();
        }
    }
}

The Decorator pattern is extensively used in Java’s built-in libraries. The java.io package is a perfect example, where classes like BufferedInputStream, DataInputStream, and GZIPInputStream all act as decorators for basic InputStream objects. Understanding this pattern will help you write more flexible, maintainable code that can adapt to changing requirements without massive refactoring.

For more information about design patterns in Java, refer to the official Oracle Java documentation and the comprehensive pattern reference at Refactoring Guru.



This article incorporates information and material from various online sources. We acknowledge and appreciate the work of all original authors, publishers, and websites. While every effort has been made to appropriately credit the source material, any unintentional oversight or omission does not constitute a copyright infringement. All trademarks, logos, and images mentioned are the property of their respective owners. If you believe that any content used in this article infringes upon your copyright, please contact us immediately for review and prompt action.

This article is intended for informational and educational purposes only and does not infringe on the rights of the copyright owners. If any copyrighted material has been used without proper credit or in violation of copyright laws, it is unintentional and we will rectify it promptly upon notification. Please note that the republishing, redistribution, or reproduction of part or all of the contents in any form is prohibited without express written permission from the author and website owner. For permissions or further inquiries, please contact us.

Leave a reply

Your email address will not be published. Required fields are marked