Implementing Strategy and Observer Patterns in Java and TypeScript
Implementing the Strategy and Observer patterns involves replacing rigid conditional logic with polymorphic interfaces. The Strategy pattern encapsulates interchangeable algorithms to avoid complex if-else or switch blocks, while the Observer pattern creates a one-to-many dependency between objects so that state changes in one object automatically notify all its dependents.
Implementing Strategy and Observer Patterns in Java and TypeScript
Design patterns are not merely academic exercises; they are proven templates for solving recurring software engineering problems. When a codebase grows, hard-coded logic becomes a liability. Transitioning to behavioral patterns like Strategy and Observer allows developers to adhere to the Open/Closed Principle—where software entities are open for extension but closed for modification.
Key Takeaways
- Strategy Pattern: Eliminates conditional sprawl by encapsulating logic into separate classes.
- Observer Pattern: Decouples the subject from its observers, enabling reactive system architectures.
- Java Implementation: Relies heavily on Interfaces and Generics for type safety.
- TypeScript Implementation: Leverages Interfaces and optional chaining for flexible, lightweight event handling.
- Scalability: Both patterns reduce technical debt by isolating changes to specific strategy or observer implementations.
The Strategy Pattern: Moving Beyond Conditional Logic
The Strategy pattern is used when you have multiple ways to perform a specific task and want to switch between them at runtime without altering the client code.
The Problem: The "Conditional Nightmare"
Many developers begin by using large switch or if-else blocks to handle different business rules. For example, a payment processor might check if a user chose "Credit Card," "PayPal," or "Bitcoin." Every time a new payment method is added, the core processing logic must be modified, increasing the risk of regression bugs.
The Solution: Encapsulated Algorithms
Instead of one massive method, the Strategy pattern defines a common interface for all supported algorithms. The client then holds a reference to this interface and delegates the work to the concrete implementation.
Java Implementation
In Java, the Strategy pattern is best implemented using a functional interface if the strategy consists of a single method.
// The Strategy Interface
public interface PaymentStrategy {
void collectPaymentDetails();
boolean validatePayment();
void pay(int amount);
}
// Concrete Strategy A
public class CreditCardPayment implements PaymentStrategy {
public void collectPaymentDetails() { /* Logic for CC */ }
public boolean validatePayment() { return true; }
public void pay(int amount) { System.out.println("Paid " + amount + " via Credit Card"); }
}
// Concrete Strategy B
public class PayPalPayment implements PaymentStrategy {
public void collectPaymentDetails() { /* Logic for PayPal */ }
public boolean validatePayment() { return true; }
public void pay(int amount) { System.out.println("Paid " + amount + " via PayPal"); }
}
// The Context
public class PaymentProcessor {
private PaymentStrategy strategy;
public void setPaymentStrategy(PaymentStrategy strategy) {
this.strategy = strategy;
}
public void processOrder(int amount) {
strategy.pay(amount);
}
}
TypeScript Implementation
TypeScript allows for a more concise implementation using interfaces and type aliases, making it ideal for frontend state management or backend API routing.
interface PaymentStrategy {
pay(amount: number): void;
}
class CreditCardPayment implements PaymentStrategy {
pay(amount: number): void {
console.log(`Paid ${amount} using Credit Card`);
}
}
class PayPalPayment implements PaymentStrategy {
pay(amount: number): void {
console.log(`Paid ${amount} using PayPal`);
}
}
class PaymentProcessor {
private strategy!: PaymentStrategy;
setStrategy(strategy: PaymentStrategy) {
this.strategy = strategy;
}
process(amount: number) {
this.strategy.pay(amount);
}
}
The Observer Pattern: Building Reactive Systems
The Observer pattern is a behavioral design pattern that defines a subscription mechanism to notify multiple objects about any events that happen to the object they are observing.
The Problem: Tight Coupling and Polling
Without the Observer pattern, a "subscriber" object must constantly poll the "subject" object to check for state changes. This wastes CPU cycles and creates a tight coupling where the subject must know exactly which objects need the update, making the system fragile and difficult to scale.
The Solution: The Pub-Sub Model
The Observer pattern introduces a "Subject" (or Observable) that maintains a list of "Observers." When the subject's state changes, it iterates through the list and calls a notification method on each observer.
Java Implementation
Java provides a robust way to handle this via a List of interfaces.
import java.util.ArrayList;
import java.util.List;
interface Observer {
void update(float price);
}
class StockTicker {
private List<Observer> observers = new ArrayList<>();
private float price;
public void addObserver(Observer observer) {
observers.add(observer);
}
public void setPrice(float price) {
this.price = price;
notifyObservers();
}
private void notifyObservers() {
for (Observer observer : observers) {
observer.update(price);
}
}
}
class MobileAppObserver implements Observer {
public void update(float price) {
System.out.println("Mobile App: Stock price updated to " + price);
}
}
TypeScript Implementation
In TypeScript, the Observer pattern is frequently used in UI frameworks (like Angular or RxJS) to synchronize the view with the data model.
interface Observer {
update(data: any): void;
}
class Subject {
private observers: Observer[] = [];
subscribe(observer: Observer): void {
this.observers.push(observer);
}
unsubscribe(observer: Observer): void {
this.observers = this.observers.filter(obs => obs !== observer);
}
notify(data: any): void {
this.observers.forEach(obs => obs.update(data));
}
}
class LoggerObserver implements Observer {
update(data: any): void {
console.log(`Log: Received update with data ${data}`);
}
}
Side-by-Side Comparison: Conditional Logic vs. Design Patterns
The primary value of these patterns is the transition from "procedural" thinking to "object-oriented" thinking.
| Feature | Conditional Logic (Anti-pattern) | Strategy/Observer Pattern |
|---|---|---|
| Adding New Features | Requires modifying existing core methods. | Requires adding a new class. |
| Testing | Requires testing the entire method for all cases. | Allows unit testing of individual strategies. |
| Coupling | High: The main class knows all implementations. | Low: The main class only knows the interface. |
| Readability | Decreases as if/else chains grow. |
Remains constant regardless of the number of options. |
For developers looking to refine these habits, learning How to Implement Common Design Patterns in Modern Code provides a broader context on when to apply these structures across different architectural layers.
When to Use Which Pattern?
Choosing the wrong pattern can lead to "over-engineering," where the code becomes more complex than the problem it solves.
Use the Strategy Pattern when:
- You have multiple versions of an algorithm.
- You need to switch algorithms at runtime based on user input or configuration.
- You want to isolate business logic from the class that uses it.
- You find yourself writing a
switchstatement that handles different "types" of a behavior.
Use the Observer Pattern when:
- A change to one object requires changing others, and you don't know how many objects need to change.
- An object should be able to notify other objects without making assumptions about who those objects are.
- You are building event-driven systems, such as notification engines or real-time dashboards.
Performance and Memory Considerations
While design patterns improve maintainability, they introduce a small amount of overhead due to increased object allocation and indirect method calls (polymorphism).
- Memory Overhead: Each concrete strategy or observer is a separate object. In high-frequency trading or embedded systems, this may lead to increased garbage collection pressure in Java.
- Time Complexity: The time complexity of the Strategy pattern remains $O(1)$ for the call. The Observer pattern is $O(n)$, where $n$ is the number of subscribers.
- Optimization: To mitigate performance hits, developers can use the Singleton pattern for stateless strategies, ensuring only one instance of a specific algorithm exists across the application. This aligns with the broader goal of knowing How to Optimize Code Performance by reducing unnecessary object instantiation.
Integration with Modern Frameworks
Modern software engineering often abstracts these patterns into framework-level features.
- Spring Framework (Java): Dependency Injection (DI) is essentially a managed Strategy pattern. By injecting a bean of an interface type, Spring allows you to swap implementations via configuration without changing the consuming class.
- Redux/Vuex (TypeScript/JS): These state management libraries are evolved versions of the Observer pattern. The "Store" acts as the Subject, and the UI components act as Observers that re-render when the state changes.
Conclusion: Building Scalable Software
The transition from writing scripts to engineering software requires a shift toward modularity. By implementing the Strategy pattern, you decouple "what" needs to be done from "how" it is done. By implementing the Observer pattern, you decouple "when" something happens from "who" needs to know about it.
CodeAmber encourages developers to move beyond basic syntax and master these architectural blueprints. Integrating these patterns not only makes your code more professional and maintainable but also demonstrates a level of technical maturity that is highly valued in senior engineering roles and technical interviews. For those preparing for such transitions, focusing on Coding Interview Preparation often involves applying these exact patterns to optimize algorithmic solutions.