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:
interface PaymentRule { double total(double amount);}
class Checkout { private final Map<String, PaymentRule> rules = Map.of( "card", amount -> amount, "cash on delivery", amount -> amount + 2.50, "voucher", amount -> Math.max(amount - 10, 0));
public double pay(String method, double amount) { return rules.get(method).total(amount); }}Checkout checkout = new Checkout();System.out.printf("€%.2f%n", checkout.pay("cash on delivery", 80));System.out.printf("€%.2f%n", checkout.pay("voucher", 80));€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 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.
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 DiscountStrategy { double apply(double amount);}
class PercentOverThresholdDiscountStrategy implements DiscountStrategy { private final double percent; private final double threshold;
public PercentOverThresholdDiscountStrategy(double percent, double threshold) { this.percent = percent; this.threshold = threshold; }
@Override public double apply(double amount) { return amount > threshold ? amount * (1 - percent / 100) : amount; }}
abstract class AbstractDiscountStrategyFactory { public abstract DiscountStrategy createStrategy();}
class DefaultDiscountStrategyFactory extends AbstractDiscountStrategyFactory { @Override public DiscountStrategy createStrategy() { return new PercentOverThresholdDiscountStrategy(10, 100); }}
class DiscountService { private final AbstractDiscountStrategyFactory factory;
public DiscountService(AbstractDiscountStrategyFactory factory) { this.factory = factory; }
public double discounted(double amount) { return factory.createStrategy().apply(amount); }}DiscountService service = new DiscountService(new DefaultDiscountStrategyFactory());System.out.printf("Total: €%.2f%n", service.discounted(120));System.out.printf("Total: €%.2f%n", service.discounted(80));Total: €108.00Total: €80.00Five classes, two patterns (Strategy and a factory) and a name like AbstractDiscountStrategyFactory. The same task, without a pattern:
static double discounted(double amount) { return amount > 100 ? amount * 0.9 : amount;}System.out.printf("Total: €%.2f%n", discounted(120));System.out.printf("Total: €%.2f%n", discounted(80));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.
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." 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.
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. Where a lambda 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
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 Java module), and in one sentence: what problem it solves. One of the three needs no pattern.
A.
class Order { private final List<Consumer<String>> listeners = new ArrayList<>();
public void onShipped(Consumer<String> listener) { listeners.add(listener); }
public void ship(String id) { for (Consumer<String> listener : listeners) { listener.accept(id); } }}Order order = new Order();order.onShipped(id -> System.out.println("Email: order " + id + " has shipped"));order.onShipped(id -> System.out.println("Warehouse: remove " + id + " from the queue"));order.ship("1042");Email: order 1042 has shippedWarehouse: remove 1042 from the queueB.
class CourierApi { public String sendParcel(String city, int grams) { return "Parcel for " + city + ", " + grams + " g"; }}
interface Shipping { String ship(String city, double kilograms);}
class CourierShipping implements Shipping { private final CourierApi api = new CourierApi();
@Override public String ship(String city, double kilograms) { return api.sendParcel(city, (int) Math.round(kilograms * 1000)); }}Shipping shipping = new CourierShipping();System.out.println(shipping.ship("Plovdiv", 1.25));Parcel for Plovdiv, 1250 gC.
static String priceLabel(double price, boolean withVat) { if (withVat) { return String.format("€%.2f incl. VAT", price * 1.20); } return String.format("€%.2f excl. VAT", price);}System.out.println(priceLabel(25, true));System.out.println(priceLabel(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 foreignsendParcel(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
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 · HashMap and HashSet · Lambdas · Design patterns
- Tasks on this topic: Design patterns