Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 54

Strategy and Observer Patterns in Practice

Recommended resources

Hello! Welcome to another lesson on design patterns.

In our last session, we dove into the Decorator and Composite patterns. You learned how, despite their structural similarities, their intent is what truly defines them: Decorator for adding responsibilities dynamically, and Composite for building part-whole hierarchies. This focus on intent is a cornerstone of the architectural mindset you're developing.

Today, we transition from Structural patterns to Behavioral patterns. These patterns are concerned with algorithms and the assignment of responsibilities between objects, focusing on how they communicate.

Our learning outcome is to implement the Strategy and Observer behavioral patterns to manage algorithms and dependencies. We will explore how the Strategy pattern allows you to define a family of algorithms and make them interchangeable, and how the Observer pattern creates a clean way for objects to react to changes in others without being tightly coupled.

1. The Strategy Pattern: Encapsulating Algorithms

Imagine you are building an e-commerce application. The system needs to calculate shipping costs, but the calculation method varies. It might be based on a flat rate, the weight of the items, or the destination country. A common but problematic approach is to use a large if-else or switch statement inside the Order class.

// A problematic approach
public class Order {
    public double calculateShippingCost(String shippingMethod) {
        if (shippingMethod.equals("flat_rate")) {
            return 5.00;
        } else if (shippingMethod.equals("by_weight")) {
            // complex weight calculation...
            return getWeight() * 1.5;
        } else if (shippingMethod.equals("by_destination")) {
            // complex destination lookup...
            return lookupRate(getDestination());
        }
        // ... and more methods to come
    }
}

This design violates the Open/Closed Principle. Every time a new shipping method is added, you have to modify this Order class, which increases the risk of introducing bugs. How can we design this so that new algorithms can be added without changing the existing context?

The Strategy pattern offers a solution. It suggests defining a family of algorithms, encapsulating each one, and making them interchangeable.

Strategy

To understand the core idea, let's start with a clear, problem-focused explanation from the Refactoring.guru website.

Please read the 'Intent', 'Problem', and 'Solution' sections. The navigation app example perfectly illustrates the problem of a monolithic class that handles multiple, changing algorithms.

How it Works: Context, Strategy, and Concrete Strategies

The pattern moves the different algorithms into separate classes called strategies. The original class, called the context, holds a reference to a strategy object and delegates the work to it.

Class Diagram of Strategy Design Pattern (Sorting Example)
A UML diagram for the Strategy pattern using a sorting example. The `SortingContext` (Context) uses a `SortingStrategy` (Strategy interface) to perform its sort operation. The specific algorithm (`BubbleSortStrategy`, `QuickSortStrategy`) is a `ConcreteStrategy` that can be swapped at runtime.

The key participants are:

  • Context: The class that contains the business logic. It holds a reference to one of the strategy objects. In our example, this would be the Order class.
  • Strategy Interface: This is an interface common to all algorithms. It declares the method that the context will call to execute the algorithm (e.g., an IShippingStrategy interface with a calculate(order) method).
  • Concrete Strategies: These are the classes that implement the different algorithms. (e.g., FlatRateShipping, WeightBasedShipping).

The client code can then choose the appropriate strategy and pass it to the context. This decouples the context from the specific implementation details of its algorithms.

Strategy Pattern – Design Patterns (ep 1) | LOW LEVEL DESIGN | Techie007 | Sukhad Anand

This video from Sukhad Anand provides an excellent walkthrough. It starts by showing the flaws of an inheritance-based design for a payment system and then refactors it using the Strategy pattern. This process of identifying a problem and applying a pattern is central to software architecture.

Please watch from the beginning to 06:31. Focus on: The problems identified in the initial inheritance-based design (00:00 - 02:35): code duplication and forcing subclasses to implement unnecessary methods. How the Strategy pattern solves this by extracting behaviors (payment, authentication) into their own interfaces and concrete implementations (02:35 - 06:31).

Test your understanding!

You are designing a data compression utility. The user should be able to choose whether to compress files using ZIP, GZIP, or BZIP2 algorithms. How would you apply the Strategy pattern to this problem? Describe the Context, the Strategy interface, and at least two Concrete Strategies.

Show answer
  • Context: A CompressionContext class. It would have a method like compressFile(filePath) and would hold a reference to a compression strategy object.
  • Strategy Interface: An ICompressionStrategy interface with a single method, compress(file).
  • Concrete Strategies:
    • ZipCompressionStrategy: Implements ICompressionStrategy and contains the logic for compressing a file using the ZIP algorithm.
    • GzipCompressionStrategy: Implements ICompressionStrategy and contains the logic for GZIP compression.

