Design Patterns

84. Decorator Pattern

The Decorator pattern wraps an object with additional behaviour by implementing the same interface and delegating to the wrapped object โ€” the java.io stream hierarchy is the canonical Java example.

Java 21 (core language)

Explanation

Decorator adds responsibilities to an object dynamically without subclassing. It implements the same interface as the object it wraps (ensuring transparency to clients) and forwards calls to the inner object, optionally adding behaviour before or after.

The java.io stream classes are built entirely on Decorator: InputStream is the base; FileInputStream provides bytes from a file; BufferedInputStream(new FileInputStream(...)) wraps it with buffering; DataInputStream(new BufferedInputStream(...)) adds typed reads. Each decorator adds one layer of behaviour; they compose arbitrarily.

Modern alternative in many cases: default methods and composition in Java 8+ โ€” but the classic Decorator structure (wrap-and-delegate) remains the clearest solution for cross-cutting behaviour like logging, caching, access control, and buffering.

Example

import java.util.function.Function;

public class DecoratorPattern {
    // Component interface
    interface DataService {
        String fetchData(String key);
    }

    // Concrete component
    static class DatabaseService implements DataService {
        @Override
        public String fetchData(String key) {
            // Simulate slow DB call
            try { Thread.sleep(10); } catch (InterruptedException e) {}
            return "data-for-" + key;
        }
    }

    // Decorator: adds logging
    static class LoggingDecorator implements DataService {
        private final DataService delegate;
        LoggingDecorator(DataService delegate) { this.delegate = delegate; }

        @Override
        public String fetchData(String key) {
            System.out.println("[LOG] Fetching: " + key);
            String result = delegate.fetchData(key);
            System.out.println("[LOG] Got:      " + result);
            return result;
        }
    }

    // Decorator: adds caching
    static class CachingDecorator implements DataService {
        private final DataService delegate;
        private final java.util.Map<String, String> cache = new java.util.HashMap<>();

        CachingDecorator(DataService delegate) { this.delegate = delegate; }

        @Override
        public String fetchData(String key) {
            return cache.computeIfAbsent(key, delegate::fetchData);
        }
    }

    public static void main(String[] args) {
        // Compose: DB + Cache + Logging
        DataService service = new LoggingDecorator(
                              new CachingDecorator(
                              new DatabaseService()));

        System.out.println(service.fetchData("user:1")); // fetches + logs
        System.out.println(service.fetchData("user:1")); // from cache (still logged)
        System.out.println(service.fetchData("user:2")); // new fetch

        // Java IO: classic Decorator usage
        System.out.println("\nJava IO Decorator chain:");
        try (var in = new java.io.DataInputStream(
                       new java.io.BufferedInputStream(
                       new java.io.FileInputStream(java.io.FileDescriptor.in)))) {
            System.out.println("Layered stream: DataInputStream -> BufferedInputStream -> FileInputStream");
            // Each layer adds one behaviour without modifying the others
        } catch (Exception e) { /* ignore for demo */ }
    }
}

Sources