Learn about Observer Pattern – a design pattern that enables objects to automatically receive notifications when changes occur, just like subscribing to a YouTube channel.
No table of contents available for this article
Observer Pattern is a design pattern belonging to the Behavioral Pattern category. This pattern defines a one-to-many dependency relationship between objects: when one object changes its state, all dependent objects are automatically notified and updated.
The remarkable thing is that an object can notify an unlimited number of other objects without needing to know specifically who they are.
Real-world example:
Imagine you subscribe and turn on notifications for your favorite YouTube channel. Whenever that channel uploads a new video (state change), YouTube automatically sends notifications to all subscribers. You don't need to constantly check if there's a new video – the system "pushes" information to you.
Observer Pattern works exactly the same way in code!
Observer Pattern consists of the following main components:
Publisher (Subject): This is the object being "observed." The Publisher maintains a list of Subscribers and is responsible for notifying all of them when an event occurs. When a new event happens, the Publisher iterates through the subscription list and calls the notification method on each Subscriber.
Subscriber Interface: This is the interface that the Publisher uses to communicate with Subscribers. In most cases, this interface only needs a single method: update().
Concrete Subscriber (SubscriberA, SubscriberB...): Specific classes that implement the Subscriber Interface. Each Concrete Subscriber defines its own action when receiving notifications from the Publisher. For example: SubscriberA might log data, while SubscriberB sends an email.
Typically, Subscribers need some context data to process updates correctly. The Publisher can pass this data as a parameter to the notification method, or even pass itself (this) so Subscribers can fetch any required data.
Client: The code that uses Observer Pattern. The Client creates Subscribers and registers them with appropriate Publishers. The great thing is that the Subscriber list is dynamically managed – objects can start or stop listening to notifications at any time during runtime.
A good design often separates subscription management logic into a separate object, making it easy to add new Subscribers without modifying Publisher code.
Advantages:
Follows the Open/Closed Principle (OCP): You can add new Subscribers without modifying the Publisher or other Subscribers. Subject and Observer can change independently and be reused separately.
Establishes relationships between objects at runtime: No need to "hard-code" connections at compile time. This provides tremendous flexibility.
Loose coupling: State changes in one object are communicated to other objects without them needing to "know each other" too closely.
No limit on Observer count: A Publisher can serve 1, 10, or 1000 Subscribers equally well.
Disadvantages:
Unexpected updates: Since Observers don't know about each other's existence, modifying the Subject might trigger a chain of expensive updates you didn't anticipate.
No guaranteed notification order: Subscribers are notified in random order (or list order), which can cause issues if your logic depends on processing sequence.
Observer Pattern is suitable when:
You need to notify multiple objects about state changes without creating tight coupling between them.
Your project needs easy extensibility with minimal code changes. Adding a new Subscriber type? No problem!
When an abstraction has two interdependent aspects. Encapsulating them in separate objects allows independent modification and reuse.
When changing one object requires changing others, but you don't know in advance how many objects need updating.
When an object needs to notify other objects without knowing specifically who they are – this is the essence of loose coupling.
Here are the systematic steps to build Observer Pattern:
Step 1: Analysis and Design
Review application logic and divide into two parts: core functionality (acting as Publisher) and reactive components (acting as Subscribers).
Step 2: Declare Subscriber Interface
csharp
public interface IObserver
{
void Update(ISubject subject);
}This interface defines the "contract" that every Subscriber must follow. The Update() method is called whenever there's a notification.
Step 3: Declare Publisher Interface
csharp
public interface ISubject
{
void Attach(IObserver observer);
void Detach(IObserver observer);
void Notify();
}The Publisher Interface defines methods for managing Subscribers: add (Attach), remove (Detach), and notify (Notify).
Step 4: Implement Concrete Publisher
csharp
public class Subject : ISubject
{
public int State { get; set; } = 0;
private List<IObserver> _observers = new List<IObserver>();
public void Attach(IObserver observer)
{
Console.WriteLine("Subject: Attached an observer.");
this._observers.Add(observer);
}
public void Detach(IObserver observer)
{
this._observers.Remove(observer);
Console.WriteLine("Subject: Detached an observer.");
}
public void Notify()
{
Console.WriteLine("Subject: Notifying observers...");
foreach (var observer in _observers)
{
observer.Update(this);
}
}
public void SomeBusinessLogic()
{
Console.WriteLine("\nSubject: I'm doing something important.");
this.State = new Random().Next(0, 10);
Thread.Sleep(15);
Console.WriteLine("Subject: My state has just changed to: " + this.State);
this.Notify();
}
}The Subject class maintains an Observer list and notifies everyone when the state changes. Note that the Publisher passes itself (this) to the Update() method so Subscribers can access required data.
Step 5: Implement Concrete Subscribers
csharp
class ConcreteObserverA : IObserver
{
public void Update(ISubject subject)
{
if ((subject as Subject).State < 3)
{
Console.WriteLine("ConcreteObserverA: Reacted to the event.");
}
}
}
class ConcreteObserverB : IObserver
{
public void Update(ISubject subject)
{
if ((subject as Subject).State == 0 || (subject as Subject).State >= 2)
{
Console.WriteLine("ConcreteObserverB: Reacted to the event.");
}
}
}Each Concrete Observer reacts differently based on Subject's state. Observer A only reacts when state < 3, while Observer B reacts when state = 0 or state >= 2.
Step 6: Client Code
csharp
class Program
{
static void Main(string[] args)
{
var subject = new Subject();
var observerA = new ConcreteObserverA();
subject.Attach(observerA);
var observerB = new ConcreteObserverB();
subject.Attach(observerB);
subject.SomeBusinessLogic();
subject.SomeBusinessLogic();
subject.Detach(observerB);
subject.SomeBusinessLogic();
}
}The Client creates the Subject and Observers, then registers them. When SomeBusinessLogic() is called, all registered Observers receive notifications.
Chain of Responsibility, Command, Mediator, and Observer all solve the problem of connecting senders and receivers, but in different ways:
Chain of Responsibility: Passes requests sequentially through a chain of handlers until one handles it.
Command: Establishes one-way connection between sender and receiver, encapsulating requests as objects.
Mediator: Eliminates direct connections, forcing components to communicate through an intermediary object.
Observer: Allows receivers to dynamically subscribe/unsubscribe to receive notifications.
Comparing Mediator and Observer:
These two patterns are quite similar in many situations. The main difference lies in their goals:
Mediator focuses on eliminating interdependencies between components – all depend on a single mediator.
Observer focuses on establishing dynamic one-way connections – some objects "subscribe" to receive updates from others.
If you found this article helpful, explore more in the Design Patterns Series to enhance your programming skills!
References
[1] Refactoring.Guru. https://refactoring.guru/design-patterns
[2] Design Patterns for Dummies, Steve Holzner, PhD
[3] Head First Design Patterns, Eric Freeman
[4] Gang of Four Design Patterns 4.0
[5] Dive into Design Patterns