In the last episode of this Design Patterns series we looked at the very simple Singleton. Now we’ll turn our attention to the frequently used and extremely useful Observer pattern. And of course you can instantly generate an Observer of your own by going to the PlantText Samples window and choosing “Design Patterns” and “Observer.”
What
Refactoring Guru describes the Observer as “a behavioral design pattern that lets you define a subscription mechanism to notify multiple objects about any events that happen to the object they’re observing.” So it watches and then notifies. Well this is a pretty darn common situation. This pattern should be used all over the place, right?!
Right! You should probably use the Observer pattern a lot. Luckily it is simple and helps you achieve good object oriented design. It just requires creating at least one Observer class and two interfaces (ISubject & IObserver). Then implement a bit of code in the Subject class to give the observer access to the state of the Subject any time a trigger fires to notify the observer. Easy peasy.
Why
So, why would we us the Observer pattern? The Observer comes in handy when you have one or more objects (observers) that need to be notified when another object (subject) changes state.
Let’s face it: lots of software is a disaster waiting to happen. Every time you need to add a new feature, it feels like you’re walking through a minefield, terrified something else will break. Why? Because you’re cramming too much into one place, using composition in ways that violate core design principles like striving for low coupling, high cohesion, separation of concerns, and the single responsibility principle. Hence the need for the Observer Design Pattern. You’ll see what I mean below as I dig into this in more detail.
There are some cases, however where you should not use the Observer pattern! You may decide NOT to use it in high performance critical sections of your code in order to minimize any overhead. And one-time notifications don’t require the Observer either…a single action would be overkill for this pattern. Also, try not to use it in simple direct communications, working with static data, and/or especially if you should be using a more complex dependency management pattern, like the Mediator or Dependency Injection patterns.
When
This pattern is particularly useful when you want to establish a one-to-many relationship between objects, where changes in one object (the subject) should automatically trigger updates in other dependent objects (observers). Here are a few real-world examples on when the Observer pattern may come in handy:
- GUI Frameworks with events like button clicks or key presses need to notify multiple event listeners. In a GUI framework like Java’s Swing, when a user interacts with a button, all objects interested in that interaction (e.g., methods to handle the event) are automatically notified. The Observer pattern helps decouple the button (subject) from specific actions (observers) that are triggered by user events.
- A stock market application that displays a real-time stock ticker. The stock price server acts as the subject, and the dashboards, charts, or other components displaying the stock prices are the observers. When the stock prices update, the server notifies all observers so they can refresh and display the latest data.
- Logging services in an application, such as errors, warnings, or other information, to different locations (for example to the console, a file, or a remote server). The subject in this case is the event being logged, and the observers are the logging mechanisms. When an event occurs, the event is broadcast to all loggers to write to the appropriate destination.
- Social media notifications. Say users want to be notified when someone they follow posts new content. Each user profile can be the subject, and their followers act as observers. Whenever the user posts new content, all their followers are notified.
- MVC (Model-View-Controller) Architecture. The model (subject) represents the data of the application, and the view (observer) is the user interface. When the model changes, all associated views are notified to refresh and display the new state of the data.
These are all good examples of using the Observer pattern to decouple your code and create more separation of concerns. Of course we are trying to build software that embodies the concepts of high cohesion and low coupling. This is a great pattern for doing exactly that!
How
You can have as many observers subscribe to a subject as you need. Using interfaces ensures that if you need to add new observers in the future that you haven’t thought of yet, you’ll already have a contract in place that sets the ground rules.
So let’s look at a specific example and some C# code that does not use the observer pattern. The problem below is composition overload and tight coupling:
public class Button { private Popup popup = new Popup(); private LoggingService loggingService = new LoggingService(); public void Click() { Console.WriteLine("Button clicked"); popup.Show("Button was clicked!"); loggingService.Log("Button was clicked!"); } }
This setup might seem straightforward, but it’s problematic for several reasons. The Button class directly interacts with both the Popup and LoggingService classes. Whenever a button click occurs, it triggers both a popup message and a log entry. This approach has several issues:
- Lack of Separation of Concerns: The button’s logic is tangled with the UI management and logging concerns. Changes in the popup or logging behavior could require modifications in the Button class, which should be avoided.
- Tight Coupling: The Button class is tightly coupled with both the Popup and LoggingService classes. If you need to change how popups or logging work, you must modify the Button class. This makes your code hard to maintain and extend as it gets more complex.
- Violation of the Single Responsibility Principle: The Button class is handling more than one responsibility. It manages user interactions (clicks) and also controls UI elements (popups) and logging. This overloading makes the Button class complex and difficult to manage.
Here’s how to refactor your code using the Observer Pattern to achieve a cleaner, more modular design:
// The Button class now only knows about observers and can notify them. public class Button { private List<IObserver> observers = new List<IObserver>(); public void Attach(IObserver observer) { observers.Add(observer); } public void Notify(string message) { foreach (var observer in observers) { observer.Update(message); } } public void Click() { Notify("Button clicked"); } } // The IObserver interface defines the contract for all observers. public interface IObserver { void Update(string message); } // The Popup class implements IObserver and handles its own updates. public class Popup : IObserver { public void Update(string message) { Console.WriteLine($"Popup shown with: {message}"); } } // The LoggingService class implements IObserver and handles its own updates. public class LoggingService : IObserver { public void Update(string message) { Console.WriteLine($"Log: {message}"); } } // Example of using the Button with observers. public class Program { public static void Main() { Button button = new Button(); Popup popup = new Popup(); LoggingService loggingService = new LoggingService(); button.Attach(popup); button.Attach(loggingService); button.Click(); // This will notify both the Popup and LoggingService to handle the message. } }
So, notice this is how the new code works:
- Button Class: The Button class is now only responsible for handling button clicks and notifying its observers. It maintains a list of IObserver instances and uses the Notify method to send messages to all attached observers.
- Observer Interface: The IObserver interface defines a single method, Update, that all observers must implement. This standardizes how observers handle notifications.
- Popup Class: The Popup class implements the IObserver interface and defines how it reacts to notifications. It independently handles displaying messages based on notifications it receives.
- LoggingService Class: The LoggingService class also implements IObserver and defines how it logs messages. It independently handles logging based on notifications it receives.
- Program Class: In the Main method, we create instances of Button, Popup, and LoggingService. Both the Popup and LoggingService are attached to the Button as observers. When the button is clicked, it sends a notification to both the Popup and LoggingService, which handle the message according to their responsibilities.
Why this approach is better:
- Low Coupling: The Button class doesn’t need to know about the specifics of how the Popup or LoggingService works. It simply notifies its observers and lets them handle the response. This makes your system more flexible and easier to manage.
- High Cohesion: Each class now focuses on a single responsibility. The Button handles click events and notification delegation, the Popup handles message display, and the LoggingService manages logging. This clear separation of duties makes the code cleaner and more maintainable.
- Separation of Concerns: The Button class deals with button clicks and notifications, while the Popup and LoggingService handle their own specific concerns. This separation allows you to modify or extend each component independently without affecting the others.
- Single Responsibility Principle: Each class adheres to the single responsibility principle. The Button class deals with button clicks and delegating notifications, the Popup class handles displaying messages, and the LoggingService class manages logging. This modular approach simplifies maintenance and enhances clarity.
- Extensibility: Adding new observers is straightforward. You can create additional classes that implement IObserver, attach them to the Button, and they will handle notifications independently. No changes are needed to the Button class or existing observers.
The Bottom Line
Abusing composition to handle all responsibilities in a single class results in tight coupling, low maintainability, and messy code. The Observer Pattern provides a cleaner, more modular approach by decoupling components and adhering to core design principles like low coupling, high cohesion, separation of concerns, and the single responsibility principle. By adopting this pattern, you’ll create a more flexible, maintainable, and scalable system. Your future self will thank you for it! Cheers!
For More on the Observer
- The Refactoring Guru has a great article on the Observer
- Observer design pattern by dofactory
- Gang of Four Observer pattern