The client code would instantiate CompressionContext and then set the desired strategy, for example: context.setStrategy(new ZipCompressionStrategy()), before calling context.compressFile(...).


2. The Observer Pattern: Managing Dependencies

Now let's turn to a different kind of problem. Imagine a social media application where users can post updates. When a user posts something, all of their followers should receive a notification.

A naive implementation might have the User object maintain a list of its followers and loop through them, directly calling a receiveNotification method on each Follower object.

This creates tight coupling. The User object (the subject) is tightly coupled to the Follower object (the observer). What if you want to introduce other types of observers, like a Dashboard that tracks user activity or an EmailService that sends email notifications? You would have to modify the User class every time.

The Observer pattern solves this by creating a subscription mechanism. It defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.

Observer

The Refactoring.guru article on the Observer pattern explains this concept clearly using a customer-and-store analogy.

Please read the 'Intent', 'Problem', 'Solution', and 'Structure' sections. Pay attention to the terminology: Publisher (or Subject) and Subscriber (or Observer).

How it Works: Publisher and Subscriber

The pattern introduces two key roles: a Publisher and a Subscriber. The Publisher maintains a list of subscribers and provides methods to add (attach) or remove (detach) subscribers from this list. When the Publisher's state changes, it iterates through its subscribers and calls a notification method on each one.

UML Class Diagram of Observer Design Pattern
A UML diagram of the Observer pattern. The `WeatherStation` (Concrete Subject) maintains a list of `Observer`s. When its state changes, it notifies all registered observers, like `PhoneDisplay` and `TVDisplay`, by calling their `update` method.

The participants are:

  • Publisher (or Subject) Interface: Declares methods for managing subscribers (e.g., attach, detach, notify).
  • Concrete Publisher: Implements the publisher interface. It maintains the state and a list of subscribers. When its state changes, it calls notify.
  • Subscriber (or Observer) Interface: Declares the notification method (e.g., update) that the publisher will call.
  • Concrete Subscribers: Implement the subscriber interface and define the action to be taken upon notification.

Because the publisher only communicates with subscribers via a common interface, it doesn't need to know anything about the concrete classes of its subscribers. This keeps the system loosely coupled and extensible.

Design Patterns in Java Every Developer Should Know | Factory, Observer, Strategy

This video from ByteMonk provides a concise implementation of the Observer pattern using a YouTube channel analogy, which is very intuitive.

Please watch from 02:57 to 05:40. Note how the NewPublisher (Concrete Subject) doesn't know or care what the EmailSubscriber (Concrete Observer) does. It simply calls the update method on all its registered observers.

This pattern is the foundation of many event-driven systems. In your experience as a full-stack developer, you've likely encountered it with JavaScript event listeners (addEventListener), where a DOM element (the publisher) fires an event, and anonymous functions (the observers) react to it.


3. Comparing Behavioral Patterns

Just like we compared Decorator and Composite, it's crucial to understand the subtle differences between these behavioral patterns to know when to apply them.

Pattern Intent Key Idea Analogy
Strategy To define a family of algorithms and make them interchangeable at runtime. The context delegates a task to a strategy object set by the client. Swapping SIM cards in a phone to change carriers.
Observer To create a subscription mechanism for one-to-many object notifications. The publisher notifies all its subscribers when its state changes. A magazine publisher sending issues to subscribers.
State To allow an object to alter its behavior when its internal state changes. The context delegates to a state object, and states can transition to other states. A traffic light changing its behavior (and color) based on its state (Green, Yellow, Red).

The Strategy and State patterns are structurally similar, but their intent differs. In Strategy, the client usually provides the context with the strategy object. In State, the context or the state objects themselves decide on the state transitions.

The Observer pattern is different. Its goal is not to encapsulate an algorithm but to create a communication channel that decouples the sender (Publisher) from the receivers (Subscribers).


Conclusion

In this lesson, you've explored two fundamental behavioral design patterns that are essential for building flexible and maintainable systems.

Key Takeaways:

  • The Strategy pattern is used to manage a family of interchangeable algorithms. It promotes the Open/Closed Principle by allowing you to add new algorithms without modifying the context class that uses them.
  • The Observer pattern provides a powerful mechanism for creating one-to-many dependencies between objects. It decouples the subject from its observers, enabling you to build highly responsive, event-driven systems.
  • Both patterns are about delegating responsibility, but their intents are distinct: Strategy is about how something is done, while Observer is about notifying others that something has happened.

Preview of the Next Lesson:
We've now covered a range of Creational, Structural, and Behavioral patterns. In our next lesson, we'll put your knowledge to the test. You will be challenged to select and justify an appropriate design pattern for a given design problem. This exercise will require you to analyze requirements and use your architectural judgment to choose the best pattern, or combination of patterns, for the job.