Skip to main content

Ch 17.3 Behavioral Patterns (Strategy, Observer, Template Method)

1. Strategy — Interchangeable Algorithms

"Encapsulates a family of algorithms behind an interface so they can be swapped at runtime." Replaces long if-else / switch chains with injectable behavior.

// Payment system example
@FunctionalInterface
interface PaymentStrategy {
boolean pay(String orderId, int amount);
}

// Concrete strategies
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 — 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 — Order: %s%n", amount, orderId);
return true;
}
}

class PayPalPayment implements PaymentStrategy {
@Override
public boolean pay(String orderId, int amount) {
System.out.printf("PayPal: %,d — Order: %s%n", amount, orderId);
return true;
}
}

// Context: class that uses the strategy
class OrderService {
private PaymentStrategy paymentStrategy;

// Inject strategy (can be changed at runtime)
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!");
}
}
}

// Usage — choose strategy at runtime
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 a 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);

Comparator is a Strategy Pattern

List<String> names = new ArrayList<>(List.of("Charlie", "alice", "BOB", "dave"));

// Different sort strategies interchangeable at runtime
names.sort(String::compareToIgnoreCase); // alphabetical (case-insensitive)
names.sort(Comparator.comparingInt(String::length)); // by length
names.sort(Comparator.comparingInt(String::length)
.thenComparing(String.CASE_INSENSITIVE_ORDER)); // by length, then alphabetical

2. Observer — Event Notification

"Automatically notifies all subscribed objects when one object's state changes." This is the foundation of event-driven architecture.

// Event type
record OrderEvent(String type, String orderId, int amount) {}

// Observer interface
@FunctionalInterface
interface OrderObserver {
void onEvent(OrderEvent event);
}

// Subject (publisher): holds the list of observers and notifies them
class OrderEventPublisher {
private final List<OrderObserver> observers = new ArrayList<>();

public void subscribe(OrderObserver observer) {
observers.add(observer);
}

public void unsubscribe(OrderObserver observer) {
observers.remove(observer);
}

public void publish(OrderEvent event) {
System.out.println("[Publisher] Publishing event: " + event.type());
for (OrderObserver observer : observers) {
observer.onEvent(event);
}
}
}

// Concrete observers
class EmailNotifier implements OrderObserver {
@Override
public void onEvent(OrderEvent e) {
if ("COMPLETED".equals(e.type())) {
System.out.println("Email: Order " + e.orderId() + " completion notice sent.");
}
}
}

class InventoryUpdater implements OrderObserver {
@Override
public void onEvent(OrderEvent e) {
if ("PLACED".equals(e.type())) {
System.out.println("Inventory: Deducting stock for order " + e.orderId());
}
}
}

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());
}
}

// Usage
OrderEventPublisher publisher = new OrderEventPublisher();

publisher.subscribe(new EmailNotifier());
publisher.subscribe(new InventoryUpdater());
publisher.subscribe(new AnalyticsTracker());

// Lambda subscription also works
publisher.subscribe(e -> {
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 — Fixed Algorithm Skeleton

"Defines the skeleton of an algorithm in a base class, deferring specific steps to subclasses." The overall flow is fixed; only the variable parts are overridden.

// Data report generator
abstract class ReportGenerator {

// Template method: algorithm skeleton (final — cannot be overridden)
public final void generateReport(String title) {
System.out.println("=== Starting report: " + title + " ===");
List<String> data = fetchData(); // step 1: fetch (subclass implements)
List<String> processed = processData(data); // step 2: process (subclass implements)
String report = formatReport(title, processed); // step 3: format (subclass implements)
saveReport(report); // step 4: save (shared logic)
System.out.println("=== Report complete ===\n");
}

// Abstract methods: subclass 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 method: subclass MAY override
protected boolean shouldCompress() { return false; }

// Shared logic: not meant to change
private void saveReport(String report) {
if (shouldCompress()) System.out.println("Compressing...");
System.out.println("Saving: " + report.substring(0, Math.min(50, report.length())) + "...");
}
}

// CSV report implementation
class CsvReportGenerator extends ReportGenerator {
@Override
protected List<String> fetchData() {
System.out.println("Loading data from CSV file");
return List.of("Alice,30,New York", "Bob,25,London", "Carol,28,Tokyo");
}

@Override
protected List<String> processData(List<String> data) {
return data.stream()
.map(row -> row.split(","))
.map(parts -> String.format("Name:%s Age:%s City:%s", parts[0], parts[1], parts[2]))
.collect(java.util.stream.Collectors.toList());
}

@Override
protected String formatReport(String title, List<String> data) {
return title + "\n" + String.join("\n", data);
}
}

// Database report implementation
class DatabaseReportGenerator extends ReportGenerator {
@Override
protected List<String> fetchData() {
System.out.println("Querying database...");
return List.of("Record1", "Record2", "Record3");
}

@Override
protected List<String> processData(List<String> data) {
return data.stream().map(String::toUpperCase)
.collect(java.util.stream.Collectors.toList());
}

@Override
protected String formatReport(String title, List<String> data) {
return "<html><h1>" + title + "</h1>" + String.join("<br>", data) + "</html>";
}

@Override
protected boolean shouldCompress() { return true; } // override hook
}

// Usage
ReportGenerator csvGen = new CsvReportGenerator();
csvGen.generateReport("Monthly User Report");

ReportGenerator dbGen = new DatabaseReportGenerator();
dbGen.generateReport("Weekly Sales Analysis");

Pro Tips

Frequency of pattern use in real-world Java:

  1. Strategy: One of the most used patterns. In Java 8+, @FunctionalInterface + lambda makes strategy implementation trivial. Comparator, Predicate, Function, Runnable are all Strategy patterns.

  2. Observer: Spring's ApplicationEvent + @EventListener, RxJava, and Project Reactor's reactive streams are modern implementations of the Observer pattern.

  3. Template Method: Spring's JdbcTemplate, RestTemplate, AbstractController, and AbstractAuthenticationProcessingFilter are all Template Method pattern examples.

Combining patterns: In practice, patterns are rarely used in isolation.

Examples of common combinations:

  • Strategy + Factory: A Factory creates the appropriate Strategy object based on configuration
  • Observer + Singleton: A singleton EventBus manages all application events
  • Builder + Factory: A Factory returns a fully-built object using a Builder internally
  • Proxy + Decorator: AOP proxies wrap business logic; decorators add behavior layers
// Spring combines patterns seamlessly:
// @Transactional = Proxy pattern
// JdbcTemplate = Template Method + Proxy
// @EventListener = Observer pattern
// @Bean factory methods = Factory pattern
@Service
public class OrderService {
@Transactional // Proxy wraps this method
public void placeOrder(Order order) {
// ...
}
}