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 = new Map([
["card", (amount) => amount],
["cash on delivery", (amount) => amount + 2.5],
["voucher", (amount) => Math.max(amount - 10, 0)],
]);
pay(method, amount) {
return this.rules.get(method)(amount);
}
}
const checkout = new Checkout();
console.log(`€${checkout.pay("cash on delivery", 80).toFixed(2)}`);
console.log(`€${checkout.pay("voucher", 80).toFixed(2)}`);
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 JavaScript, 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.

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:

class DiscountStrategy {
apply(amount) {
throw new Error("apply is not implemented");
}
}
class PercentOverThresholdDiscountStrategy extends DiscountStrategy {
constructor(percent, threshold) {
super();
this.percent = percent;
this.threshold = threshold;
}
apply(amount) {
return amount > this.threshold ? amount * (1 - this.percent / 100) : amount;
}
}
class AbstractDiscountStrategyFactory {
createStrategy() {
throw new Error("createStrategy is not implemented");
}
}
class DefaultDiscountStrategyFactory extends AbstractDiscountStrategyFactory {
createStrategy() {
return new PercentOverThresholdDiscountStrategy(10, 100);
}
}
class DiscountService {
constructor(factory) {
this.factory = factory;
}
discounted(amount) {
return this.factory.createStrategy().apply(amount);
}
}
const service = new DiscountService(new DefaultDiscountStrategyFactory());
console.log(`Total: €${service.discounted(120).toFixed(2)}`);
console.log(`Total: €${service.discounted(80).toFixed(2)}`);
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:

function discounted(amount) {
return amount > 100 ? amount * 0.9 : amount;
}
console.log(`Total: €${discounted(120).toFixed(2)}`);
console.log(`Total: €${discounted(80).toFixed(2)}`);
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 JavaScript this is even clearer: a class that throws "not implemented" to look abstract is Java written in JavaScript. 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 JavaScript module), and in one sentence: what problem it solves. One of the three needs no pattern.

A.

class Order {
listeners = [];
onShipped(listener) {
this.listeners.push(listener);
}
ship(id) {
for (const listener of this.listeners) {
listener(id);
}
}
}
const order = new Order();
order.onShipped((id) => console.log(`Email: order ${id} has shipped`));
order.onShipped((id) => console.log(`Warehouse: remove ${id} from the queue`));
order.ship("1042");
Output
Email: order 1042 has shipped
Warehouse: remove 1042 from the queue

B.

class CourierApi {
sendParcel(city, grams) {
return `Parcel for ${city}, ${grams} g`;
}
}
class CourierShipping {
api = new CourierApi();
ship(city, kilograms) {
return this.api.sendParcel(city, Math.round(kilograms * 1000));
}
}
const shipping = new CourierShipping();
console.log(shipping.ship("Plovdiv", 1.25));
Output
Parcel for Plovdiv, 1250 g

C.

function priceLabel(price, withVat) {
if (withVat) {
return `€${(price * 1.2).toFixed(2)} incl. VAT`;
}
return `€${price.toFixed(2)} excl. VAT`;
}
console.log(priceLabel(25, true));
console.log(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 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 ·