Design patterns
In short: A design pattern is a proven solution to a problem that comes up in many programs, for example Strategy, Observer, Factory Method, Singleton and Adapter.
Problem
The method that grows with every promotion
The price in the shop depends on the promotion:
static decimal FinalPrice(decimal price, string promo){ if (promo == "student") { return price * 0.9m; } else if (promo == "black friday") { return Math.Max(price - 20, 0); } else if (promo == "loyal customer") { return price * 0.95m; } return price;}Console.WriteLine($"€{FinalPrice(120, "student"):F2}");Console.WriteLine($"€{FinalPrice(120, "black friday"):F2}");Console.WriteLine($"€{FinalPrice(120, "none"):F2}");€108.00€100.00€120.00else if in this method, and often in two more: the one for the invoice and the one for the confirmation email. The method knows about all the promotions at once, and every change touches it. This problem is so common that its solution has a name.The idea
Design patterns
Patterns fall into three groups:
- Creational: how objects are made. For example Singleton (only one object of the class) and Factory Method (something else decides which class to create).
- Structural: how classes and objects are put together into bigger ones. For example Adapter (connects incompatible interfaces) and Composite (a tree of parts that you work with as one whole).
- Behavioral: how objects split the work and talk to each other. For example Strategy (a replaceable rule) and Observer (subscribers to an event).
Why they are worth knowing:
- you do not reinvent a solution that others have already tested;
- the code changes with less risk, because the part that changes is kept separate;
- the names are a shared language. "Put an adapter here" tells a colleague a whole structure in one word.
More precisely, if you are curious
Func, Action) do the job of Strategy, and events (event) the job of Observer. A pattern is not a goal: if one if does the job, you do not need a pattern.See it
Strategy: a replaceable rule
Func<decimal, decimal>: a function that takes a price and returns a price. A type for a function like this is called a delegate in C#. The checkout does not know which promotion it is, it only calls Discount(price):class Checkout{ public Func<decimal, decimal> Discount { get; set; }
public Checkout(Func<decimal, decimal> discount) { Discount = discount; }
public decimal Total(decimal price) { return Math.Max(Discount(price), 0); }}var checkout = new Checkout(price => price);Console.WriteLine($"No discount: €{checkout.Total(120):F2}");
checkout.Discount = price => price * 0.9m;Console.WriteLine($"Student: €{checkout.Total(120):F2}");
checkout.Discount = price => price - 20;Console.WriteLine($"Black Friday: €{checkout.Total(120):F2}");No discount: €120.00Student: €108.00Black Friday: €100.00Checkout does not change. The rule can also be changed while the program is running: you store a new lambda in checkout.Discount.This is Strategy: a family of rules, each of the same type (here Func<decimal, decimal>), so they are interchangeable. The compare lambda you pass to a list's Sort is also a strategy: Sort knows how to sort, and you say what to sort by.More precisely, if you are curious
In the books, the class that uses the strategy is called the "context" (here Checkout). In Java the strategy is an interface. In C# an interface works too, but then you cannot pass a lambda: a lambda converts to a delegate, and an interface is not a delegate.
interface IDiscountStrategy{ decimal Apply(decimal price);}
IDiscountStrategy student = price => price * 0.9m;error CS1660: Cannot convert lambda expression to type 'IDiscountStrategy' because it is not a delegate typeThat is why a replaceable rule made of one function is a delegate in C#. If the strategy has its own fields, it can be a class, and the checkout gets its method. new PercentDiscount(10).Apply, without parentheses, is a delegate to the Apply method of that object:
class PercentDiscount{ private decimal percent;
public PercentDiscount(decimal percent) { this.percent = percent; }
public decimal Apply(decimal price) { return price * (1 - percent / 100); }}var checkout = new Checkout(new PercentDiscount(10).Apply);Console.WriteLine($"Student: €{checkout.Total(120):F2}");Student: €108.00A class is the better choice when the strategy has its own fields or is long. A lambda, when it is one line.
See it
Observer: subscribers to an event
When the stock of a product changes, the website has to show the new count, and the warehouse has to reorder if there are only a few left. Tomorrow it may be an email to customers too. The product should not know about any of them.
class Product{ private string name; private int quantity; private List<Action<string, int>> listeners = new List<Action<string, int>>();
public Product(string name, int quantity) { this.name = name; this.quantity = quantity; }
public void Subscribe(Action<string, int> listener) { listeners.Add(listener); }
public int Quantity { get { return quantity; } set { if (value == quantity) { return; } quantity = value; foreach (var listener in listeners) { listener(name, quantity); } } }}var phone = new Product("Phone", 0);phone.Subscribe((name, qty) => Console.WriteLine($"Website: {name}, {qty} in stock"));phone.Subscribe((name, qty) =>{ if (qty < 3) { Console.WriteLine($"Warehouse: reorder {name}"); }});
phone.Quantity = 10;phone.Quantity = 2;Website: Phone, 10 in stockWebsite: Phone, 2 in stockWarehouse: reorder Phone- Whoever wants to know subscribes with
Subscribeand passes a lambda. Its type isAction<string, int>: a delegate that takes a name and a count and returns nothing. - The stock is a property
Quantity. Onphone.Quantity = 10itssetruns: the product goes through the list and calls each subscriber. Who they are and what they do is none of its business. - If the count has not changed, nobody is bothered.
var phone = new Product("Phone", 0);phone.Subscribe((name, qty) => Console.WriteLine($"Website: {name}, {qty} in stock"));phone.Subscribe((name, qty) =>{ if (qty < 3) { Console.WriteLine($"Warehouse: reorder {name}"); }});
phone.Quantity = 10;phone.Quantity = 2;Step by step, in words
- Phone with 0 in stock and an empty listeners list.
- The website subscribes: the lambda goes into the list.
- So does the warehouse. The product does not know who they are, only that it can call them with a name and a count.
- The stock becomes 10. The product goes through the list and calls each subscriber in the order they subscribed. The website shows the new count.
- The warehouse checks 10 < 3: no, so it does nothing.
- The stock becomes 2. The product goes through the list and calls each subscriber in the order they subscribed. The website shows the new count.
- The warehouse checks 2 < 3: yes, so it reorders.
This is Observer. The product is the observed object (the subject), and the listeners are the observers. Buttons in graphical programs work exactly like this: "when clicked, call this".
More precisely, if you are curious
- In C# the list of subscribers is built into the language: an event (
event).+=subscribes,-=unsubscribes, andStockChanged?.Invoke(...)calls all the subscribers. With no subscribers the event isnull, which is why there is?.: then nothing is called.
class Product{ private string name; private int quantity;
public event Action<string, int> StockChanged;
public Product(string name, int quantity) { this.name = name; this.quantity = quantity; }
public int Quantity { get { return quantity; } set { if (value == quantity) { return; } quantity = value; StockChanged?.Invoke(name, quantity); } }}- You unsubscribe the same lambda you subscribed, so you keep it in a variable. A subscriber that nobody unsubscribes stays in memory as long as the product does.
var phone = new Product("Phone", 5);Action<string, int> site = (name, qty) => Console.WriteLine($"Website: {qty} in stock");phone.StockChanged += site;phone.Quantity = 4;phone.StockChanged -= site;phone.Quantity = 3;Console.WriteLine("The website is no longer listening");Website: 4 in stockThe website is no longer listening- From outside, the event can only be subscribed to and unsubscribed from. Only the class itself can raise it:
phone.StockChanged("Phone", 0);error CS0070: The event 'Product.StockChanged' can only appear on the left hand side of += or -= (except when used from within the type 'Product')- In the old lecture the observed class is called
Subject, with the methodsattachandnotifyAllObservers, and the observer is an abstract class with a methodupdate(). The idea is the same, and with delegates you do not need a class for each observer. - The listeners are called one after another in the thread that changed the count. If one of them is slow, all of them wait.
Try it
Who will hear it?
Product is the one from the card before this one.
What will the program print?
var coffee = new Product("Coffee", 5);coffee.Subscribe((name, qty) => Console.WriteLine($"A: {qty}"));coffee.Quantity = 4;coffee.Quantity = 4;coffee.Subscribe((name, qty) => Console.WriteLine($"B: {qty}"));coffee.Quantity = 0;Quantity = 4 notifies A. The second one is not a change, and nobody hears anything. B subscribes after that and does not learn about past changes. At 0 both of them hear it, in the order they subscribed.See it
Factory Method: which class to create
new EmailNotification() in ten places, each with its own if.interface INotification{ void Send(string to, string text);}
class EmailNotification : INotification{ public void Send(string to, string text) { Console.WriteLine($"Email to {to}: {text}"); }}
class SmsNotification : INotification{ public void Send(string to, string text) { Console.WriteLine($"SMS to {to}: {text}"); }}static class NotificationFactory{ public static INotification Create(string channel) { return channel switch { "email" => new EmailNotification(), "sms" => new SmsNotification(), _ => throw new ArgumentException($"Unknown channel: {channel}"), }; }}string[,] customers ={ { "email", "maria@example.com" }, { "sms", "+359 88 123 4567" }, { "pigeon", "5 Shipka St." }};for (int i = 0; i < customers.GetLength(0); i++){ try { var n = NotificationFactory.Create(customers[i, 0]); n.Send(customers[i, 1], "Your order has been shipped."); } catch (ArgumentException e) { Console.WriteLine(e.Message); }}Email to maria@example.com: Your order has been shipped.SMS to +359 88 123 4567: Your order has been shipped.Unknown channel: pigeonINotification. Only the factory knows exactly which class is behind it. A new channel, for example push, is a new class and a new line in the switch. The sending is not touched.More precisely, if you are curious
- This is the most common form in practice. Its more precise name is "simple factory". The Factory Method from the GoF book is something else: the parent has an abstract method that creates the object, and the subclasses decide what kind:
abstract class Shop{ protected abstract INotification CreateNotification();
public void ConfirmOrder(string to) { CreateNotification().Send(to, "The order has been received."); }}
class OnlineShop : Shop{ protected override INotification CreateNotification() { return new EmailNotification(); }}
class PhoneShop : Shop{ protected override INotification CreateNotification() { return new SmsNotification(); }}new OnlineShop().ConfirmOrder("maria@example.com");new PhoneShop().ConfirmOrder("+359 88 123 4567");Email to maria@example.com: The order has been received.SMS to +359 88 123 4567: The order has been received.ConfirmOrder is written once, in Shop, and does not know what notification it sends.
- In old examples the factory returns
nullfor an unknown kind. Then the caller blows up with aNullReferenceExceptionfar from the real cause:
static class NotificationFactory{ public static INotification Create(string channel) { if (channel == "email") { return new EmailNotification(); } if (channel == "sms") { return new SmsNotification(); } return null; }}
var n = NotificationFactory.Create("pigeon");try{ n.Send("5 Shipka St.", "Your order has been shipped.");}catch (NullReferenceException e){ Console.WriteLine($"NullReferenceException: {e.Message}");}NullReferenceException: Object reference not set to an instance of an object.An exception with a clear message in the factory is better.
See it
Singleton: only one object
The application's settings, like the currency, are read once. If every part of the program makes its own settings object, they are read many times and can drift apart.
class AppConfig{ private static AppConfig instance;
public string Currency { get; } = "EUR";
private AppConfig() { Console.WriteLine("Reading the settings..."); }
public static AppConfig Instance { get { if (instance == null) { instance = new AppConfig(); } return instance; } }}var a = AppConfig.Instance;var b = AppConfig.Instance;Console.WriteLine(a == b);Console.WriteLine(b.Currency);Reading the settings...TrueEUR- The constructor is
private. Nobody outside the class can writenew AppConfig(). - The only object is in the static field
instance. The propertyInstancecreates it the first time it is read and then always returns that one. - "Reading the settings..." is printed once, and
a == bisTrue: there is one object.
This is Singleton.
More precisely, if you are curious
- If two threads (parts of the program that run at the same time) read
Instanceat the same moment, both may seenulland create two objects. - The safest Singleton in C# is a
static readonlyfield. The runtime initializes the static fields of a class exactly once, before the class is used, even when many threads ask for it at once:
class Settings{ public static readonly Settings Instance = new Settings();
public string Currency { get; } = "EUR";
private Settings() { Console.WriteLine("Creating the settings..."); }}Console.WriteLine(Settings.Instance.Currency);Console.WriteLine(Settings.Instance == Settings.Instance);Creating the settings...EURTrue- If creating the object is expensive and should happen only when it is needed, there is
Lazy<T>. The object is created on the first.Value, also once and also with many threads:
class AppConfig{ private static readonly Lazy<AppConfig> instance = new Lazy<AppConfig>(() => new AppConfig());
public string Currency { get; } = "EUR";
private AppConfig() { Console.WriteLine("Reading the settings..."); }
public static AppConfig Instance { get { return instance.Value; } }}Console.WriteLine("The program starts");var a = AppConfig.Instance;var b = AppConfig.Instance;Console.WriteLine(a == b);The program startsReading the settings...True- A Singleton is a global variable for the whole program with a nice name. Anyone can use it from anywhere, so it is hard to test and hard to replace. It is often better to create the object once in
Mainand pass it through the constructor to the ones that use it.
Try it
Find the bug
Order numbers have to be unique, so the counter is a Singleton. But the program gave out the number 1001 twice:
Console.WriteLine(OrderNumbers.Instance.NextNumber());Console.WriteLine(OrderNumbers.Instance.NextNumber());Console.WriteLine(new OrderNumbers().NextNumber());100110021001Which line in the class is the bug on?
Pick the line with the bug.
The constructor is public. Then anyone can write new OrderNumbers() and get a new counter that starts again from 1001. A Singleton is protected by one single line: private OrderNumbers(). With it, the third line in Main does not even compile:
error CS0122: 'OrderNumbers.OrderNumbers()' is inaccessible due to its protection levelSee it
Adapter: connects incompatible interfaces
The shop pays through an interface with an amount in euros and a "yes or no" answer. The bank gives you an old library that wants cents and returns an error code. The library is not yours, and you cannot change it.
// The bank's old library. You cannot change it.class LegacyBankClient{ // The amount is in cents. Returns 0 on success and an error code otherwise. public int Charge(long cents, string reference) { Console.WriteLine($"Bank: {cents} cents for {reference}"); return cents <= 50000 ? 0 : 51; }}You write a class that looks like what the shop expects, and inside it calls the bank:
// This is what the shop expects.interface IPaymentGateway{ bool Pay(string orderId, decimal amountEur);}
class BankAdapter : IPaymentGateway{ private LegacyBankClient bank;
public BankAdapter(LegacyBankClient bank) { this.bank = bank; }
public bool Pay(string orderId, decimal amountEur) { long cents = (long)Math.Round(amountEur * 100); return bank.Charge(cents, "ORDER-" + orderId) == 0; }}IPaymentGateway gateway = new BankAdapter(new LegacyBankClient());Console.WriteLine(gateway.Pay("1001", 49.90m));Console.WriteLine(gateway.Pay("1002", 1299.00m));Bank: 4990 cents for ORDER-1001TrueBank: 129900 cents for ORDER-1002Falsetrue on the way back. The shop does not know there is an old library behind it. If tomorrow you change banks, you write a new adapter, and the shop is not touched.This is Adapter: like a plug adapter that changes neither the plug nor the wall.
More precisely, if you are curious
The classic example in the books, and in the old lecture, is a media player. AudioPlayer can play only mp3, and an existing class AdvancedMediaPlayer has the methods playVlc and playMp4. MediaAdapter implements the player's interface and forwards to the right method. The structure is the same as here.
Exercise
Patterns in the shop
Solve the tasks in your editor.
1. Discounts as strategies. A static class Discounts with methods that return a lambda of type Func<decimal, decimal>: None(), Percent(decimal p) and Fixed(decimal amount) (never below €0). A class Cart with a list of prices and a replaceable discount in the property Discount. Add coffee beans (€18.50), honey (€12) and tea (€4.20), and print the total with no discount, with −10%, with −€5 and with −€50.
No discount: €34.70-10%: €31.23-€5: €29.70-€50: €0.002. "Notify me when it is in stock". The product keeps a list of customers who are waiting. When the stock goes from 0 to more than 0, it notifies all of them and clears the list, because the notification is one-time. The console has 0 in stock, Maria and Georgi subscribe, then the stock becomes 5, 0 and 3.
Email to Maria: Console is in stock (5 units)SMS to Georgi: Console is in stockEnd3. A notification factory. Add PushNotification and a factory for the three channels that ignores uppercase and lowercase. Read a number of lines and then lines "channel;address". Send "Your reservation is confirmed." to each address, and for an unknown channel print the exception's message.
4Email;maria@example.comsms;+359 88 123 4567push;Elena's phonefax;02 123 456Email to maria@example.com: Your reservation is confirmed.SMS to +359 88 123 4567: Your reservation is confirmed.Push to Elena's phone: Your reservation is confirmed.Unknown channel: faxOldThermometer returns the temperature in Fahrenheit with ReadFahrenheit(). The new system works with interface ITemperatureSensor { double Celsius(); }. Write ThermometerAdapter (°C = (°F − 32) × 5 / 9). In a list of sensors, put two old thermometers (71.6 °F and 39.2 °F) through the adapter, and one new sensor DigitalSensor that returns 21.5 °C. A lambda does not work here: the interface is not a delegate.Office: 22.0 °CWarehouse: 4.0 °CServer room: 21.5 °CMore tasks
- Shipping as a strategy. Make a
Func<decimal, decimal>for the shipping price to an office, to an address and from the store, and put the chosen one in the checkout together with the discount. - A log as a Singleton. A class
Loggerwith a propertyInstanceand a methodLog(string text)that numbers the lines. Check that the numbers continue when you call it from two different methods. - A price observer. Add an event for price changes to the product, and send a notification only when the price drops by at least 10%.
Test
Check yourself
The idea
Next: the patterns up close
Here you saw five patterns briefly, with lambdas. The Software engineering course has a whole module on patterns: each one starts from code that hurts, has an animation of which object calls which, and says when you don't need it. Builder, Decorator, Composite, Command, State, Template Method, Iterator and Repository are there too.
The module is here: Design patterns.Related lessons
Section titled "Related lessons"- Builds on: Lists · Nested classes · Lambdas and delegates · record, switch and patterns
- Continues in: Why patterns · Strategy · Observer · Factory Method and Simple Factory
- Tasks on this topic: Modern C#