Airbrake Blog

Behavioral Design Patterns: Observer

Written by Frances Banks | Aug 30, 2017 6:29:18 AM

Today we'll continue our journey through the Guide to Software Design Patterns series with a deep dive into the observer design pattern. The observer pattern is categorized as a behavioral design pattern, and its primary purpose is to allow a observer to "subscribe" to push-notifications generated by a provider.

In this article we'll examine the observer design pattern by digging into both a real world example, along with a fully-functional C# code sample. By the end of this piece you should have a solid understanding of what the observer pattern is and how it can be implemented into your own code projects, so let's get crackin'!

In the Real World

The observer design pattern consists of three main components:

  • Provider - Sends notifications of data to subscribed observers. It does so by keeping track of all subscribed observers.
  • Observer - Receives notifications of data from providers. Keeps track of received data so it can properly handle (and potentially ignore) data notifications that have already been received.
  • Data - The data that the provider is sending to observers via push-notification.

Paradoxically, it's rather challenging to come up with a real world example of an observer pattern that doesn't involve computing, because nearly every technological service or device we use on a daily basis implements an observer pattern in one way or another. App notifications on your phone or email alerts from your favorite shopping site are just two of the extremely common observer pattern scenarios many of us frequently experience.

One slightly less technical real world example of the observer pattern is when you sign up for a magazine subscription, such as WIRED (one of my personal favorites). While computers are obviously handling all the logic behind-the-scenes, the observer pattern is still being used in this case. You (an observer) have subscribed to a magazine provider. When the provider publishes a new issue (the data), you receive it in the mail a few days later. Critically, this is not a one-to-one relationship, in which the provider is sending the notification (magazine) only to you. Instead, they're able to send it to thousands of people all at once, just as most digital services do when you sign-up for their email newsletter.

Full Code Sample

Below is the full code sample we'll be using in this article. It can be copied and pasted if you'd like to play with the code yourself and see how everything works.

// Program.cs
using Observer.News;

namespace Observer
{
class Program
{
static void Main(string[] args)
{
NewsTest();
}

internal static void NewsTest()
{
// Create news agencies (providers).
var associatedPress = new Agency("Associated Press");
var reuters = new Agency("Reuters");

// Create newspapers (observers).
var newYorkTimes = new Newspaper("The New York Times");
var washingtonPost = new Newspaper("The Washington Post");

// AP publication. Neither newspaper subscribes, so no output.
associatedPress.Publish("Waiting the worst with Harvey, the storm that won’t go away", "Juliet Linderman");

// Times subscribes to AP.
newYorkTimes.Subscribe(associatedPress);

// Post subscribes to Reuters.
washingtonPost.Subscribe(reuters);

// Reuters publications.
reuters.Publish("Japan retail sales slow in July, still top expectations", "Stanley White");
reuters.Publish("Transgender members in U.S. military may serve until study completed: Mattis", "Reuters Staff");

// AP publications.
associatedPress.Publish("Chicago changes course, wants police reforms with court role", "Don Babwin and Michael Tarm");
associatedPress.Publish("US Open fashion: Crystals, shapes and knee-high socks", "James Martinez");

// Post subscribes to AP.
washingtonPost.Subscribe(associatedPress);

// AP Publications, both Times and Post should receive.
associatedPress.Publish("Game of Thrones: Trust me, I’m a Targaryen", "Paul Wiseman, Josh Boak, and Christopher Rugaber");
associatedPress.Publish("Merkel: Europe still ‘hasn’t done homework’ on refugees", "Geir Moulson");

// Post unsubscribes from AP.
washingtonPost.Unsubscribe(associatedPress);

// AP publication, should only be picked up by Times.
associatedPress.Publish("Hajj pilgrimage entangled in web of Saudi politics", "Aya Batrawy");

// Perform cleanup for AP.
associatedPress.Shutdown();

// Few more Reuters publications.
reuters.Publish("Google, Apple face off over augmented reality technology", "Stephen Nellis");
reuters.Publish("Under investor pressure, Goldman to explain trading strategy", "Olivia Oran");
reuters.Publish("UK retailers see Brexit hit to consumers without detailed customs plans", "Reuters Staff");
}
}
}

// <News/>Agency.cs
using System;
using System.Collections.Generic;

