17.4 Structural Patterns (Decorator, Adapter, Proxy)
1. Decorator
"Dynamically add functionality to an object without modifying it." More flexible than inheritance for composing behavior.
interface Coffee { String getDescription(); int getCost(); }
class SimpleCoffee implements Coffee {
@Override public String getDescription() { return "Americano"; }
@Override public int getCost() { return 3000; }
}
abstract class CoffeeDecorator implements Coffee {
protected final Coffee coffee;
CoffeeDecorator(Coffee coffee) { this.coffee = coffee; }
}
class MilkDecorator extends CoffeeDecorator {
MilkDecorator(Coffee c) { super(c); }
@Override public String getDescription() { return coffee.getDescription() + " + Milk"; }
@Override public int getCost() { return coffee.getCost() + 500; }
}
class SyrupDecorator extends CoffeeDecorator {
private final String flavor;
SyrupDecorator(Coffee c, String flavor) { super(c); this.flavor = flavor; }
@Override public String getDescription() { return coffee.getDescription() + " + " + flavor + " Syrup"; }
@Override public int getCost() { return coffee.getCost() + 300; }
}
class WhipDecorator extends CoffeeDecorator {
WhipDecorator(Coffee c) { super(c); }
@Override public String getDescription() { return coffee.getDescription() + " + Whip"; }
@Override public int getCost() { return coffee.getCost() + 700; }
}
// Layer behaviors dynamically
Coffee order = new SyrupDecorator(
new WhipDecorator(new MilkDecorator(new SimpleCoffee())),
"Vanilla"
);
System.out.printf("%s: %,d won%n", order.getDescription(), order.getCost());
// Americano + Milk + Whip + Vanilla Syrup: 4,500 won
2. Adapter
"Convert incompatible interfaces to work together." Like a power adapter for foreign outlets, fits existing code to a new interface without modifying it.
class LegacyPaymentSystem {
public String doTransaction(String userId, double amount) {
return String.format("LEGACY_OK: user=%s, amount=%.0f", userId, amount);
}
public boolean checkBalance(String userId) { return true; }
}
interface ModernPaymentGateway {
PaymentResult processPayment(Payment payment);
}
record Payment(String userId, int amount, String currency) {}
record PaymentResult(boolean success, String transactionId, String message) {}
class LegacyPaymentAdapter implements ModernPaymentGateway {
private final LegacyPaymentSystem legacy;
LegacyPaymentAdapter(LegacyPaymentSystem legacy) { this.legacy = legacy; }
@Override
public PaymentResult processPayment(Payment payment) {
if (!legacy.checkBalance(payment.userId()))
return new PaymentResult(false, null, "Insufficient balance");
double amount = convertToKRW(payment.amount(), payment.currency());
String result = legacy.doTransaction(payment.userId(), amount);
return new PaymentResult(true, "TXN_" + System.currentTimeMillis(), result);
}
private double convertToKRW(int amount, String currency) {
return switch (currency) {
case "USD" -> amount * 1300.0;
case "EUR" -> amount * 1420.0;
default -> amount;
};
}
}
ModernPaymentGateway gateway = new LegacyPaymentAdapter(new LegacyPaymentSystem());
PaymentResult result = gateway.processPayment(new Payment("user123", 100, "USD"));
System.out.println(result);
3. Proxy
"Control access to an object through a surrogate." Used for lazy initialization, caching, logging, and access control.
interface UserRepository { String findById(Long id); }
class DatabaseUserRepository implements UserRepository {
@Override public String findById(Long id) {
System.out.println("🗄️ DB query: SELECT * FROM users WHERE id=" + id);
return "User_" + id;
}
}
// Caching Proxy
class CachedUserRepositoryProxy implements UserRepository {
private final UserRepository real;
private final Map<Long, String> cache = new HashMap<>();
CachedUserRepositoryProxy(UserRepository real) { this.real = real; }
@Override
public String findById(Long id) {
if (cache.containsKey(id)) {
System.out.println("⚡ Cache hit: id=" + id);
return cache.get(id);
}
String user = real.findById(id);
cache.put(id, user);
return user;
}
}
UserRepository repo = new CachedUserRepositoryProxy(new DatabaseUserRepository());
repo.findById(1L); // DB query
repo.findById(1L); // Cache hit (no DB query)
repo.findById(2L); // DB query
repo.findById(2L); // Cache hit
Pro Tip
Structural patterns in the real world:
-
Decorator: Spring Security's filter chain and Java I/O stream chaining are classic examples.
BufferedReader reader = new BufferedReader( // Decorator
new InputStreamReader( // Decorator
new FileInputStream("file.txt") // Real stream
)
); -
Adapter: Wrapping third-party library interfaces, converting legacy APIs to modern interfaces.
-
Proxy: Spring AOP (
@Transactional,@Cacheable,@Aspect) is implemented using proxy patterns via JDK dynamic proxies or CGLIB.