Summary: in this tutorial, you’ll learn how to use the C# observer pattern to notify objects (observers) when the state of another object (subject) changes.
Introduction to C# Observer pattern
The Observer pattern defines a one-to-many dependency between objects so that when one object (known as the subject) changes state, all its dependencies known as observers are notified and updated automatically.
The following UML diagram illustrates the Observer pattern:
Here are the participants in the observer pattern:
ISubject
provides an interface for subscribing and unsubscribingIObserver
objects.IObserver
defines an updating interface for objects that should be notified of changes in a subject.
stores the state of interest to theConcreteSubject
ConcreteObserver
objects. The
sends a notification to its observers when its state changes.ConcreteSubject
ConcreteObserver
has a reference to aConcreteSubject
object. It also implements theIObserver
interface to keep its state consistent with the state of the subject.
The ISubject
and IObservers
objects are linked through a one-to-many relationship. It means that a subject can have multiple observers subscribed to it.
When the state of the ISubject
changes, it notifies all of its IObserver
objects by calling their Update
methods.
The IObserver
objects can then access the ISubject
‘s new state and update their own state.
C# Observer pattern example
Let’s say you want to develop a program that manages stock quotes. Whenever the price of the stock changes, you want to display it in the console as well as save the data into a file. To do that, you can use the Observer pattern.
First, define an IObserver
interface that has a method called
. The Update
method has two parameters Update
symbol
and price
r representing the stock’s state:
public interface IObserver
{
public void Update(string symbol, decimal price);
}
Code language: C# (cs)
Second, define the ISubject
interface that manages the IObserver
objects such as subscribing, unsubscribing, and notifying the IObserver
objects:
public interface ISubject
{
void Subscribe(IObserver observer);
void Unsubscribe(IObserver observer);
void NotifyObservers();
}
Code language: C# (cs)
Third, define the Stock
class that implements the ISubject
interface:
public class Stock : ISubject
{
public string Symbol { get; set; }
private decimal _price;
public decimal Price
{
get => _price;
set
{
if (_price != value)
{
_price = value;
NotifyObservers();
}
}
}
public Stock(string symbol, decimal price)
{
Symbol = symbol;
Price = price;
}
private readonly List<IObserver> _observers = new();
public void NotifyObservers() => _observers.ForEach(observer => observer.Update(Symbol, Price));
public void Subscribe(IObserver observer) => _observers.Add(observer);
public void Unsubscribe(IObserver observer) => _observers.Remove(observer);
}
Code language: C# (cs)
The Stock
class has two properties Symbol
and Price
that represent stock symbol and price respectively.
The Stock
class maintains a list of
objects as a IObserver
List<IObserver>
. Since the Stock
class implements the ISubject
interface, it needs to provide implementations for the Subscribe
, Unsubscribe
, and NotifyObservers
methods:
Subscribe
– adds anIObserver
object to the_observers
list.Unsubscribe
– removes anIObserver
object from the_observers
list.NotifyObservers
– notifiesIObserver
object in the_observers
list by iterating the list and calling theUpdate()
method of each object.
In the Price
setter, if the price changes, we call the NotifiyObservers
method to notify the observers by calling their Update
method.
Fourth, define a Display
class that displays the stock whenever the price changes. The Display
class implements the IObserver
interface:
public class Display : IObserver
{
private readonly ISubject _subject;
public Display(ISubject subject)
{
_subject = subject;
_subject.Subscribe(this);
}
public void Update(string symbol, decimal price)
{
Console.WriteLine($"{symbol}: {price}");
}
}
Code language: C# (cs)
Notice that the Display
method maintains a member as an ISubject
object. In the constructor, it assigns the subject to the _subject
member and calls the Subscribe()
method of the _subject
object to subscribe itself as an observer.
Fifth, define a Logger
class that implements the IObserver
interface:
public class Logger : IObserver
{
private readonly ISubject _subject;
private readonly string _filename;
public Logger(ISubject subject, string filename)
{
_subject = subject;
_subject.Subscribe(this);
_filename = filename;
}
public void Update(string symbol, decimal price)
{
using var streamWriter = File.AppendText(_filename);
streamWriter.WriteLine($"{symbol}:{price}");
}
}
Code language: C# (cs)
The Logger
class is similar to the Display
class except that it saves the stock data into a text file specified by the filename.
Finally, define the Program class with the Main()
method as the entry point of the program:
public class Program
{
public static void Main(string[] args)
{
// Create a new stock
var stock = new Stock("ABC", 100m);
// Create two observers Display & Logger
var display = new Display(stock);
var logger = new Logger(stock, "stock.txt");
// Change the price, both display and logger
// will be notified and updated
stock.Price += 2;
stock.Price -= 1;
Console.ReadLine();
// remove the logger from the observer list
stock.Unsubscribe(logger);
// Change the price, only the display is notified
// and updated
stock.Price += 3;
Console.ReadLine();
}
}
Code language: C# (cs)
In the Main()
method, we create a stock object as the subject and the Display
and Logger
objects as the observers.
We then change the price of the stock twice, which updates the Display
and Logger
objects. As a result, you’ll see the console display the stock with new prices:
ABC: 102
ABC: 101
Press Enter (or Return) to remove the Logger from the observer list
Code language: C# (cs)
and the stock.txt
file will have two entries.
If you hit the Enter (or Return key), the stock removes the Logger
object from its observer list and changes its price. At this point, only the Display
object is notified and updated:
ABC: 102
ABC: 101
Press Enter (or Return) to remove the Logger from the observer list
ABC: 104
Code language: C# (cs)
Because the Logger
is not in the observer list, it is not notified and updated. Hence, the stock.txt
has no new entries.
Summary
- Use the C# Observer pattern to define a one-to-many dependency between objects, so that when one object changes, all its dependents are notified and updated automatically.