namespace Observer.News
{
/// <summary>
/// News agency that publishes articles,
/// which can be picked up by other outlets like Newspapers.
///
/// Acts as 'Provider' in Observer pattern.
/// </summary>
public class Agency : IObservable<Article>, IComparable
{
private readonly List<Article> _articles = new List<Article>();
private readonly List<IObserver<Article>> _observers = new List<IObserver<Article>>();

public string Name { get; }

public Agency(string name)
{
Name = name;
}

/// <summary>
/// Invoked by observers that wish to subscribe to Agency notifications.
/// </summary>
/// <param name="observer">Observer to add.</param>
/// <returns>IDisposable reference, which allows observers to unsubscribe.</returns>
public IDisposable Subscribe(IObserver<Article> observer)
{
// Check if list contains observer.
if (!_observers.Contains(observer))
{
// Add observer to list.
_observers.Add(observer);
}

// Return a new Unsubscriber<Article> instance.
return new Unsubscriber<Article>(_observers, observer);
}

/// <summary>
/// Comparison method for IComparison interface, used for sorting.
/// </summary>
/// <param name="agency">Agency to be compared.</param>
/// <returns>Comparison result.</returns>
public int CompareTo(object agency)
{
if (agency is null) return 1;

var other = agency as Agency;

// Check that parameter is Article.
if (other is null) throw new ArgumentException("Compared object must be an Agency instance.", nameof(agency));

// Sort by name.
return string.Compare(Name, other.Name, StringComparison.Ordinal);
}

/// <summary>
/// Publishes a new article with title and author.
/// </summary>
/// <param name="title">Article title.</param>
/// <param name="author">Article author.</param>
public void Publish(string title, string author)
{
// Create new Article.
var article = new Article(title, author, this);

// If article already exists, abort.
if (_articles.Contains(article)) return;

// Add article to list.
_articles.Add(article);

// Invoke OnNext for every subscribed observer.
foreach (var observer in _observers)
{
observer.OnNext(article);
}
}

/// <summary>
/// Halts all notification pushes, invokes OnCompleted for all observers,
/// and removes all subscribed observers.
/// </summary>
public void Shutdown()
{
foreach (var observer in _observers)
observer.OnCompleted();

_observers.Clear();
}
}
}

// <News/>Article.cs
using System;

namespace Observer.News
{
/// <summary>
/// Contains basic publication information, including source Agency.
/// </summary>
public class Article : IComparable
{
public Agency Agency { get; }

public string Author { get; }

public string Title { get; }

internal Article(string title, string author, Agency agency)
{
Agency = agency;
Author = author;
Title = title;
}

/// <summary>
/// Comparison method for IComparison interface, used for sorting.
/// </summary>
/// <param name="article">Article to be compared.</param>
/// <returns>Comparison result.</returns>
public int CompareTo(object article)
{
if (article is null) return 1;

var other = article as Article;

// Check that parameter is Article.
if (other is null) throw new ArgumentException("Compared object must be an Article instance.", nameof(article));

// If author difference, sort by author first.
// Otherwise, sort by title.
var authorDiff = string.Compare(Author, other.Author, StringComparison.Ordinal);
return authorDiff != 0 ? authorDiff : string.Compare(Title, other.Title, StringComparison.Ordinal);
}

public override string ToString()
{
return $"'{Title}' by {Author} via {Agency.Name}";
}
}
}

// <News/>Newspaper.cs
using System;
using System.Collections.Generic;
using Utility;

