Skip to content
wannacode

Why patterns

In short: A design pattern is a named, proven solution to a recurring problem in code; the classic 23 patterns fall into three families: creational, structural and behavioural.

Problem

Two paragraphs for one word

A code review in an online shop. Peter reads the checkout and leaves a comment on the highlighted line:

class Checkout
{
private readonly Dictionary<string, Func<decimal, decimal>> rules = new()
{
["card"] = amount => amount,
["cash on delivery"] = amount => amount + 2.50m,
["voucher"] = amount => Math.Max(amount - 10, 0),
};
public decimal Pay(string method, decimal amount)
{
return rules[method](amount);
}
}
var checkout = new Checkout();
Console.WriteLine($"€{checkout.Pay("cash on delivery", 80):F2}");
Console.WriteLine($"€{checkout.Pay("voucher", 80):F2}");
Output
€82.50
€70.00

Peter: I don't get what this is. The checkout keeps a table where every payment method is a separate rule. All the rules have the same shape: they take a sum and return a sum. While the program runs, the checkout takes the rule by the name the customer picked and calls it, without knowing which one it is. Is this on purpose? If installments arrive tomorrow, do I have to touch Pay?

Maria: It is on purpose. You don't touch Pay, you add a row to the table. The rule only computes and remembers nothing, so the checkout can ask it several times: for the preview, for the invoice, for the payment. Otherwise every question would give a different sum…

Two people, two paragraphs, and the most important thing is still not said: what happens if the name is not in the table?

Maria could have written one word: Strategy. Peter would have known what he was looking at and what to check. The rule only computes. A new method is a new row, and the checkout is not touched. An unknown name is caught before it reaches the table. Many people have already solved these questions, and they are written down under that name.

The idea

A pattern is the name of a solution

A design pattern is a proven solution to a problem that comes up in many projects, with a name everyone knows it by. It is not a library and it is not code to copy: every project writes it its own way. A pattern carries four things:

  • a name: one word instead of two paragraphs;
  • the problem it solves: for example "the rule changes, and the checkout must not be touched";
  • the roles: who holds what and who calls whom;
  • the cost: what you pay for it (more classes, one more jump when you read) and when it is not worth it.

Most of the names come from the book "Design Patterns" (1994) by four authors whom developers call the "Gang of Four". It describes 23 patterns in three families. The module goes through the most used ones:

Repository and layers is not from the book. It is an architectural pattern: where in the program the database lives.

The module ends with an exercise, The shop: which pattern where: a messy shop you organise yourself.

See it

The trap: a pattern for its own sake

The task is one: a 10% discount for orders over €100. Someone has just read about patterns:

interface IDiscountStrategy
{
decimal Apply(decimal amount);
}
class PercentOverThresholdDiscountStrategy : IDiscountStrategy
{
private readonly decimal percent;
private readonly decimal threshold;
public PercentOverThresholdDiscountStrategy(decimal percent, decimal threshold)
{
this.percent = percent;
this.threshold = threshold;
}
public decimal Apply(decimal amount)
{
return amount > threshold ? amount * (1 - percent / 100) : amount;
}
}
abstract class AbstractDiscountStrategyFactory
{
public abstract IDiscountStrategy CreateStrategy();
}
class DefaultDiscountStrategyFactory : AbstractDiscountStrategyFactory
{
public override IDiscountStrategy CreateStrategy()
{
return new PercentOverThresholdDiscountStrategy(10, 100);
}
}
class DiscountService
{
private readonly AbstractDiscountStrategyFactory factory;
public DiscountService(AbstractDiscountStrategyFactory factory)
{
this.factory = factory;
}
public decimal Discounted(decimal amount)
{
return factory.CreateStrategy().Apply(amount);
}
}
var service = new DiscountService(new DefaultDiscountStrategyFactory());
Console.WriteLine($"Total: €{service.Discounted(120):F2}");
Console.WriteLine($"Total: €{service.Discounted(80):F2}");
Output
Total: €108.00
Total: €80.00

Five classes, two patterns (Strategy and a factory) and a name like AbstractDiscountStrategyFactory. The same task, without a pattern:

static decimal Discounted(decimal amount)
{
return amount > 100 ? amount * 0.9m : amount;
}
Console.WriteLine($"Total: €{Discounted(120):F2}");
Console.WriteLine($"Total: €{Discounted(80):F2}");
Output
Total: €108.00
Total: €80.00

