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}")€82.50€70.00Peter: 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:
- Creational: how and where objects are made. Factory Method and Simple Factory, Builder, Singleton and Dependency Injection.
- Structural: how objects are put together into something bigger. Adapter and Facade, Decorator, Composite.
- Behavioral: who calls whom and who decides what. Strategy, Observer, Command, State, Template Method and Iterator.
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 intofor. 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}")Total: €108.00Total: €80.00Five 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 amountprint(f"Total: €{discounted(120):.2f}")print(f"Total: €{discounted(80):.2f}")Total: €108.00Total: €80.00The 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 withABC 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
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.
AbstractDiscountStrategyFactorysays how it is made,discountedsays 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
switchshows 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")Email: order 1042 has shippedWarehouse: remove 1042 from the queueB.
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))Parcel for Plovdiv, 1250 gC.
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))€30.00 incl. VAT€25.00 excl. VATThe answers
- A is Observer. The order doesn't know who listens to it. The email and the warehouse subscribe, and
shipcalls them in the order they subscribed. A new listener doesn't touchOrder. - B is Adapter.
CourierShippingtranslates yourship(kilograms) into the foreignsend_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
switchor a chain ofifs in one of your projects that repeats in two places. Which word would you name it with in a review?
Test
Check yourself
Related lessons
Section titled "Related lessons"- Builds on: Lists: append, remove and sort · dict and set · Lambdas · Design patterns
- Tasks on this topic: Design patterns