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 ofdata
to subscribedobservers
. It does so by keeping track of all subscribedobservers
.Observer
- Receives notifications ofdata
fromproviders
. Keeps track of receiveddata
so it can properly handle (and potentially ignore)data
notifications that have already been received.Data
- The data that theprovider
is sending toobservers
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&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!