The output is the same. The difference is in the reading: to find out how big the discount is, in the first version you go through five classes, and in the second you read one line.

The patterns in the first version solve no problem at all. There is one discount, nobody changes it while the program runs, and nobody picks between discounts. When the second discount arrives and the customer has to pick, that is when the function becomes a strategy. Not earlier.

Try it

Which family, and is a pattern needed

1. Builder, Singleton and Factory Method belong to one family. Which one?
2. The courier's library wants sendParcel(city, grams), and your code calls ship(city, kilograms). Which family is the solution from?
3. The label shows the price "incl. VAT" or "excl. VAT". It is decided in one place, and the two variants have not changed in years. Do you need a pattern?
4. The customer picks one of five payment methods, and a new method arrives every month. Do you need a pattern?

The idea

When a pattern hurts

A pattern is an answer to a problem. Without the problem, only the cost is left:

  • "We might need it." An interface with one implementation, a factory for one class, a strategy with one variant. Everyone who reads the code pays for it, and nobody uses it.
  • The name says the pattern, not the job. AbstractDiscountStrategyFactory says how it is made, Discounted says what it does. If you can't describe the class without the words of the pattern, you probably don't need it.
  • A pattern from another language. Where a delegate (Func<decimal, decimal>) would do, an interface and a class for every rule are too much.
  • The pattern before the pain. The good order is the other way around. You write the simple thing. When the same switch shows up in a third place or the rules become five, you refactor to the pattern. By then you know exactly what changes.

You learn patterns to recognize them and name them in other people's code and in reviews. Not to put them everywhere.

Exercise

Spot the pattern

Solve this in your editor.

Three snippets from one shop. Run them, and for each one say: is there a pattern, which one is it (Strategy, Observer, Factory Method, Singleton or Adapter from the "Design patterns" lesson in the Modern C# module), and in one sentence: what problem it solves. One of the three needs no pattern.

A.

class Order
{
private readonly List<Action<string>> listeners = new();
public void OnShipped(Action<string> listener)
{
listeners.Add(listener);
}
public void Ship(string id)
{
foreach (var listener in listeners)
{
listener(id);
}
}
}
var order = new Order();
order.OnShipped(id => Console.WriteLine($"Email: order {id} has shipped"));
order.OnShipped(id => Console.WriteLine($"Warehouse: remove {id} from the queue"));
order.Ship("1042");
Output
Email: order 1042 has shipped
Warehouse: remove 1042 from the queue

B.

class CourierApi
{
public string SendParcel(string city, int grams)
{
return $"Parcel for {city}, {grams} g";
}
}
interface IShipping
{
string Ship(string city, decimal kilograms);
}
class CourierShipping : IShipping
{
private readonly CourierApi api = new();
public string Ship(string city, decimal kilograms)
{
return api.SendParcel(city, (int)Math.Round(kilograms * 1000));
}
}
IShipping shipping = new CourierShipping();
Console.WriteLine(shipping.Ship("Plovdiv", 1.25m));
Output
Parcel for Plovdiv, 1250 g

C.

static string PriceLabel(decimal price, bool withVat)
{
if (withVat)
{
return $"€{price * 1.20m:F2} incl. VAT";
}
return $"€{price:F2} excl. VAT";
}
Console.WriteLine(PriceLabel(25, true));
Console.WriteLine(PriceLabel(25, false));
Output
€30.00 incl. VAT
€25.00 excl. VAT
The answers
  • A is Observer. The order doesn't know who listens to it. The email and the warehouse subscribe, and Ship calls them in the order they subscribed. A new listener doesn't touch Order.
  • B is Adapter. CourierShipping translates your Ship (kilograms) into the foreign SendParcel (grams), without touching either of them.
  • C needs no pattern. Two variants, in one place, nobody changes them. Strategy here would be an interface and two classes instead of one if.

More tasks

  • When C needs a pattern. The shop starts selling in five countries with different VAT, and the customer picks the country. What changes? Write which pattern would help and why, without code.
  • The pattern that disappears. Take the five classes from the card "The trap" and replace them with one function. Count the lines before and after. The output must stay the same.
  • Your code. Find a switch or a chain of ifs in one of your projects that repeats in two places. Which word would you name it with in a review?

Test

Check yourself

1. What is a design pattern?
2. Why does the name of a pattern help in a code review?
3. What is a sign that a pattern is not needed?
4. What is the good order?
5. Adapter, Decorator and Composite are:

Made by Ivo Gergov as part of Evo Tech's campaign for accessible programming education ·