namespace Observer.News
{
/// <summary>
/// Receives Articles from Agencies for publication.
///
/// Acts as 'Observer' in Observer pattern.
/// </summary>
public class Newspaper : IObserver<Article>
{
private readonly List<Article> _articles = new List<Article>();
private readonly SortedList<Agency, IDisposable> _cancellations = new SortedList<Agency, IDisposable>();

public string Name { get; }

public Newspaper(string name)
{
Name = name;
}

/// <summary>
/// Fires when provider has finished.
/// Destroys all saved articles.
/// </summary>
public virtual void OnCompleted()
{
_articles.Clear();
}

public void OnError(Exception exception)
{
throw new NotImplementedException();
}

/// <summary>
/// Called by provider with newly-added articles.
/// </summary>
/// <param name="article">New notification.</param>
public virtual void OnNext(Article article)
{
// If article already exists, ignore.
if (_articles.Contains(article)) return;

// Add article.
_articles.Add(article);

// Output article.
Logging.Log($"{Name} received {article}");
}

/// <summary>
/// Subscribe to passed Agency notifications.
/// </summary>
/// <param name="agency">Agency to subscribe to.</param>
public virtual void Subscribe(Agency agency)
{
// Add cancellation token.
_cancellations.Add(agency, agency.Subscribe(this));

// Output subscription info.
Logging.LineSeparator($"{Name} SUBSCRIBED TO {agency.Name}", 70);
}

/// <summary>
/// Unsubscribes from passed Agency notifications.
/// </summary>
/// <param name="agency">Agency to unsubscribe from.</param>
public virtual void Unsubscribe(Agency agency)
{
// Dispose.
_cancellations[agency].Dispose();

// Remove all articles from agency.
_articles.RemoveAll(x => x.Agency == agency);

// Output subscription info.
Logging.LineSeparator($"{Name} UNSUBSCRIBED TO {agency.Name}", 70);
}
}
}

// Unsubscriber.cs

using System;
using System.Collections.Generic;

namespace Observer
{
internal class Unsubscriber<T> : IDisposable
{
private readonly List<IObserver<T>> _observers;
private readonly IObserver<T> _observer;

internal Unsubscriber(List<IObserver<T>> observers, IObserver<T> observer)
{
_observers = observers;
_observer = observer;
}

public void Dispose()
{
if (_observers.Contains(_observer))
{
_observers.Remove(_observer);
}
}
}
}

// <Utility/>Logging.cs
using System;
using System.Diagnostics;
using System.Xml.Serialization;

namespace Utility
{
/// <summary>
/// Houses all logging methods for various debug outputs.
/// </summary>
public static class Logging
{
private const char SeparatorCharacterDefault = '-';
private const int SeparatorLengthDefault = 40;

/// <summary>
/// Determines type of output to be generated.
/// </summary>
public enum OutputType
{
/// <summary>
/// Default output.
/// </summary>
Default,
/// <summary>
/// Output includes timestamp prefix.
/// </summary>
Timestamp
}

/// <summary>
/// Outputs to <see cref="Debug.WriteLine(String)"/>.
/// </summary>
/// <param name="value">Value to be output to log.</param>
/// <param name="outputType">Output type.</param>
public static void Log(string value, OutputType outputType = OutputType.Default)
{
Output(value, outputType);
}

/// <summary>
/// Outputs to <see cref="Debug.WriteLine(String)"/>.
/// </summary>
/// <param name="value">Value to be output to log.</param>
/// <param name="arg0"></param>
public static void Log(string value, object arg0)
{
Debug.WriteLine(value, arg0);
}

/// <summary>
/// Outputs to <see cref="Debug.WriteLine(String)"/>.
/// </summary>
/// <param name="value">Value to be output to log.</param>
/// <param name="arg0"></param>
/// <param name="arg1"></param>
public static void Log(string value, object arg0, object arg1)
{
Debug.WriteLine(value, arg0, arg1);
}

/// <summary>
/// Outputs to <see cref="Debug.WriteLine(String)"/>.
/// </summary>
/// <param name="value">Value to be output to log.</param>
/// <param name="arg0"></param>
/// <param name="arg1"></param>
/// <param name="arg2"></param>
public static void Log(string value, object arg0, object arg1, object arg2)
{
Debug.WriteLine(value, arg0, arg1, arg2);
}

/// <summary>
/// When <see cref="Exception"/> parameter is passed, modifies the output to indicate
/// if <see cref="Exception"/> was expected, based on passed in `expected` parameter.
/// <para>Outputs the full <see cref="Exception"/> type and message.</para>
/// </summary>
/// <param name="exception">The <see cref="Exception"/> to output.</param>
/// <param name="expected">Boolean indicating if <see cref="Exception"/> was expected.</param>
/// <param name="outputType">Output type.</param>
public static void Log(Exception exception, bool expected = true, OutputType outputType = OutputType.Default)
{
var value = $"[{(expected ? "EXPECTED" : "UNEXPECTED")}] {exception}: {exception.Message}";

Output(value, outputType);
}

private static void Output(string value, OutputType outputType = OutputType.Default)
{
Debug.WriteLine(outputType == OutputType.Timestamp
? $"[{StopwatchProxy.Instance.Stopwatch.Elapsed}] {value}"
: value);
}

/// <summary>
/// Outputs to <see cref="Debug.WriteLine(Object)"/>.
///
/// ObjectDumper: http://stackoverflow.com/questions/852181/c-printing-all-properties-of-an-object&amp;lt;/cref
/// </summary>
/// <param name="value">Value to be output to log.</param>
/// <param name="outputType">Output type.</param>
public static void Log(object value, OutputType outputType = OutputType.Default)
{
if (value is IXmlSerializable)
{
Debug.WriteLine(value);
}
else
{
Debug.WriteLine(outputType == OutputType.Timestamp
? $"[{StopwatchProxy.Instance.Stopwatch.Elapsed}] {ObjectDumper.Dump(value)}"
: ObjectDumper.Dump(value));
}
}

/// <summary>
/// Outputs a dashed line separator to <see cref="Debug.WriteLine(String)"/>.
/// </summary>
/// <param name="length">Total separator length.</param>
/// <param name="char">Separator character.</param>
public static void LineSeparator(int length = SeparatorLengthDefault, char @char = SeparatorCharacterDefault)
{
Debug.WriteLine(new string(@char, length));
}

/// <summary>
/// Outputs a dashed line separator to <see cref="Debug.WriteLine(String)"/>,
/// with inserted text centered in the middle.
/// </summary>
/// <param name="insert">Inserted text to be centered.</param>
/// <param name="length">Total separator length.</param>
/// <param name="char">Separator character.</param>
public static void LineSeparator(string insert, int length = SeparatorLengthDefault, char @char = SeparatorCharacterDefault)
{
// Default output to insert.
var output = insert;

if (insert.Length < length)
{
// Update length based on insert length, less a space for margin.
length -= insert.Length + 2;
// Halve the length and floor left side.
var left = (int) Math.Floor((decimal) (length / 2));
var right = left;
// If odd number, add dropped remainder to right side.
if (length % 2 != 0) right += 1;

// Surround insert with separators.
output = $"{new string(@char, left)} {insert} {new string(@char, right)}";
}

// Output.
Debug.WriteLine(output);
}
}
}

