you can see from the above that the observable notifies all the things that are observing it that something of interest has occurred within the context of the observable, this notification will invoke a function that is defined within the observers.
Before we dive into the UML of the observer pattern let's take a look at a very simple implementation using an event handler.
using System;
namespace pav.ObserverPattern {
class Program {
static void Main(string[] args) {
var observable = new Observable();
var o1 = new Observer
{ Name = "Observer one" };
var o2 = new Observer
{ Name = "Observer two" };
var o3 = new Observer
{ Name = "Observer three" };
observable.Notify += o1.Notified;
observable.Notify += o2.Notified;
observable.Notify += o3.Notified;
observable.NotifiyObservers();
}
}
class Observable {
public event
EventHandler Notify;
internal void
NotifiyObservers() => Notify?.Invoke(this, new EventArgs());
}
class Observer {
public string Name { get; set; }
public void
Notified(object sender, EventArgs args) => Console.WriteLine($"{Name} was
notified");
}
}
Above we see that we have an observable class that has a public field of the event type. This is the reference that the observable keeps to the observers execute methods. When the observable invokes the notify event it will execute all the registered methods of the observers.
Now if you are familiar with delegates then this may look very familiar however the two main differences between events and delegates is that events can not be overridden or invoked from outside of their containing class.
Next let's look at a more structured approach to the observer pattern; c# has predefined generic interfaces for both an observable and its observers.
In this version of the observer pattern there are two main interfaces to realize and those are the IObservable and IObserver, as mentioned above the observable notifies all of the observers that something happened. The IObservable interface only defines one function "Subscribe" and this function returns an implementation of the IDisposable interface, this is meant to create a reference to each observer that can then be used to detach the observer from the observers notification list. As the "Subscribe" function name implies this is also where our observers are registered for notifications.
So let's implement these interfaces
now this looks like a shit show; and it doesn't even contain our application which holds references to the Observable and its Observers, so let's try and make some sense of it. The Observable keeps a list of observers that are "watching" it; for each observer that is Subscribed the Observable returns an unsubscriber object that the observer holds a reference to and uses to detach itself from the observable notification list.
Enough Jibber Jabber, let's get started. Firstly let's set up our Observer, because it's the simplest
That's all there's to that, the IObserver<T> interface defines three methods:
So let's implement these interfaces
now this looks like a shit show; and it doesn't even contain our application which holds references to the Observable and its Observers, so let's try and make some sense of it. The Observable keeps a list of observers that are "watching" it; for each observer that is Subscribed the Observable returns an unsubscriber object that the observer holds a reference to and uses to detach itself from the observable notification list.
Enough Jibber Jabber, let's get started. Firstly let's set up our Observer, because it's the simplest
class Observer<T> : IObserver<T>
{
IDisposable unsubscriber;
public void
OnCompleted() => unsubscriber.Dispose();
public void
OnError(Exception error) => Console.WriteLine(error.Message);
public void OnNext(T
value) => Console.WriteLine(value);
public virtual void Subscribe(IObservable<T>
observable) =>unsubscriber = observable?.Subscribe(this);
}
That's all there's to that, the IObserver<T> interface defines three methods:
- OnCompleted() which is generally used to detach the observer from the observable
- OnError(error) which is used to let observer know that some sort of error occurred within the observable
- OnNext(value) which is used execute some sort of logic when then observable signals that something occurred
The Observer class holds a reference to an IDisposable object which facilitates the detachment from the Observable as well as a "Subscribe" method that lets the Observer become linked with the Observerable and get that reference to the Unsubscriber.
next let's take a look at the observable
class Observable<T> : IObservable<T>
{
IList<IObserver<T>>
observers = new List<IObserver<T>>();
public IDisposable Subscribe(IObserver<T>
observer)
{
if (!observers.Contains(observer))
observers.Add(observer);
return new Unsubscriber<T>(observer,
observers);
}
public void
NotifyObservers(T message)
{
foreach (var observer in observers)
observer.OnNext(message);
}
private class Unsubscriber<T> : IDisposable
{
//removed for
brevity
}
}
The IObserverable<T> interface only defines one function which is meant to store a reference to the observers and returns that unsubscriber class that implements IDisposable. We provide our Observable with a "NotifyObservers" method which is used to pass a message on to whomever is listening/watching.
Above we left the Unsubscriber nested class empty as not to clutter the Observable, this class is nested because it's existence only makes sense within the context of the Observable.
private class Unsubscriber<T> : IDisposable
{
IList<IObserver<T>> observers;
IObserver<T> observer;
public Unsubscriber(IObserver<T> observer, IList<IObserver<T>> observers)
{
this.observer = observer;
this.observers = observers;
}
public void Dispose() => observers?.Remove(observer);
}
next let's put it all together with a simple console application to execute our example
using System;
using System.Collections.Generic;
namespace pav.ObserverPatternSimple
{
class Observer<T> :
IObserver<T>
{
IDisposable unsubscriber;
public void
OnCompleted() => unsubscriber.Dispose();
public void
OnError(Exception error) => Console.WriteLine(error.Message);
public void OnNext(T
value) => Console.WriteLine(value);
public virtual void Subscribe(IObservable<T>
observable)
=> unsubscriber = observable?.Subscribe(this);
}
class Observable<T> :
IObservable<T>
{
IList<IObserver<T>>
observers = new
List<IObserver<T>>();
public IDisposable Subscribe(IObserver<T> observer)
{
if (!observers.Contains(observer))
observers.Add(observer);
return new
Unsubscriber<T>(observer, observers);
}
public void
NotifyObservers(T message)
{
foreach (var observer in observers)
observer.OnNext(message);
}
private class Unsubscriber<T> : IDisposable
{
IList<IObserver<T>>
observers;
IObserver<T> observer;
public Unsubscriber(IObserver<T> observer, IList<IObserver<T>>
observers)
{
this.observer = observer;
this.observers = observers;
}
public void
Dispose() => observers?.Remove(observer);
}
}
class Program
{
static void Main(string[] args)
{
var observable = new Observable<string>();
var observers = new
List<Observer<string>>
{ new Observer<string>(), new Observer<string>(), new Observer<string>()
};
};
foreach (var observer in observers)
observer.Subscribe(observable);
observable.NotifyObservers("One");
observers[1].OnCompleted();
observable.NotifyObservers("Two");
}
}
}
now as you can see form the above we create an observable and a collection of observers. we subscribe the observable to the observers and call our notification method on our observable.
As you can see the first time around each of our observers writes it's message to the console, however the second time around when we call the "OnComplete" method on our second observer we only get two messages because it used the Unsubscriber to detach itself from the observables notification list.
Next let's look at still a contrived example, but something with a little more substance.
We are going to create a fire alarm as our observable and sprinklers as our observers, the idea being that when our fire alarm is activated it'll send a signal to the sprinklers to activate.
let's start with our Fire alarm.
class FireAlarm : IObservable<bool>
{
IList<IObserver<bool>> sprinklers = new List<IObserver<bool>>();
public IDisposable Subscribe(IObserver<bool> sprinkler)
{
if (!sprinklers.Contains(sprinkler))
sprinklers.Add(sprinkler);
return new
Unsubscriber(sprinklers, sprinkler);
}
public void TurnOn()
{
foreach (var sprinkler in sprinklers)
sprinkler.OnNext(true);
}
public void
TurnOff()
{
foreach (var sprinkler in sprinklers)
sprinkler.OnNext(false);
}
private class Unsubscriber : IDisposable
{
//removed for
brevity
}
}
Next notice that I've removed the implementation for the Unsubscribe class, we'll take a closer look next, but for now let's just focus on the heart of the FireAlarm.
If we inspect the IObservable interface we see that it only defines a contract for the Subscribe function; this function takes in an observer and returns an implementation of IDisposable. This subscribe function must store a reference to all the observers and provide them with functionality to stop listening to our Observable, namely the Unsubscriber class which i'll dive into later.
Our Observable also should have one or more functions or methods to notify the observers that something has occurred, in the above we created TurnOn and TurnOff which both just iterate through the registered observers and invoke the OnNext method with a payload that was defined at the class level when we committed to the IObservable interface and in this case is a bool representing to turn on or off.
now let's take a closer look at the Unsubscriber class
class FireAlarm : IObservable<bool>
{
// removed for
brevity
private class Unsubscriber : IDisposable
{
private IList<IObserver<bool>> sprinklers;
private IObserver<bool> splrinkler;
public Unsubscriber(IList<IObserver<bool>> sprinklers, IObserver<bool> splrinkler)
{
this.sprinklers = sprinklers;
this.splrinkler = splrinkler;
}
public void
Dispose()
{
if (splrinkler != null)
sprinklers?.Remove(splrinkler);
}
}
}
The unsubscriber class takes in a reference to the sprinkler itself and to the fire alarm's list of sprinklers so that the unsubscriber can access the sprinkler within the dispose method defined by the IDisposable interface and to be able to remove that itself from the fire alarms collection of sprinklers.
Next let's take a look at our Sprinklers
class Sprinkler : IObserver<bool>
{
public string Name { get; set; }
IDisposable unsubscriber;
public virtual void Subscribe(IObservable<bool> smokeDetector)
{
this.unsubscriber = smokeDetector.Subscribe(this);
}
public virtual void Unsubscribe()
{
Console.WriteLine($"\r\n{Name} has
been decommissioned");
this.unsubscriber.Dispose();
}
public void
OnCompleted() => Console.WriteLine("OnCompleted");
public void
OnError(Exception error) => Console.WriteLine("Execption");
public void OnNext(bool value)
{
if (value)
Console.WriteLine($"{Name}\tactivated");
else
Console.WriteLine($"{Name}\tdeactivated");
}
}
we have a subscribe method that takes in a reference to the Fire alarm (observable) and simultaneously registers the sprinkler (observer) to receive notifications from the Observable through the fire alarm's Subscribe function that returns a reference to an unsubscribe object which is used to remove the observer from the observable's notification list.
now we also defined an Unsubscribe method that allows the sprinkler to remove itself from the fire alarms notification list via the unsubscriber field that is instantiated within the Subscribe method. Thus far none of the members of the Sprinkler class we've discussed are defined within the IObserver Interface.
The generic IObserver Interface defines three methods: OnCompleted(), OnError(Exception error), and OnNext(); the implementation of logic is really up to you, but the common approach for each is the following:
- OnCompleted(): this is fired as a notification that the observable will no longer be sending notifications for whatever reason, thus if your observable will no longer send data it makes sense to stop listening, or maybe not, it's really up to you.
- OnError(Exception error): should your Observer encounter some sort of internal exception here is the place where the observable could pass the exception to the observer and let each individual observer handle the exception there own way. In this example so far there is only one type of observer, however there could be many types.
- OnNext(T value): this is the method that the observers business logic should fire, for example our sprinkler should turn on or off based on the value parameter, for a dispatcher it may call or recall the fire department based on the value parameter.
next to reiterate the fact that you can have multiple types of observers we created a Fire Dispatcher whose job is to call the fire department in the event that a fire alarm is activated and recall the fire department if deactivated.
class Dispatcher : IObserver<bool>
{
public string Name { get; set; }
IDisposable unsubscriber;
public virtual void Subscribe(IObservable<bool> smokeDetector)
=> this.unsubscriber = smokeDetector.Subscribe(this);
public virtual void Unsubscribe()
{
Console.WriteLine($"\r\n{Name} has
been decommissioned");
this.unsubscriber.Dispose();
}
public void
OnCompleted()
{
Console.WriteLine("the firealarm will no longer dispatch notifications");
Unsubscribe();
}
public void
OnError(Exception error) => Console.WriteLine("error
");
public void OnNext(bool value)
{
if (value)
Console.WriteLine("the fire department has been called");
else
Console.WriteLine("false alarm FD has been recalled");
}
}
Finally let's put it all together with a console interface
using System;
using System.Collections.Generic;
namespace pav.ObserverPatternFire
{
class Program
{
static void Main(string[] args)
{
List<Sprinkler> sprinklers = new List<Sprinkler>() { new Sprinkler { Name = "Sprinkler One" },
new Sprinkler { Name = "Sprinkler
Two" }, new
Sprinkler { Name = "Sprinkler Three" },
new Sprinkler { Name = "Sprinkler
Four" }, new
Sprinkler { Name = "Sprinkler Five" }
};
FireAlarm fireAlarm = new FireAlarm();
foreach (var sprinkler in sprinklers)
sprinkler.Subscribe(fireAlarm);
Dispatcher dispatcher = new Dispatcher();
dispatcher.Subscribe(fireAlarm);
ConsoleKey? commandKey = null;
do
{
PrintMainMenu();
switch (commandKey)
{
case ConsoleKey.D1:
fireAlarm.TurnOn();
break;
case ConsoleKey.D2:
fireAlarm.TurnOff();
break;
case ConsoleKey.D3:
var sprinkler =
GetSprinkler(sprinklers);
if (sprinkler != null)
{
sprinkler.Unsubscribe();
sprinklers.Remove(sprinkler);
PrintMainMenu(false);
}
break;
}
} while ((commandKey = Console.ReadKey().Key) != ConsoleKey.Escape);
}
static void
PrintMainMenu(bool clear = true)
{
if (clear) Console.Clear();
Console.WriteLine("1) Turn on Fire Alarm");
Console.WriteLine("2) Turn off Fire Alarm");
Console.WriteLine("3) Decommission a sprinkler\r\n");
}
static Sprinkler GetSprinkler(IList<Sprinkler> sprinklers)
{
if (sprinklers.Count == 0)
return null;
var index = -1;
Console.WriteLine("\tSelect sprinker to decommision");
for (var i = 0; i
< sprinklers.Count; i++)
Console.WriteLine($"\t{i}) {sprinklers[i].Name}");
while (!int.TryParse(Console.ReadKey().KeyChar.ToString(),
out index) && (index < 0 ||
index > sprinklers.Count)) ;
return sprinklers[index];
}
}
class FireAlarm : IObservable<bool>
{
IList<IObserver<bool>> sprinklers = new List<IObserver<bool>>();
public IDisposable Subscribe(IObserver<bool> sprinkler)
{
if (!sprinklers.Contains(sprinkler))
sprinklers.Add(sprinkler);
return new
Unsubscriber(sprinklers, sprinkler);
}
public void TurnOn()
{
foreach (var sprinkler in sprinklers)
sprinkler.OnNext(true);
}
public void
TurnOff()
{
foreach (var sprinkler in sprinklers)
sprinkler.OnNext(false);
}
private class Unsubscriber : IDisposable
{
private IList<IObserver<bool>> sprinklers;
private IObserver<bool> sprinkler;
public Unsubscriber(IList<IObserver<bool>> sprinklers, IObserver<bool> sprinkler)
{
this.sprinklers = sprinklers;
this.sprinkler = sprinkler;
}
public void
Dispose()
{
if (sprinkler != null)
sprinklers?.Remove(sprinkler);
}
}
}
class Sprinkler : IObserver<bool>
{
public string Name { get; set; }
IDisposable unsubscriber;
public virtual void Subscribe(IObservable<bool> smokeDetector)
=> this.unsubscriber =
smokeDetector.Subscribe(this);
public virtual void Unsubscribe()
{
Console.WriteLine($"\r\n{Name} has
been decommissioned");
this.unsubscriber.Dispose();
}
public void
OnCompleted() => Console.WriteLine("OnCompleted");
public void
OnError(Exception error) => Console.WriteLine("Execption");
public void OnNext(bool value)
{
if (value)
Console.WriteLine($"{Name}\tactivated");
else
Console.WriteLine($"{Name}\tdeactivated");
}
}
class Dispatcher : IObserver<bool>
{
public string Name { get; set; }
IDisposable unsubscriber;
public virtual void Subscribe(IObservable<bool> smokeDetector)
=> this.unsubscriber =
smokeDetector.Subscribe(this);
public virtual void Unsubscribe()
{
Console.WriteLine($"\r\n{Name} has
been decommissioned");
this.unsubscriber.Dispose();
}
public void
OnCompleted()
{
Console.WriteLine("the firealarm will no longer dispatch notifications");
Unsubscribe();
}
public void
OnError(Exception error) => Console.WriteLine("error
");
public void OnNext(bool value)
{
if (value)
Console.WriteLine("the fire department has been called");
else
Console.WriteLine("false alarm FD has been recalled");
}
}
}