Skip to main content
Advertisement

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.

Advertisement