17.3 Behavioral Patterns (Strategy, Observer, Template Method)
1. Strategy
"Separate algorithms into interfaces, swappable at runtime." Eliminates conditional branching by injecting behavior from outside.
@FunctionalInterface
interface PaymentStrategy {
boolean pay(String orderId, int amount);
}
class CreditCardPayment implements PaymentStrategy {
private final String cardNumber;
CreditCardPayment(String cardNumber) { this.cardNumber = cardNumber; }
@Override public boolean pay(String orderId, int amount) {
System.out.printf("💳 Credit card (%s) $%d payment - order: %s%n",
cardNumber.substring(cardNumber.length() - 4), amount, orderId);
return true;
}
}
class KakaoPayPayment implements PaymentStrategy {
@Override public boolean pay(String orderId, int amount) {
System.out.printf("📱 KakaoPay $%d payment - order: %s%n", amount, orderId);
return true;
}
}
class OrderService {
private PaymentStrategy paymentStrategy;
public void setPaymentStrategy(PaymentStrategy strategy) { this.paymentStrategy = strategy; }
public void checkout(String orderId, int amount) {
System.out.println("=== Processing order: " + orderId + " ===");
if (paymentStrategy.pay(orderId, amount)) System.out.println("✅ Payment successful!");
else System.out.println("❌ Payment failed!");
}
}
OrderService service = new OrderService();
service.setPaymentStrategy(new CreditCardPayment("1234-5678-9012-3456"));
service.checkout("ORDER-001", 45000);
service.setPaymentStrategy(new KakaoPayPayment());
service.checkout("ORDER-002", 12000);
// Lambda as one-off strategy
service.setPaymentStrategy((orderId, amount) -> {
System.out.printf("🏦 Bank transfer $%d - order: %s%n", amount, orderId);
return true;
});
service.checkout("ORDER-003", 89000);
2. Observer
"Automatically notify all subscribers when an object's state changes." The core of event-driven systems.
record OrderEvent(String type, String orderId, int amount) {}
@FunctionalInterface
interface OrderObserver { void onEvent(OrderEvent event); }
class OrderEventPublisher {
private final List<OrderObserver> observers = new ArrayList<>();
public void subscribe(OrderObserver obs) { observers.add(obs); }
public void unsubscribe(OrderObserver obs) { observers.remove(obs); }
public void publish(OrderEvent event) {
System.out.println("[Publisher] Event: " + event.type());
observers.forEach(obs -> obs.onEvent(event));
}
}
class EmailNotifier implements OrderObserver {
@Override public void onEvent(OrderEvent e) {
if ("COMPLETED".equals(e.type()))
System.out.println("📧 Email: Order " + e.orderId() + " completion notification sent");
}
}
class AnalyticsTracker implements OrderObserver {
@Override public void onEvent(OrderEvent e) {
System.out.printf("📊 Analytics: [%s] order=%s amount=%d%n", e.type(), e.orderId(), e.amount());
}
}
OrderEventPublisher publisher = new OrderEventPublisher();
publisher.subscribe(new EmailNotifier());
publisher.subscribe(new AnalyticsTracker());
publisher.subscribe(e -> { // Lambda subscriber
if (e.amount() > 100000) System.out.println("🌟 VIP alert: High-value order " + e.orderId());
});
publisher.publish(new OrderEvent("PLACED", "ORD-001", 45000));
publisher.publish(new OrderEvent("COMPLETED", "ORD-001", 45000));
publisher.publish(new OrderEvent("PLACED", "ORD-002", 150000));
3. Template Method
"Fix the algorithm skeleton while delegating specific steps to subclasses."
abstract class ReportGenerator {
// Template method: algorithm skeleton (final - cannot be overridden)
public final void generateReport(String title) {
System.out.println("=== Generating " + title + " report ===");
List<String> data = fetchData();
List<String> processed= processData(data);
String report = formatReport(title, processed);
saveReport(report);
System.out.println("=== Report complete ===\n");
}
// Abstract: must implement
protected abstract List<String> fetchData();
protected abstract List<String> processData(List<String> data);
protected abstract String formatReport(String title, List<String> data);
// Hook: optional override
protected boolean shouldCompress() { return false; }
private void saveReport(String report) {
if (shouldCompress()) System.out.println("Compressing...");
System.out.println("Saved: " + report.substring(0, Math.min(50, report.length())) + "...");
}
}
class CsvReportGenerator extends ReportGenerator {
@Override protected List<String> fetchData() {
System.out.println("📄 Loading from CSV");
return List.of("Alice,30,Seoul", "Bob,25,Busan");
}
@Override protected List<String> processData(List<String> data) {
return data.stream()
.map(row -> row.split(","))
.map(p -> String.format("Name:%s Age:%s City:%s", p[0], p[1], p[2]))
.collect(java.util.stream.Collectors.toList());
}
@Override protected String formatReport(String title, List<String> data) {
return title + "\n" + String.join("\n", data);
}
}
new CsvReportGenerator().generateReport("Monthly User Report");
Real-world pattern usage:
-
Strategy: Most commonly used pattern. In Java 8+, implement with
@FunctionalInterface+ lambda.Comparator,Predicate,Functionare all strategy patterns. -
Observer: Spring's
ApplicationEvent+@EventListener, and RxJava/Project Reactor's reactive streams are modern implementations of the observer pattern. -
Template Method: Spring's
JdbcTemplate,RestTemplate,AbstractControllerare classic examples.
Pattern combinations: Real-world code rarely uses patterns in isolation. Common combos: Strategy + Factory (create strategy objects via factory), Observer + Singleton (global event bus).