Structural Design Patterns
Implement Adapter, Decorator, Proxy, and Composite patterns to compose objects and classes.
Overview
Key Points
- •Adapter wraps an incompatible class to match a required interface
- •Object Adapter uses composition; Class Adapter uses multiple inheritance (not possible in Java, so object adapter is the standard)
- •Decorator implements the same interface as the decorated object and delegates + adds behavior
- •Decorators can be stacked: new BoldDecorator(new ItalicDecorator(text))
- •Java I/O streams use Decorator heavily: new BufferedReader(new FileReader(path))
- •Proxy implements the same interface and intercepts calls for logging, security, caching, or lazy loading
- •Java dynamic proxies (java.lang.reflect.Proxy) create proxies at runtime
- •Composite allows treating leaf and composite nodes uniformly through a common interface
Examples
Adapter: plugging a legacy library into a new interface
// New interface the rest of the application uses
interface PaymentProcessor {
void processPayment(double amount);
}
// Legacy library with incompatible API
class LegacyPaymentGateway {
public void executeTransaction(String amount, String currency) {
System.out.println("Legacy gateway: " + amount + " " + currency);
}
}
// Adapter wraps the legacy class and exposes the new interface
class PaymentGatewayAdapter implements PaymentProcessor {
private final LegacyPaymentGateway legacy;
PaymentGatewayAdapter(LegacyPaymentGateway legacy) {
this.legacy = legacy;
}
@Override
public void processPayment(double amount) {
legacy.executeTransaction(String.valueOf(amount), "USD");
}
}
public class Main {
public static void main(String[] args) {
PaymentProcessor processor = new PaymentGatewayAdapter(new LegacyPaymentGateway());
processor.processPayment(99.99); // calls legacy internally
}
}The client (Main) uses PaymentProcessor and never knows about LegacyPaymentGateway. Swapping to a different gateway only requires a different Adapter.
Decorator: stacking text formatting behaviors
interface TextFormatter {
String format(String text);
}
class PlainText implements TextFormatter {
@Override
public String format(String text) { return text; }
}
abstract class TextDecorator implements TextFormatter {
protected final TextFormatter inner;
TextDecorator(TextFormatter inner) { this.inner = inner; }
}
class BoldDecorator extends TextDecorator {
BoldDecorator(TextFormatter inner) { super(inner); }
@Override
public String format(String text) {
return "**" + inner.format(text) + "**";
}
}
class UpperCaseDecorator extends TextDecorator {
UpperCaseDecorator(TextFormatter inner) { super(inner); }
@Override
public String format(String text) {
return inner.format(text).toUpperCase();
}
}
public class Main {
public static void main(String[] args) {
TextFormatter f = new BoldDecorator(new UpperCaseDecorator(new PlainText()));
System.out.println(f.format("hello")); // **HELLO**
}
}Decorators are composed by nesting constructors. Each layer adds one responsibility. Adding a new formatting behavior requires only a new decorator class, not changes to existing classes.
Proxy: caching proxy for an expensive operation
interface DataService {
String fetchData(String key);
}
class RealDataService implements DataService {
@Override
public String fetchData(String key) {
System.out.println("Fetching from database: " + key);
return "data-for-" + key; // simulates expensive DB call
}
}
class CachingProxy implements DataService {
private final DataService real = new RealDataService();
private final java.util.Map<String, String> cache = new java.util.HashMap<>();
@Override
public String fetchData(String key) {
return cache.computeIfAbsent(key, real::fetchData);
}
}
public class Main {
public static void main(String[] args) {
DataService service = new CachingProxy();
System.out.println(service.fetchData("user:1")); // fetches from DB
System.out.println(service.fetchData("user:1")); // returns from cache
System.out.println(service.fetchData("user:2")); // fetches from DB
}
}The client uses DataService and does not know about caching. The proxy transparently intercepts calls and serves cached results when available.
Common Mistakes
- Confusing Adapter with Decorator. Adapter changes the interface. Decorator keeps the same interface but adds behavior. If the interface before and after wrapping is the same, it is Decorator.
- Confusing Proxy with Decorator. Proxy controls access to the same interface (caching, security, remote). Decorator adds behavior to the same interface. The distinction is intent, not structure.
- Creating too many decorator layers. Each layer adds a method call. For performance-critical paths, consider combining decorators into a single class.
Note
Java I/O is the textbook example of Decorator: InputStream, FileInputStream, BufferedInputStream, DataInputStream all implement or extend the same base. You can wrap them in any order to compose behavior: new DataInputStream(new BufferedInputStream(new FileInputStream("file.dat"))).
Exercises
Write Java code and click Run to execute it instantly. When your output looks correct, mark as done.
Completed: 0 / 2 exercises
Knowledge Check
Question 1 of 4