DEV Community

Cover image for Observer Pattern in C#
Kostas Kalafatis
Kostas Kalafatis

Posted on

Observer Pattern in C#

Originally posted here

The Observer is a behavioural design pattern that lets us define a subscription mechanism to notify multiple objects about any events that happen to the entity they're observing.

The Observer pattern allows objects to notify their observers when their internal state changes, meaning that a single class has to be aware of the entities that observe it. It also needs to communicate to those observers about alterations in the subject's state. Further, the observers should be notified automatically.

You can find the example code of this post, on GitHub

Conceptualizing the Problem

Let's imagine that we have an online store. We have the entities Customer and Store. One of our customers has a very peculiar taste and is interested in a particular brand of super universal remotes. These remotes should be available in our store very soon.

The customer could visit the store every day and check product availability. But while the product is still en route, most of these trips would be pointless.

On the other hand, the store could send tons of emails to all customers each time a new product becomes available, saving some customers from endless trips to the store. At the same time, it'd upset other customers who aren't interested in new products.

Now we are in a conflict. Either our customers will have to waste time checking product availability, or the store will have to waste resources notifying the wrong customers.

An object with an observed state is commonly known as a subject, but since it notifies other objects about state changes, we'll call it a publisher. All other objects that want to track changes to the publisher's state are called subscribers.

The Observer pattern suggests that we add a subscription mechanism to the publisher class so individual objects can subscribe to or unsubscribe from a stream of events sent by the publisher.

In reality, this mechanism consists of an array field for storing a list of references to the subscriber objects and several public methods which allow adding subscribers to and removing them from that list.

Observer-Publisher Diagram

Now the publisher goes over to its subscribers and calls their specific notification method.

Real-world applications might have dozens of different subscriber classes interested in tracking events of the same publisher class. We wouldn't want to couple the publisher to all of those classes.

Thus, it's crucial that all subscribers implement the same interface and that the publisher only communicates with them exclusively via that interface. The interface should declare the notification method along with a set of parameters that the publisher can use to pass some contextual data along with the notification.

If our application uses several different publisher classes, we can make all publishers use the same interface. This interface would only need to describe a few subscription methods. The interface would allow subscribers to observe the publishers' states without coupling to their concrete implementations.

Structuring the Observer Pattern

In its base implementation, the Observer pattern has four subscribers:

Observer Class Diagram

  • Publisher: The Publisher issues events of interest to other objects. The events occur when the publisher changes its state or executes some behaviour. Publishers contain the subscription infrastructure that lest new subscribers join and current subscribers leave the list. When a new event happens, the publisher goes oven the subscription list and calls the notification method declared in the subscriber interface on each subscriber object.
  • Subscriber: The Subscriber interface declares the notification interface. In most cases, it consists of a single Update method. The method may have several parameters that let the publisher pass some event details along with the update.
  • Concrete Subscriber: The Concrete Subscribers perform some actions in response to notifications issued by the publisher. All of these classes must implement the same interface so the publisher isn't coupled to concrete classes. Usually, subscribers need some contextual information to handle the update correctly. For that reason, publishers often pass some context data as arguments for the notification method. The publisher can pass itself as an argument, letting the subscriber fetch any required data directly.
  • Client: The Client creates publisher and subscriber objects separately and then registers subscribers for published objects.

To demonstrate our observer, let's imagine that we are going to implement a system that tracks the fluctuating prices of goods in our local farmer's markets.

On some days, the goods will be more expensive than on other days, due to factors like the season, the size of the harvest or the quality of the goods themselves. Further, we need to allow restaurants to watch the prices and place an order when the price of a particular item falls below a specified threshold, which is different for each restaurant.

First, let's define our Subject participant abstract class Goods, which needs to implement the methods by which it can attach or detach observers and keeps track of a certain good's current price:

using Observer.Observers;

namespace Observer.Subjects
{
    /// <summary>
    /// The Subject abstract class
    /// </summary>
    public abstract class Goods
    {
        private double pricePerKilo;
        private List<IRestaurant> restaurants = new List<IRestaurant>();

        protected Goods(double pricePerKilo)
        {
            this.pricePerKilo = pricePerKilo;
        }

        public void Attach(IRestaurant restaurant)
        {
            restaurants.Add(restaurant);
        }

        public void Detach(IRestaurant restaurant)
        {
            restaurants.Remove(restaurant);
        }

        public void Notify()
        {
            foreach(IRestaurant restaurant in restaurants)
            {
                restaurant.Update(this);
            }

            Console.WriteLine();
        }

