In this article, we are going to talk about a behavioral design pattern, the Observer Pattern. We are going to learn what problem this pattern solves and how we can implement it in C#.
If you want to read more about design patterns in C#, you can inspect our C# Design Patterns page.
Let’s start.
About the Observer Design Pattern
The Observer design pattern allows us to establish a notification mechanism between objects. It enables multiple objects to subscribe to another object and get notified when an event occurs to this observed object. So, on one hand, we have a Provider (sometimes called a Subject or a Publisher) which is the observed object. On the other hand, there are one or more Observers, which are objects subscribing to the Provider. An Observer can subscribe to a Provider and get notified whenever a predefined condition happens. This predefined condition is usually an event or a state change.
When to Use Observer Design Pattern
This pattern is helpful whenever we want to implement some kind of distributed notification system within our application. Let’s say we have an e-commerce application, where some customers are interested in the products of a particular seller. So, instead of checking for new products every now and then, they can just subscribe to the seller and receive real-time updates. The Observer pattern allows us to do so in a distributed manner, where any customer can subscribe or unsubscribe to any seller.
Implementing Observer Design Pattern in C#
C# has two powerful interfaces that are designed particularly to implement the Observer pattern: IObserver<T>
and IObservable<T>
. For an object to be a Provider it has to implement IObservable<T>
, while to be an Observer it has to implement IObserver<T>
; where T
is the type of notification object sent from the Provider to its Observers.
Let’s say we are developing a submission system for a company where applicants can apply for jobs. So, we want to notify HR specialists whenever a new applicant applies for a job.
To start, let’s define our first main model; Application
:
public class Application { public int JobId { get; set; } public string ApplicantName { get; set; } public Application(int jobId, string applicantName) { JobId = jobId; ApplicantName = applicantName; } }
Next, let’s define HRSpecialist
class:
public class HRSpecialist { public string Name { get; set; } public List<Application> Applications { get; set; } public HRSpecialist(string name) { Name = name; Applications = new(); } public void ListApplications() { if(Applications.Any()) foreach (var app in Applications) Console.WriteLine($"Hey, {Name}! {app.ApplicantName} has just applied for job no. {app.JobId}"); else Console.WriteLine($"Hey, {Name}! No applications yet."); } }
This class has a list of applications and a method that lists all these applications if any.
To continue, let’s create another class that will act as a repository for the applications:
public class ApplicationsHandler { private readonly List<IObserver<Application>> _observers; public List<Application> Applications { get; set; } public ApplicationsHandler() { _observers = new(); Applications = new(); } }
This class, which will act as our Provider, maintains two collections:
Applications
– a list of the submitted applications_observers
– a list of objects that will receive a notification when we receive a new application
Implementing the Provider
As we’ve mentioned, the Provider has to implement IObservable<Application>
. So let’s do it by modifying the ApplicationsHandler
class:
public class ApplicationsHandler : IObservable<Application> { private readonly List<IObserver<Application>> _observers; public List<Application> Applications { get; set; } public ApplicationsHandler() { _observers = new(); Applications = new(); } public IDisposable Subscribe(IObserver<Application> observer) { if (!_observers.Contains(observer)) { _observers.Add(observer); foreach (var item in Applications) observer.OnNext(item); } return new Unsubscriber(_observers, observer); } public void AddApplication(Application app) { Applications.Add(app); foreach (var observer in _observers) observer.OnNext(app); } public void CloseApplications() { foreach (var observer in _observers) observer.OnCompleted(); _observers.Clear(); } }
We implement the Subscribe
method, which takes a reference to an IObserver<Application>
implementation, adds it to the Observers
collection, and provides it with existing data by calling its OnNext
method. The Subscribe
method returns an IDisposable
implementation that allows the observer to unsubscribe. We are going to create the Unsubscriber
class in a moment.
We also create the AddApplication
method, which adds an application and notifies each observer by calling its OnNext
method. The CloseApplications
method calls OnCompleted
of each observer to notify them that there are no more upcoming applications.
Our Provider is almost ready to send notifications, let’s just define Unsubscriber
:
public class Unsubscriber : IDisposable { private readonly List<IObserver<Application>> _observers; private readonly IObserver<Application> _observer; public Unsubscriber(List<IObserver<Application>> observers, IObserver<Application> observer) { _observers = observers; _observer = observer; } public void Dispose() { if (_observers.Contains(_observer)) _observers.Remove(_observer); } }
Our Provider is ready. Now, let’s configure HRSpecialist
to be able to subscribe to ApplicationsHandler
.
Implementing the Observer
Let’s implement IObserver<Application>
by adding a single field and a few more methods to the Observer HRSpecialist
class:
public class HRSpecialist : IObserver<Application> { private IDisposable _cancellation; // previous code public virtual void Subscribe(ApplicationsHandler provider) { _cancellation = provider.Subscribe(this); } public virtual void Unsubscribe() { _cancellation.Dispose(); Applications.Clear(); } public void OnCompleted() { Console.WriteLine($"Hey, {Name}! We are not accepting any more applications"); } public void OnError(Exception error) { // This is called by the provider if any exception is raised, no need to implement it here } public void OnNext(Application value) { Applications.Add(value); } }
We implement the interface’s three methods: OnNext
which receives the notification, OnError
which handles any exception raised, and OnCompleted
which indicates that there are no more upcoming notifications. These methods are called by the Provider, as we’ve seen in ApplicationsHandler
.
We also add Subscribe
method, which calls the Provider’s Subscribe
method and assigns the returned Unsubscriber
object to _cancellation
. This is used by the Unsubscriber
method to unsubscribe.
That’s it, we are ready to test our implementation. Let’s head to Main
and play around with it:
class Program { static void Main(string[] args) { var observer1 = new HRSpecialist("Bill"); var observer2 = new HRSpecialist("John"); var provider = new ApplicationsHandler(); observer1.Subscribe(provider); observer2.Subscribe(provider); provider.AddApplication(new(1, "Jesus")); provider.AddApplication(new(2, "Dave")); observer1.ListApplications(); observer2.ListApplications(); observer1.Unsubscribe(); Console.WriteLine(); Console.WriteLine($"{observer1.Name} unsubscribed"); Console.WriteLine(); provider.AddApplication(new(3, "Sofia")); observer1.ListApplications(); observer2.ListApplications(); Console.WriteLine(); provider.CloseApplications(); } }
We create a provider and two observers: Bill and John. Both subscribe to the provider. We add some applications and expect both observers to receive them as well. Bill unsubscribes, he will not receive any more applications. Finally, the provider stops receiving applications, notifies the subscribers (only John in this case), and cancels all subscriptions.
The final output demonstrates this mechanism:
Hey, Bill! Jesus has just applied for job no. 1 Hey, Bill! Dave has just applied for job no. 2 Hey, John! Jesus has just applied for job no. 1 Hey, John! Dave has just applied for job no. 2 Bill unsubscribed Hey, Bill! No applications yet. Hey, John! Jesus has just applied for job no. 1 Hey, John! Dave has just applied for job no. 2 Hey, John! Sofia has just applied for job no. 3 Hey, John! We are not accepting any more applications
Conclusion
In this article, we have learned why and when to use Observer Design Pattern and we have learned how to implement it with our example app using C#.