How It Works In Code

Our code sample uses the concept of news agencies like the Associated Press and Reuters. These agencies gather and create news reports from across the world, and then sell said reports to specific news organizations, such as newspapers and TV networks. To keep things a bit simpler, our sample code will encompass that relationship between just two news agencies (Associated Press and Reuters) and two newspapers (The Washington Post and The New York Times).

We've taken a handful of today's top headlines from both agencies, and have created our example observer pattern code around the notion that these newspapers will subscribe (and possibly also unsubscribe) to said agencies, thereby receiving notifications of news stories as they're published.

To achieve this we'll start with the most basic object in the codebase, the Article class:

// <News/>Article.cs
using System;

namespace Observer.News
{
/// <summary>
/// Contains basic publication information, including source Agency.
/// </summary>
public class Article : IComparable
{
public Agency Agency { get; }

public string Author { get; }

public string Title { get; }

internal Article(string title, string author, Agency agency)
{
Agency = agency;
Author = author;
Title = title;
}

/// <summary>
/// Comparison method for IComparison interface, used for sorting.
/// </summary>
/// <param name="article">Article to be compared.</param>
/// <returns>Comparison result.</returns>
public int CompareTo(object article)
{
if (article is null) return 1;

var other = article as Article;

// Check that parameter is Article.
if (other is null) throw new ArgumentException("Compared object must be an Article instance.", nameof(article));

// If author difference, sort by author first.
// Otherwise, sort by title.
var authorDiff = string.Compare(Author, other.Author, StringComparison.Ordinal);
return authorDiff != 0 ? authorDiff : string.Compare(Title, other.Title, StringComparison.Ordinal);
}

public override string ToString()
{
return $"'{Title}' by {Author} via {Agency.Name}";
}
}
}

An Article is the basic form of data that an Agency will produce and publish, which sends it out to all subscribed observers -- Newspapers in this case. The Article class has some basic properties, though we're foregoing the actual article content and just using the title and author to differentiate them from one another. We also want to know what Agency published said Article, so that property is also included. Lastly, we implement the IComparable interface so we can compare and sort Articles elsewhere in the code.