        public double PricePerKilo
        {
            get { return pricePerKilo; }
            set
            {
                if(pricePerKilo != value)
                {
                    pricePerKilo = value;
                    Notify();
                }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

We also need some ConcreteSubjects which represent the price of specific products, in this case, Cucumber and Mutton.

namespace Observer.Subjects
{
    /// <summary>
    /// A ConcreteSubject class
    /// </summary>
    public class Cucumbers : Goods
    {
        public Cucumbers(double pricePerKilo) : base(pricePerKilo)
        {
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
namespace Observer.Subjects
{
    /// <summary>
    /// A ConcreteSubject class
    /// </summary>
    public class Mutton : Goods
    {
        public Mutton(double pricePerKilo) : base(pricePerKilo)
        {
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Now we can define our Observer participant. Remember that restaurants want to observe the vegetable prices, so our Observer will naturally be an interface IRestaurant, and this interface must define a method by which its implementors can be updated:

using Observer.Subjects;

namespace Observer.Observers
{
    public interface IRestaurant
    {
        public void Update(Goods goods);
    }
}
Enter fullscreen mode Exit fullscreen mode

Finally, we need our ConcreteObserver class, which represents specific restaurants. This class must implement the Update() method from IRestaurant:

using Observer.Subjects;

namespace Observer.Observers
{
    /// <summary>
    /// The ConcreteObserver class
    /// </summary>
    public class Restaurant : IRestaurant
    {
        private string name;
        private Dictionary<Goods, double> goodsThresholds;

        public Restaurant(string name)
        {
            this.name = name;
            goodsThresholds = new Dictionary<Goods, double>();
        }

        public void AddGoodsThreshold(Goods good, double threshold)
        {
            goodsThresholds.Add(good, threshold);
        }

        public void Update(Goods goods)
        {
            Console.WriteLine($"Notified {name} of {goods.GetType().Name}'s price change to {Math.Round(goods.PricePerKilo, 2)} per kilo");

            if (goods.PricePerKilo < goodsThresholds[goods])
                Console.WriteLine($"{name} wants to buy some {goods.GetType().Name}!");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

The restaurants will only buy goods if the price dips below a certain threshold amount, which differs per restaurant.

To put this all together, in our Main() method we can define a few restaurants that want to observe the price of cucumbers and mutton, and then fluctuate the price:

using Observer.Observers;
using Observer.Subjects;

Restaurant bobsBurgers = new Restaurant("Bob's Burgers");
Restaurant krustyBurger = new Restaurant("Krusty Burger");
Restaurant carrotPalace = new Restaurant("Carrot Palace");

Cucumbers cucumbers = new Cucumbers(0.82);
Mutton mutton = new Mutton(12.9);

bobsBurgers.AddGoodsThreshold(cucumbers, 0.77);
bobsBurgers.AddGoodsThreshold(mutton, 10.8);

krustyBurger.AddGoodsThreshold(mutton, 8.2);

carrotPalace.AddGoodsThreshold(cucumbers, 0.89);

cucumbers.Attach(bobsBurgers);
cucumbers.Attach(carrotPalace);

mutton.Attach(krustyBurger);
mutton.Attach(bobsBurgers);

// Create price fluctuation
cucumbers.PricePerKilo = 0.78;
cucumbers.PricePerKilo = 0.65;
cucumbers.PricePerKilo = 1.02;

mutton.PricePerKilo = 12.0;
mutton.PricePerKilo = 10.2;
mutton.PricePerKilo = 6.81;

Console.ReadKey();
Enter fullscreen mode Exit fullscreen mode

If we run the application, we will see that as the price changes, the restaurants get notified, and if the price drops below each restaurant's threshold, that restaurant wants to place an order. Below is the output:

Observer Demo Output

As you can see, the subject-object (Cucumbers and Mutton) automatically notify the observer restaurants of their price changes. which can then decide what to do with that information.

Difference Between Mediator and Observer

The difference between the Mediator and the Observer pattern is often elusive. In most cases, we can implement either of these patterns; but there are cases where we can implement both.

The primary goal of the Mediator is to eliminate dependencies among a set of components. These components instead become dependent on a single mediator object. The primary goal of the Observer, on the other hand, is to establish dynamic one-way connections between objects, where some objects act as subordinates of others.

One popular implementation of the Mediator pattern relies on the Observer pattern. The mediator plays the role of the publisher and the components act as subscribers to the mediator events. When the Mediator is implemented this way, it looks very similar to the Observer pattern.

When you are confused about whether you have a Mediator or an Observer remember that the Mediator can be implemented differently. For example, you can permanently link all components to the same mediator object. This implementation won't resemble the Observer pattern but will still be an instance of the Mediator pattern.

Now if you have a program where all components are publishers, allowing dynamic connections between each other, you have a distributed set of Observers, since there is no centralized Mediator object.

Pros and Cons of Observer Pattern

βœ” We can introduce new subscriber classes without having to change the publisher's code, thus satisfying the Open/Closed Principle ❌There is no specific order that the subscribers are notified
βœ” We can establish relations between objects at runtime

Relations with Other Patterns

  • Chain of Responsibility, Command, Mediator and Observer are patterns that address various ways of connecting senders and receivers of requests.
    • The Chain of Responsibility pattern passes a request sequentially along a dynamic chain of receivers until one of them handles it
    • The Command pattern establishes unidirectional communication channels between senders and receivers
    • The Mediator pattern eliminates direct connections between senders and receivers, forcing them to communicate indirectly via the mediator object.
    • The Observer pattern lets receivers dynamically subscribe to and unsubscribe from receiving requests.

Final Thoughts

In this article, we have discussed what is the Observer pattern, when to use it and what are the pros and cons of using this design pattern. We also examined how the Observer pattern relates to other classic design patterns.

The Observer pattern seeks to allow Observer objects to automatically receive notifications (and possibly change their own state) when a Subject class changes its state. In short, should the Subject change, the Observers will be notified about said change.

The Observer design pattern is helpful in many ways and is quite flexible if appropriately used. However, it's worth noting that the Observer pattern, along with the rest of the design patterns presented by the Gang of Four, is not a panacea or a be-all-end-all solution when designing an application. Once again it's up to the engineers to consider when to use a specific pattern. After all these patterns are useful when used as a precision tool, not a sledgehammer.

Top comments (2)

Collapse
 
mteheran profile image
Miguel Teheran

Another well explained patter.

Collapse
 
kalkwst profile image
Kostas Kalafatis

Thank you!