Observer
In short: Observer lets one object notify its subscribers when something happens, without knowing who they are; in C# this is an event.
Problem
The payment that calls everyone
A customer pays order #1042 for €80. After that, a receipt has to go out by email and SMS, the warehouse has to reserve the goods, and accounting has to issue an invoice. The payments class does it all itself:
class Payments{ private readonly EmailService email = new EmailService(); private readonly SmsService sms = new SmsService(); private readonly Warehouse warehouse = new Warehouse(); private readonly InvoiceService invoices = new InvoiceService();
public void MarkPaid(Order order) { email.SendReceipt(order); sms.NotifyPaid(order); warehouse.Reserve(order); invoices.Issue(order); }}Each service is a small class that prints what it did:
var payments = new Payments();payments.MarkPaid(new Order(1042, 80m));Email: receipt for #1042SMS: order #1042 is paidWarehouse: reserve #1042Invoice: €80.00 for #1042It works. But marketing wants loyalty points on every payment. That is one more field and one more line in MarkPaid. Tomorrow comes a push notification, the day after, statistics. The code every payment goes through is touched for things that have nothing to do with the payment.
Worse, today the SMS gateway is not responding:
class SmsService{ public void NotifyPaid(Order order) { throw new InvalidOperationException("SMS gateway is not responding"); }}var payments = new Payments();try{ payments.MarkPaid(new Order(1042, 80m));}catch (InvalidOperationException e){ Console.WriteLine("Payment failed: " + e.Message);}Email: receipt for #1042Payment failed: SMS gateway is not respondingThe money is taken, and the page tells the customer the payment failed. The warehouse reserved nothing, there is no invoice. One service nobody thinks is important stopped the whole payment.
The idea
Observer: whoever wants to know subscribes
The payments shouldn't know who cares about them. They should only know that someone wants to find out when an order is paid.
So the payments have an event (event): a list of subscribers built into the language. Every subscriber has the same shape, Action<Order>: a function that takes the order. Whoever wants to know subscribes from outside with +=. On a payment, the object raises the event, and it calls every subscriber with the order.This is the Observer pattern. It has three roles:
- the subject is where something happens. Here it is the payments. Books also call it the publisher;
- the subscribers (observers) want to find out. They are email, SMS, the warehouse;
- the notification is the walk through the list: every subscriber is given what happened, here the paid order.
A new subscriber is new code outside. The subject is not touched. A subscriber can also unsubscribe, and then it doesn't hear any more.
See it
The subscribers find out on their own
Paid event. Paid?.Invoke(order) calls every subscriber. With no subscribers the event is null, hence the ?.:record Order(int Id, decimal Total);
class Payments{ public event Action<Order> Paid;
public void MarkPaid(Order order) { Paid?.Invoke(order); }}Action<Order>:Action<Order> email = order => Console.WriteLine($"Email: receipt for #{order.Id}");Action<Order> sms = order => Console.WriteLine($"SMS: order #{order.Id} is paid");Action<Order> warehouse = order => Console.WriteLine($"Warehouse: reserve #{order.Id}");Action<Order> loyalty = order => Console.WriteLine($"Points: +{(int)order.Total} for #{order.Id}");Three subscribers, a payment. Then loyalty points arrive, and the customer turns SMS off:
var payments = new Payments();payments.Paid += email;payments.Paid += sms;payments.Paid += warehouse;payments.MarkPaid(new Order(1042, 80m));
payments.Paid += loyalty;payments.Paid -= sms;payments.MarkPaid(new Order(1043, 25m));Email: receipt for #1042SMS: order #1042 is paidWarehouse: reserve #1042Email: receipt for #1043Warehouse: reserve #1043Points: +25 for #1043var payments = new Payments();payments.Paid += email;payments.Paid += sms;payments.Paid += warehouse;payments.MarkPaid(new Order(1042, 80m));
payments.Paid += loyalty;payments.Paid -= sms;payments.MarkPaid(new Order(1043, 25m));Step by step, in words
- The Paid event has no subscribers yet.
- email subscribes with +=: it goes into the Paid event.
- sms and warehouse too. Payments doesn't know who they are, only that they take an order.
- Order #1042 is paid. The notification goes out to every subscriber, in the order they subscribed.
- One after another, in the same call: receipt, SMS, reservation. Each subscriber does its own job.
- Loyalty points arrive: loyalty subscribes from outside. Not a single line of Payments changes.
- The customer turns SMS off: sms unsubscribes and leaves the list.
- Order #1043: the notification reaches email, warehouse and loyalty. sms is not on the list any more and hears nothing.
Payments is the same code from start to end. The points appeared without a single line of it changing, and the SMS stopped without the payments noticing.
More precisely, if you are curious
- From outside, the event can only be subscribed to (
+=) and unsubscribed from (-=). Only the class that owns it can raise it or clear all subscribers. With a plain field of typeAction<Order>, anyone could writepayments.Paid = null. - You unsubscribe the same lambda you subscribed, so it is in a variable (
sms).-=with a new lambda, even with the same code, removes nothing. - In the .NET libraries, events are of type
EventHandler<T>: the subscriber also gets who raised the event (sender). You will see it in WPF and in the web frameworks. The idea is the same. - Subscribers are called one after another, in the same call to
MarkPaid. A slow SMS gateway slows down the payment too. Big shops send the event to a queue (a message broker), and the services pick it up from there when they are ready.
See it
One subscriber fails, the others shouldn't
Observer removed the link between the payment and the SMS, but not the error. The SMS subscriber throws:
Action<Order> sms = order => throw new InvalidOperationException("SMS gateway is not responding");var payments = new Payments();payments.Paid += email;payments.Paid += sms;payments.Paid += warehouse;try{ payments.MarkPaid(new Order(1042, 80m));}catch (InvalidOperationException e){ Console.WriteLine("Payment failed: " + e.Message);}Email: receipt for #1042Payment failed: SMS gateway is not respondingSubscribers are called in a loop, and the exception from the SMS leaves the loop. The warehouse still reserves nothing, and the error still reaches the payment. The subject has to protect every call on its own:
public void MarkPaid(Order order){ if (Paid == null) { return; } foreach (Action<Order> listener in Paid.GetInvocationList()) { try { listener(order); } catch (Exception e) { Console.WriteLine("A subscriber failed: " + e.Message); } }}With the same program:
var payments = new Payments();payments.Paid += email;payments.Paid += sms;payments.Paid += warehouse;try{ payments.MarkPaid(new Order(1042, 80m));}catch (InvalidOperationException e){ Console.WriteLine("Payment failed: " + e.Message);}Email: receipt for #1042A subscriber failed: SMS gateway is not respondingWarehouse: reserve #1042The payment went through, the warehouse reserved, and the failure is recorded. In a real shop this line goes to the log, and the SMS is retried later.
Paid?.Invoke(order) calls the subscribers one after another and stops at the first exception. GetInvocationList() gives you the subscribers one by one, so you can catch the error of each.Try it
Predict the output
The subscribers are the ones from the card "The subscribers find out on their own". What does the program print?
var payments = new Payments();payments.Paid += email;payments.Paid += warehouse;payments.Paid += email;payments.MarkPaid(new Order(7, 10m));
payments.Paid -= warehouse;payments.MarkPaid(new Order(8, 10m));Try it
Find the bug
var payments = new Payments();payments.MarkPaid(new Order(1042, 80m));Console.WriteLine("The bank statement is processed");Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.Which line has the bug?
Pick the line with the bug.
null. Paid.Invoke(order) calls a method on null and throws NullReferenceException. The nightly job fails on the first order. The fix is one character: Paid?.Invoke(order) calls nothing when there are no subscribers. Subscribers are optional, so the subject must not depend on whether there are any.More precisely, if you are curious
A subscriber that unsubscribes during the notification is safe in C#. The delegate of the event doesn't change: -= makes a new list, and the current notification goes on over the old one:
var payments = new Payments();Action<Order> coupon = null;coupon = order =>{ Console.WriteLine($"Welcome coupon: #{order.Id}"); payments.Paid -= coupon;};payments.Paid += coupon;payments.Paid += email;payments.MarkPaid(new Order(1042, 80m));payments.MarkPaid(new Order(1043, 25m));Welcome coupon: #1042Email: receipt for #1042Email: receipt for #1043With a List<Action<Order>> of your own and foreach, the same thing throws InvalidOperationException (Collection was modified). In Java it is ConcurrentModificationException, and in Python the subscriber after the removed one is skipped with no error.
The idea
When you don't need it
Observer has a price. The code doesn't say who will react to the payment: to find out, you search for every place that subscribes. A subscriber nobody unsubscribes lives in memory as long as the subject lives. It's not worth it when:
- there is one reaction and it is always the same. One call is clearer than a list with one subscriber;
- the step is part of the action itself. If the payment is invalid without an invoice, the invoice is not a subscriber: it is called directly, and its failure stops the payment;
- the order matters and the steps depend on each other (first the warehouse, then the payment, then the invoice). That is a sequence, not an event. How to put it in one place is in the lesson "Adapter and Facade".
It's worth it when the reactions are many, they grow, and the subject must not depend on them: notifications, points, statistics, logs.
Exercise
The invoice as a subscriber
Solve the task in your editor.
Invoice. Make the invoice a subscriber: a lambda invoice of type Action<Order> that prints the amount with two digits after the point. Subscribe email, the warehouse and the invoice, and pay order #1042 for €80. The payments are not touched.
Email: receipt for #1042Warehouse: reserve #1042Invoice: €80.00 for #1042More tasks
- A gift over €100. A subscriber that prints "Gift: free delivery for #…" only for orders over €100. Pay one for €80 and one for €120.
- The coupon that unsubscribes itself. Write the coupon subscriber from "Find the bug" and payments that handle it. Check that it works with
eventwith no change, then try aList<Action<Order>>of your own and see the error. - Two events. Add refunds too. A second event
Refundedand aMarkRefundedmethod. Email listens to both, the warehouse only to the payment.
Test
Check yourself
Related lessons
Section titled "Related lessons"- Builds on: Lambdas and delegates · record, switch and patterns · Design patterns · Strategy
- Continues in: The shop: which pattern where · Events and queues · Transactional outbox
- Tasks on this topic: Design patterns