Next we have the Agency class, which acts as the provider in this example by publishing news Articles:

// <News/>Agency.cs
using System;
using System.Collections.Generic;

namespace Observer.News
{
/// <summary>
/// News agency that publishes articles,
/// which can be picked up by other outlets like Newspapers.
///
/// Acts as 'Provider' in Observer pattern.
/// </summary>
public class Agency : IObservable<Article>, IComparable
{
private readonly List<Article> _articles = new List<Article>();
private readonly List<IObserver<Article>> _observers = new List<IObserver<Article>>();

public string Name { get; }

public Agency(string name)
{
Name = name;
}

/// <summary>
/// Invoked by observers that wish to subscribe to Agency notifications.
/// </summary>
/// <param name="observer">Observer to add.</param>
/// <returns>IDisposable reference, which allows observers to unsubscribe.</returns>
public IDisposable Subscribe(IObserver<Article> observer)
{
// Check if list contains observer.
if (!_observers.Contains(observer))
{
// Add observer to list.
_observers.Add(observer);
}

// Return a new Unsubscriber<Article> instance.
return new Unsubscriber<Article>(_observers, observer);
}

/// <summary>
/// Comparison method for IComparison interface, used for sorting.
/// </summary>
/// <param name="agency">Agency to be compared.</param>
/// <returns>Comparison result.</returns>
public int CompareTo(object agency)
{
if (agency is null) return 1;

var other = agency as Agency;

// Check that parameter is Article.
if (other is null) throw new ArgumentException("Compared object must be an Agency instance.", nameof(agency));

// Sort by name.
return string.Compare(Name, other.Name, StringComparison.Ordinal);
}

/// <summary>
/// Publishes a new article with title and author.
/// </summary>
/// <param name="title">Article title.</param>
/// <param name="author">Article author.</param>
public void Publish(string title, string author)
{
// Create new Article.
var article = new Article(title, author, this);

// If article already exists, abort.
if (_articles.Contains(article)) return;

// Add article to list.
_articles.Add(article);

// Invoke OnNext for every subscribed observer.
foreach (var observer in _observers)
{
observer.OnNext(article);
}
}

/// <summary>
/// Halts all notification pushes, invokes OnCompleted for all observers,
/// and removes all subscribed observers.
/// </summary>
public void Shutdown()
{
foreach (var observer in _observers)
observer.OnCompleted();

_observers.Clear();
}
}
}

As previously mentioned, a provider should keep track of all its active observers, so the Agency definition begins with a list of Articles and observers, which are defined by implementing the IObserver interface.

