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:
RULES = {
"card": lambda amount: amount,
"cash on delivery": lambda amount: amount + 2.50,
"voucher": lambda amount: max(amount - 10, 0),
}
def pay(self, method, amount):
return self.RULES[method](amount)
checkout = Checkout()
print(f"€{checkout.pay("cash on delivery", 80):.2f}")
print(f"€{checkout.pay("voucher", 80):.2f}")
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 functions or 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.

In Python, some patterns almost disappear, because the language already has them. A function is a value, so Strategy and Command are often just a function. Walking through a collection (Iterator) is built into for. Every lesson in the module says where that is the case, instead of writing Java with a different syntax.The @ sign above a function in Python is called a decorator. It is related to the Decorator pattern, but it is not the same thing; the Decorator lesson shows both.

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:

from abc import ABC, abstractmethod
class DiscountStrategy(ABC):
@abstractmethod
def apply(self, amount):
pass
class PercentOverThresholdDiscountStrategy(DiscountStrategy):
def __init__(self, percent, threshold):
self.percent = percent
self.threshold = threshold
def apply(self, amount):
if amount > self.threshold:
return amount * (1 - self.percent / 100)
return amount
class AbstractDiscountStrategyFactory(ABC):
@abstractmethod
def create_strategy(self):
pass
class DefaultDiscountStrategyFactory(AbstractDiscountStrategyFactory):
def create_strategy(self):
return PercentOverThresholdDiscountStrategy(10, 100)
class DiscountService:
def __init__(self, factory):
self.factory = factory
def discounted(self, amount):
return self.factory.create_strategy().apply(amount)
service = DiscountService(DefaultDiscountStrategyFactory())
print(f"Total: €{service.discounted(120):.2f}")
print(f"Total: €{service.discounted(80):.2f}")
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:

def discounted(amount):
return amount * 0.9 if amount > 100 else amount
print(f"Total: €{discounted(120):.2f}")
print(f"Total: €{discounted(80):.2f}")
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.

In Python this is even clearer: an abstract class with ABC for a one-line rule is Java written in Python. A function is already a value you can pass around and keep.

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." A class with one method where a function would do, a factory for one class, a table with one rule. 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. A Java pattern copied class by class: an abstract class, a factory and a class with one method.
  • 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 Python module), and in one sentence: what problem it solves. One of the three needs no pattern.

A.

class Order:
def __init__(self):
self.listeners = []
def on_shipped(self, listener):
self.listeners.append(listener)
def ship(self, order_id):
for listener in self.listeners:
listener(order_id)
order = Order()
order.on_shipped(lambda order_id: print(f"Email: order {order_id} has shipped"))
order.on_shipped(lambda order_id: print(f"Warehouse: remove {order_id} from the queue"))
order.ship("1042")
Output
Email: order 1042 has shipped
Warehouse: remove 1042 from the queue

B.

class CourierApi:
def send_parcel(self, city, grams):
return f"Parcel for {city}, {grams} g"
class CourierShipping:
def __init__(self):
self.api = CourierApi()
def ship(self, city, kilograms):
return self.api.send_parcel(city, round(kilograms * 1000))
shipping = CourierShipping()
print(shipping.ship("Plovdiv", 1.25))
Output
Parcel for Plovdiv, 1250 g

C.

def price_label(price, with_vat):
if with_vat:
return f"€{price * 1.20:.2f} incl. VAT"
return f"€{price:.2f} excl. VAT"
print(price_label(25, True))
print(price_label(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 send_parcel (grams), without touching either of them.
  • C needs no pattern. Two variants, in one place, nobody changes them. Strategy here would be two functions and a table 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 ·