The Subscribe(IObserver<Article> observer) method can be called by any IObserver<Article> object (such as Newspaper, which we'll see in a moment). Doing so ensures that the observer is now tracked and will subscribe to all notifications created by this Agency instance. The Subscribe(IObserver<Article> observer) method also returns a new instance of Unsubscriber<Article>, which implements the IDisposable interface and allows the Dispose() method to be called, thereby releasing any unmanaged resources when invoked:

// Unsubscriber.cs
using System;
using System.Collections.Generic;

namespace Observer
{
internal class Unsubscriber<T> : IDisposable
{
private readonly List<IObserver<T>> _observers;
private readonly IObserver<T> _observer;

internal Unsubscriber(List<IObserver<T>> observers, IObserver<T> observer)
{
_observers = observers;
_observer = observer;
}

public void Dispose()
{
if (_observers.Contains(_observer))
{
_observers.Remove(_observer);
}
}
}
}

Back to the Agency instance methods, we'll skip over CompareTo(object agency) since that's self-explanatory. The Publish(string title, string author) method performs most of the logic when creating a new Article. It adds the Article to the local Articles list, then invokes the OnNext(Article article) method of all subscribed observers. We'll look at this in a moment within the Newspaper class, but this method is required by the IObservable<T> interface and is the means by which observers are alerted of new data.

It's also worth noting that we've elected to accept direct string parameters in the Publish(string title, string author) definition and used those to create a new local Article instance. This was done to simplify this code example, but in a real world scenario, it would likely be beneficial to create Article instances in an outside scope, and then pass them via a method signature like Publish(Article article).

The final method, Shutdown(), can be called if we want this Agency instance to halt all notifications and remove all current observers.

The last object in our observer design pattern trifecta is the Newspaper class, which acts as our observer entity and subscribes to Agency instances, in order to receive Article publications pushed via the Publish(string title, string author) method:

// <News/>Newspaper.cs
using System;
using System.Collections.Generic;
using Utility;

namespace Observer.News
{
/// <summary>
/// Receives Articles from Agencies for publication.
///
/// Acts as 'Observer' in Observer pattern.
/// </summary>
public class Newspaper : IObserver<Article>
{
private readonly List<Article> _articles = new List<Article>();
private readonly SortedList<Agency, IDisposable> _cancellations = new SortedList<Agency, IDisposable>();

public string Name { get; }

public Newspaper(string name)
{
Name = name;
}

/// <summary>
/// Fires when provider has finished.
/// Destroys all saved articles.
/// </summary>
public virtual void OnCompleted()
{
_articles.Clear();
}

public void OnError(Exception exception)
{
throw new NotImplementedException();
}

/// <summary>
/// Called by provider with newly-added articles.
/// </summary>
/// <param name="article">New notification.</param>
public virtual void OnNext(Article article)
{
// If article already exists, ignore.
if (_articles.Contains(article)) return;

// Add article.
_articles.Add(article);

// Output article.
Logging.Log($"{Name} received {article}");
}

/// <summary>
/// Subscribe to passed Agency notifications.
/// </summary>
/// <param name="agency">Agency to subscribe to.</param>
public virtual void Subscribe(Agency agency)
{
// Add cancellation token.
_cancellations.Add(agency, agency.Subscribe(this));

// Output subscription info.
Logging.LineSeparator($"{Name} SUBSCRIBED TO {agency.Name}", 70);
}

/// <summary>
/// Unsubscribes from passed Agency notifications.
/// </summary>
/// <param name="agency">Agency to unsubscribe from.</param>
public virtual void Unsubscribe(Agency agency)
{
// Dispose.
_cancellations[agency].Dispose();

// Remove all articles from agency.
_articles.RemoveAll(x => x.Agency == agency);

// Output subscription info.
Logging.LineSeparator($"{Name} UNSUBSCRIBED TO {agency.Name}", 70);
}
}
}

As an observer, the Newspaper class stores a list of Articles it has received, so it can avoid duplicates since an observer shouldn't know (or care) when or how often a provider will push notifications out. It also maintains a SortedList<TKey, TValue> where TKey is Agency and TValue is IDisposable. This allows a Newspaper instance to subscribe to multiple providers (Agencies) at once, and when desired, unsubscribe only from the desired Agency (while maintaining other subscriptions).

As you may notice, Newspaper implements the IObserver<Article> interface, which requires that the following three methods be implemented.

OnCompleted(), which is invoked by the provider to indicate that it has stopped sending notifications:

/// <summary>
/// Fires when provider has finished.
/// Destroys all saved articles.
/// </summary>
public virtual void OnCompleted()
{
_articles.Clear();
}

OnError(), which indicates that the provider experienced an error. We've elected not to implement this method since no errors will be generated, but it's required by the interface:

public void OnError(Exception exception)
{
throw new NotImplementedException();
}

Finally, the OnNext(Article article) method, which is invoked by the provider when a new Article is published. This is the bread and butter of this class, and allows the observer to receive new data notifications:

/// <summary>
/// Called by provider with newly-added articles.
/// </summary>
/// <param name="article">New notification.</param>
public virtual void OnNext(Article article)
{
// If article already exists, ignore.
if (_articles.Contains(article)) return;

// Add article.
_articles.Add(article);

// Output article.
Logging.Log($"{Name} received {article}");
}

Since Newspaper is an observer, we also need to implement some way for it to subscribe to providers (Agencies). The Subscribe(Agency agency) method allows the Newspaper to subscribe to the passed agency instance, by invoking the IDisposable Subscribe(IObserver<Article> observer) method within the Agency class and adding the IDisposable token to the local cancellations list for that Agency:

/// <summary>
/// Subscribe to passed Agency notifications.
/// </summary>
/// <param name="agency">Agency to subscribe to.</param>
public virtual void Subscribe(Agency agency)
{
// Add cancellation token.
_cancellations.Add(agency, agency.Subscribe(this));

// Output subscription info.
Logging.LineSeparator($"{Name} SUBSCRIBED TO {agency.Name}", 70);
}

Similarly, we also have the Unsubscribe(Agency agency) method, which calls the Dispose() method of the cancellations list element for the passed agency. It also finds and removes all stored Articles that were produced by that Agency:

/// <summary>
/// Unsubscribes from passed Agency notifications.
/// </summary>
/// <param name="agency">Agency to unsubscribe from.</param>
public virtual void Unsubscribe(Agency agency)
{
// Dispose.
_cancellations[agency].Dispose();

// Remove all articles from agency.
_articles.RemoveAll(x => x.Agency == agency);

// Output subscription info.
Logging.LineSeparator($"{Name} UNSUBSCRIBED TO {agency.Name}", 70);
}

Whew! Now, with everything setup and ready to go, we can test this out by creating a few Agency and Newspaper instances, adding some subscriptions, publishing some articles, and seeing the results in the log output:

// Program.cs
using Observer.News;

namespace Observer
{
class Program
{
static void Main(string[] args)
{
NewsTest();
}

internal static void NewsTest()
{
// Create news agencies (providers).
var associatedPress = new Agency("Associated Press");
var reuters = new Agency("Reuters");

// Create newspapers (observers).
var newYorkTimes = new Newspaper("The New York Times");
var washingtonPost = new Newspaper("The Washington Post");

// AP publication. Neither newspaper subscribes, so no output.
associatedPress.Publish("Waiting the worst with Harvey, the storm that won’t go away", "Juliet Linderman");

// Times subscribes to AP.
newYorkTimes.Subscribe(associatedPress);

// Post subscribes to Reuters.
washingtonPost.Subscribe(reuters);

// Reuters publications.
reuters.Publish("Japan retail sales slow in July, still top expectations", "Stanley White");
reuters.Publish("Transgender members in U.S. military may serve until study completed: Mattis", "Reuters Staff");

// AP publications.
associatedPress.Publish("Chicago changes course, wants police reforms with court role", "Don Babwin and Michael Tarm");
associatedPress.Publish("US Open fashion: Crystals, shapes and knee-high socks", "James Martinez");

// Post subscribes to AP.
washingtonPost.Subscribe(associatedPress);

// AP Publications, both Times and Post should receive.
associatedPress.Publish("Game of Thrones: Trust me, I’m a Targaryen", "Paul Wiseman, Josh Boak, and Christopher Rugaber");
associatedPress.Publish("Merkel: Europe still ‘hasn’t done homework’ on refugees", "Geir Moulson");

// Post unsubscribes from AP.
washingtonPost.Unsubscribe(associatedPress);

// AP publication, should only be picked up by Times.
associatedPress.Publish("Hajj pilgrimage entangled in web of Saudi politics", "Aya Batrawy");

// Perform cleanup for AP.
associatedPress.Shutdown();

// Few more Reuters publications.
reuters.Publish("Google, Apple face off over augmented reality technology", "Stephen Nellis");
reuters.Publish("Under investor pressure, Goldman to explain trading strategy", "Olivia Oran");
reuters.Publish("UK retailers see Brexit hit to consumers without detailed customs plans", "Reuters Staff");
}
}
}

As you can see, we start by creating Agency instances for Associated Press and Reuters. We also create Newspaper instances for The New York Times and The Washington Post. Before we examine it any further, let's jump ahead a bit and look at the output from executing the code above, which we'll reference as we go through the remainder of the NewsTest() method:

--------- The New York Times SUBSCRIBED TO Associated Press ----------
------------- The Washington Post SUBSCRIBED TO Reuters --------------
The Washington Post received 'Japan retail sales slow in July, still top expectations' by Stanley White via Reuters
The Washington Post received 'Transgender members in U.S. military may serve until study completed: Mattis' by Reuters Staff via Reuters
The New York Times received 'Chicago changes course, wants police reforms with court role' by Don Babwin and Michael Tarm via Associated Press
The New York Times received 'US Open fashion: Crystals, shapes and knee-high socks' by James Martinez via Associated Press
--------- The Washington Post SUBSCRIBED TO Associated Press ---------
The New York Times received 'Game of Thrones: Trust me, I’m a Targaryen' by Paul Wiseman, Josh Boak, and Christopher Rugaber via Associated Press
The Washington Post received 'Game of Thrones: Trust me, I’m a Targaryen' by Paul Wiseman, Josh Boak, and Christopher Rugaber via Associated Press
The New York Times received 'Merkel: Europe still ‘hasn’t done homework’ on refugees' by Geir Moulson via Associated Press
The Washington Post received 'Merkel: Europe still ‘hasn’t done homework’ on refugees' by Geir Moulson via Associated Press
-------- The Washington Post UNSUBSCRIBED TO Associated Press --------
The New York Times received 'Hajj pilgrimage entangled in web of Saudi politics' by Aya Batrawy via Associated Press
The Washington Post received 'Google, Apple face off over augmented reality technology' by Stephen Nellis via Reuters
The Washington Post received 'Under investor pressure, Goldman to explain trading strategy' by Olivia Oran via Reuters
The Washington Post received 'UK retailers see Brexit hit to consumers without detailed customs plans' by Reuters Staff via Reuters

Even though the next line of our method has associatedPress publishing an article by Juliet Linderman, our output doesn't display that article anywhere. This is because neither of our Newspaper instances have subscribed to associatedPress at this point. This illustrates how the observer pattern is ignorant of the relationships behind-the-scenes. It doesn't care who is subscribed, it just pushes notifications and those who happen to be listening will receive them.

Anyway, the next section of code has newYorkTimes subscribing to associatedPress, along with washingtonPost subscribing to reuters, both of which are reflected in the output log:

--------- The New York Times SUBSCRIBED TO Associated Press ----------
------------- The Washington Post SUBSCRIBED TO Reuters --------------

reuters then publishes two Articles and, now that it has subscribed, washingtonPost receives both of those notifications:

The Washington Post received 'Japan retail sales slow in July, still top expectations' by Stanley White via Reuters
The Washington Post received 'Transgender members in U.S. military may serve until study completed: Mattis' by Reuters Staff via Reuters

Similarly, associatedPress publishes two Articles of its own, which newYorkTimes receives:

The New York Times received 'Chicago changes course, wants police reforms with court role' by Don Babwin and Michael Tarm via Associated Press
The New York Times received 'US Open fashion: Crystals, shapes and knee-high socks' by James Martinez via Associated Press

The washingtonPost then subscribes to associatedPress as well, followed by two more publications from associatedPress. Now that associatedPress has two subscribers, we should see two outputs for each Article published:

The New York Times received 'Game of Thrones: Trust me, I’m a Targaryen' by Paul Wiseman, Josh Boak, and Christopher Rugaber via Associated Press
The Washington Post received 'Game of Thrones: Trust me, I’m a Targaryen' by Paul Wiseman, Josh Boak, and Christopher Rugaber via Associated Press
The New York Times received 'Merkel: Europe still ‘hasn’t done homework’ on refugees' by Geir Moulson via Associated Press
The Washington Post received 'Merkel: Europe still ‘hasn’t done homework’ on refugees' by Geir Moulson via Associated Press

The washingtonPost has now elected to unsubscribe from associatedPress, just before associatedPress publishes one last Article. Both these events are shown in the log, with the last article only being picked up by the remaining observer (newYorkTimes):

-------- The Washington Post UNSUBSCRIBED TO Associated Press --------
The New York Times received 'Hajj pilgrimage entangled in web of Saudi politics' by Aya Batrawy via Associated Press

Finally, reuters publishes three more Articles, and we illustrate that the washingtonPost observer can remain subscribed to an Agency even after leaving another, since it receives all three notifications:

The Washington Post received 'Google, Apple face off over augmented reality technology' by Stephen Nellis via Reuters
The Washington Post received 'Under investor pressure, Goldman to explain trading strategy' by Olivia Oran via Reuters
The Washington Post received 'UK retailers see Brexit hit to consumers without detailed customs plans' by Reuters Staff via Reuters

There you have it! Hopefully this article provided a bit of insight into the capabilities of the observer design pattern, and gave you some basic tools you can use in your own future projects. For more information on all the other popular design patterns, head on over to our ongoing design pattern series here!