Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Design Patterns (GoF and modern)  Module 1 – Why patterns, and when not to use them

The design principles behind the patterns

Program to an interface, prefer composition over inheritance and hide what varies: the principles behind the patterns, with SOLID, Liskov and Demeter.

  • Beginner
  • 30 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7, Node.js 24.21.0 and quickjs 0.32.0
  • By MySmartCoPilot
Free: 30 minutes

What you will learn

  • Apply "program to an interface, not an implementation" in a short refactor
  • Choose composition over inheritance and explain what it allows while the program runs
  • Identify what varies in a requirement and hide it behind a stable interface
  • Map patterns to SOLID, information hiding, the Liskov substitution principle and the Law of Demeter

Before you start

What you’ll learn · Before you start

What you will learn

  • Apply "program to an interface, not an implementation" in a short refactor
  • Choose composition over inheritance and explain what it allows while the program runs
  • Identify what varies in a requirement and hide it behind a stable interface
  • Map patterns to SOLID, information hiding, the Liskov substitution principle and the Law of Demeter

Before you start

On this page

The 23 classic patterns rest on a few principles. Design Patterns states two of them in its first chapter: program to an interface, not an implementation, and favour object composition over class inheritance. A third runs through the whole catalogue: find what varies and hide it behind a stable interface, which is the idea David Parnas published in 1972 as information hiding. Each pattern applies these principles to one recurring problem, so knowing them lets you rebuild a pattern you have half forgotten, and tells you when a pattern would cost more than it gives.

This lesson works through the three principles in code, then ties the patterns to the principles you will hear in design discussions: SOLID, the Liskov substitution principle and the Law of Demeter. It ends with what to do when two principles disagree, because they often do.

Program to an interface, not an implementation

Code that uses an object should depend only on the operations it needs, not on the class that happens to provide them. Then any object with those operations can be passed in: the real one, a faster one, or a test double that records what it was asked to do. Here is a notifier that ignores the principle. It inherits from an email sender, so it is an email sender:

A notifier that is an email sender Python · notifier/inherit.py
"""A notifier that inherits from the email sender: every notification is an email, fixed when the class is written."""


class EmailSender:
    def send(self, to, text):
        # Stands in for a real mail call (nothing leaves this program): it prints what it would send.
        print(f"  email to {to}: {text}")


class OrderNotifier(EmailSender):
    """Inherits send(): an OrderNotifier is an EmailSender."""

    def order_shipped(self, customer, order_id):
        self.send(customer, f"Order {order_id} has shipped")


OrderNotifier().order_shipped("asha@example.com", 1042)


# A test that must not send mail has to subclass the notifier and override the method it inherited.
class SilentNotifier(OrderNotifier):
    def __init__(self):
        self.sent = []

    def send(self, to, text):
        self.sent.append((to, text))


silent = SilentNotifier()
silent.order_shipped("ravi@example.com", 7)
print("recorded by the override:", silent.sent)
print("classes for email, SMS and a test double:", ["OrderNotifier", "SmsOrderNotifier", "SilentNotifier"])

Output

  email to asha@example.com: Order 1042 has shipped
recorded by the override: [('ravi@example.com', 'Order 7 has shipped')]
classes for email, SMS and a test double: ['OrderNotifier', 'SmsOrderNotifier', 'SilentNotifier']

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 inherit.py

Everything about the channel was decided when the class was written. A test that must not send mail has to subclass the notifier and override a method it inherited, and SMS needs yet another subclass. The second version is written against an interface, a Channel with one operation, send(to, text), and receives its channel from outside:

A notifier that holds a channel

Python · notifier/compose.py

"""The same notifier holding a channel: new channels and test doubles plug in without a subclass."""
from typing import Protocol


class Channel(Protocol):
    """The interface the notifier is written against: anything with send(to, text)."""

    def send(self, to: str, text: str) -> None: ...


class EmailChannel:
    def send(self, to, text):
        print(f"  email to {to}: {text}")


class SmsChannel:
    def send(self, to, text):
        print(f"  SMS to {to}: {text}")


class RecordingChannel:
    """A test double: it keeps what would have been sent."""

    def __init__(self):
        self.sent = []

    def send(self, to, text):
        self.sent.append((to, text))


class OrderNotifier:
    def __init__(self, channel: Channel):
        self._channel = channel  # a Channel, whichever class it is

    def order_shipped(self, customer, order_id):
        self._channel.send(customer, f"Order {order_id} has shipped")


OrderNotifier(EmailChannel()).order_shipped("asha@example.com", 1042)

recorder = RecordingChannel()
OrderNotifier(recorder).order_shipped("ravi@example.com", 7)
print("recorded by the test double:", recorder.sent)

# The channel can be chosen from data while the program runs, for example each customer's preference.
channels = {"email": EmailChannel(), "sms": SmsChannel()}
for customer, preference in [("meena@example.com", "email"), ("+1 555 0100", "sms")]:
    OrderNotifier(channels[preference]).order_shipped(customer, 1043)

Output

  email to asha@example.com: Order 1042 has shipped
recorded by the test double: [('ravi@example.com', 'Order 7 has shipped')]
  email to meena@example.com: Order 1043 has shipped
  SMS to +1 555 0100: Order 1043 has shipped

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 compose.py

JavaScript · notifier/compose.mjs

// The same notifier holding a channel: new channels and test doubles plug in without a subclass.

class EmailChannel {
  send(to, text) {
    console.log(`  email to ${to}: ${text}`);
  }
}

class SmsChannel {
  send(to, text) {
    console.log(`  SMS to ${to}: ${text}`);
  }
}

class RecordingChannel {
  // A test double: it keeps what would have been sent.
  sent = [];
  send(to, text) {
    this.sent.push([to, text]);
  }
}

class OrderNotifier {
  #channel; // anything with send(to, text)
  constructor(channel) {
    this.#channel = channel;
  }
  orderShipped(customer, orderId) {
    this.#channel.send(customer, `Order ${orderId} has shipped`);
  }
}

new OrderNotifier(new EmailChannel()).orderShipped('asha@example.com', 1042);

const recorder = new RecordingChannel();
new OrderNotifier(recorder).orderShipped('ravi@example.com', 7);
console.log('recorded by the test double:', JSON.stringify(recorder.sent));

// The channel can be chosen from data while the program runs, for example each customer's preference.
const channels = { email: new EmailChannel(), sms: new SmsChannel() };
for (const [customer, preference] of [['meena@example.com', 'email'], ['+1 555 0100', 'sms']]) {
  new OrderNotifier(channels[preference]).orderShipped(customer, 1043);
}

Output

  email to asha@example.com: Order 1042 has shipped
recorded by the test double: [["ravi@example.com","Order 7 has shipped"]]
  email to meena@example.com: Order 1043 has shipped
  SMS to +1 555 0100: Order 1043 has shipped

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node compose.mjs

OrderNotifier never names a channel class. In Python the interface is a typing.Protocol, which any class satisfies just by having the method, and in JavaScript it is only a convention, an object with a send method. The test double is an ordinary class, and the last loop picks each customer’s channel from data while the program runs, which no subclass can do.

Favour composition over inheritance

The Gang of Four compare two ways to reuse code. A subclass reuses its parent from the inside: it sees the parent’s implementation, so a change in the parent can break it, and the combination is fixed when the class is written. They call this white-box reuse. An object that holds another object reuses it from the outside, through its interface only, and the object it holds can be chosen, and replaced, while the program runs: black-box reuse. The cost of composition is one more object to create and wire together.

The difference grows with the number of things that vary. Three reports that must each come in two formats need six subclasses if every pair is a class, and a third format adds three more. Split the two things that vary instead, a report that holds a format, and you write five classes, and a new format adds one:

Six report classes, one per report and format pair, against a Report that holds a Format, five classes in all.Inheritance: one class per pair6 classes; a new format adds 3Composition: a report holds a format5 classes; a new format adds 1SalesCsvReportStockCsvReportStaffCsvReportSalesHtmlReportStockHtmlReportStaffHtmlReportReportSales · Stock · StaffFormatCsv · Htmlholds a

Three reports in two formats: by inheritance, then by composition

Text description of the diagram

Two designs for three reports (sales, stock and staff) in two formats (CSV and HTML).

The first panel, "Inheritance: one class per pair", is a grid of six classes: SalesCsvReport, StockCsvReport and StaffCsvReport, then SalesHtmlReport, StockHtmlReport and StaffHtmlReport. Each pair of a report and a format is its own class.

The second panel, "Composition: a report holds a format", has two boxes. A Report box lists its three kinds, Sales, Stock and Staff, and an arrow labelled "holds a" leads from it to a Format box, which lists Csv and Html.

Each panel title also counts the classes. The inheritance design has 6 classes, and a new format adds 3 more, one per report. The composition design has 5 classes, and a new format adds 1.

That second shape is the Bridge pattern, one of the structural patterns of this track, and the exercise of this lesson builds it. Inheritance is still the right tool when the subclass truly is a kind of its parent, keeps every promise the parent makes, and the parent rarely changes, or when a framework asks you to fill in the steps of an algorithm it controls, which is the Template Method pattern.

Encapsulate what varies

In 1972 David Parnas compared two ways of dividing one program into modules. The usual way followed the steps of the processing. His way gave each module one design decision that was likely to change, such as a data format or how lines are stored, and hid that decision from every other module. When a decision changed, only its module changed. “Encapsulate what varies” is the same advice in three words.

To apply it, list the changes a requirement is likely to bring, from the product plan, the history of similar code or plain questions such as “will there be another channel?”. Name the axis of each change and put it behind an interface; the rest of the code then never names a concrete choice. Most patterns are this move applied to one axis:

  • Strategy hides which algorithm runs, such as a pricing rule.
  • Observer hides who is listening for a change.
  • Abstract Factory hides which family of related objects is created.
  • Iterator hides how a collection is walked.
  • Adapter hides that an interface you depend on does not match the one you have.

The lesson on coupling, cohesion and information hiding in the low-level design track measures this criterion on two designs.

The Liskov substitution principle

Programming to an interface only works if every implementation keeps the interface’s promises. Barbara Liskov and Jeannette Wing defined this in 1994 as behavioural subtyping: objects of a subtype must behave as the supertype’s specification says, so that code written against the supertype works with any subtype. A subtype breaks the principle in two common ways. It can demand more than the contract, refusing calls the supertype accepts, or it can promise less, skipping something the supertype guarantees.

One contract test, run against every implementation, catches both. The contract of Shelf says that put(item) adds the item, so count() grows by one and the item appears in items():

One contract test for four shelves Python · contracts/contract_test.py
"""One contract test, run against every implementation of the Shelf interface."""
import bisect
from abc import ABC, abstractmethod


class Shelf(ABC):
    """The contract: put(item) adds the item, so count() grows by one and item is in items()."""

    @abstractmethod
    def put(self, item): ...

    @abstractmethod
    def items(self): ...

    def count(self):
        return len(self.items())


class ListShelf(Shelf):
    def __init__(self):
        self._items = []

    def put(self, item):
        self._items.append(item)

    def items(self):
        return list(self._items)


class SortedShelf(Shelf):
    """Keeps its items sorted: the contract says nothing about order, so this is still a Shelf."""

    def __init__(self):
        self._items = []

    def put(self, item):
        bisect.insort(self._items, item)

    def items(self):
        return list(self._items)


class UniqueShelf(Shelf):
    """Ignores an item it already holds: a weaker promise than the contract makes."""

    def __init__(self):
        self._items = {}  # a dict keeps the order items arrived in

    def put(self, item):
        self._items.setdefault(item, None)

    def items(self):
        return list(self._items)


class FrozenShelf(Shelf):
    """A read-only shelf: put() refuses, a stronger demand than the contract makes."""

    def __init__(self, items=()):
        self._items = tuple(items)

    def put(self, item):
        raise TypeError("this shelf is read-only")

    def items(self):
        return list(self._items)


def check_contract(make_shelf):
    shelf = make_shelf()
    for item in ["mug", "lamp", "mug"]:
        before = shelf.count()
        try:
            shelf.put(item)
        except Exception as error:
            return f"FAIL put({item!r}) raised {type(error).__name__}: {error}"
        if shelf.count() != before + 1:
            return f"FAIL put({item!r}) left count() at {shelf.count()}, expected {before + 1}"
        if item not in shelf.items():
            return f"FAIL {item!r} is missing from items()"
    return "pass"


for implementation in (ListShelf, SortedShelf, UniqueShelf, FrozenShelf):
    print(f"{implementation.__name__:<12} {check_contract(implementation)}")

Output

ListShelf    pass
SortedShelf  pass
UniqueShelf  FAIL put('mug') left count() at 2, expected 3
FrozenShelf  FAIL put('mug') raised TypeError: this shelf is read-only

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 contract_test.py

SortedShelf passes even though it reorders the items, because the contract says nothing about order: changing how an operation works is allowed. UniqueShelf promises less, since a second mug leaves the count where it was, and FrozenShelf demands more, since it refuses put() altogether. A read-only shelf is a perfectly good type; it is just not a Shelf, and the fix is a smaller interface without put() that both kinds of shelf can implement. Every pattern that works through an interface, Strategy, State, Composite, Decorator and Proxy among them, relies on this principle. The Liskov substitution lesson of the low-level design track covers the rules of the contract in depth.

SOLID, mapped to patterns

Robert C. Martin collected five principles of class design in the 1990s, and they became known as SOLID. Each one has patterns that put it into practice:

Principle What it asks Patterns that apply it
Single responsibility Keep together what changes for the same reason, and separate what changes for different reasons Strategy and Command move an algorithm or a request out of a class; Visitor moves operations out of the classes they work on
Open-closed Add behaviour by adding code, not by editing code that works Strategy, Decorator, Observer, Template Method
Liskov substitution Implementations keep the contract of their interface Every pattern built on an interface: Strategy, State, Composite, Decorator, Proxy
Interface segregation Keep interfaces small, so that callers depend only on what they use Role interfaces such as a one-method subscriber in Observer; Adapter narrows an interface for one caller
Dependency inversion High-level code depends on abstractions, and the details implement them Dependency injection, Abstract Factory, Factory Method

The low-level design track has a lesson for each principle, such as the single responsibility principle and the open-closed principle. Here it is enough to see that a pattern is often the concrete form of a principle: “open for extension” in code usually means a Strategy or a Decorator.

The Law of Demeter

The Law of Demeter is a style rule from Northeastern University, found in 1987 by Ian Holland and published by Karl Lieberherr, Ian Holland and Arthur Riel at the OOPSLA conference in 1988. Its slogan is “only talk to your immediate friends”. A method should call methods only on its own object, on its parameters, on objects it creates and on the objects its object holds directly. A chain such as order.customer.address.city breaks it: the caller now depends on the customer, the address and their fields, so renaming one field breaks every chain that passes through it.

Asking the nearest object instead, order.shipping_city(), keeps that knowledge in one place. Facade and Mediator are two patterns that honour the law at a larger scale, by giving callers one object to talk to instead of a web of others. Applied mechanically, though, the law produces classes full of forwarding methods, which is its own kind of cost.

When principles conflict

The principles pull against each other, and much of design is choosing which one wins in a given place:

  • Don’t repeat yourself against decoupling. Two similar functions that change for different reasons, one with tax rules and one with shipping rules, should stay separate; sharing them would tie the two reasons together.
  • Open-closed against “you aren’t gonna need it”. A seam for a change that never comes is pure cost, a subject of the lesson on when patterns hurt.
  • Demeter against small interfaces. Every forwarding method that hides a chain makes the object’s interface bigger.
  • Composition against simplicity. Every object you inject is one more thing to create, wire and read.

Patterns are recorded trade-offs, not laws. That is why every pattern lesson in this track lists the consequences: so you can see which principle a pattern serves and what it charges for it.

Practise: six subclasses into two hierarchies

Three reports in two formats, one subclass per pair. Split them into report classes that hold a format, so that the Markdown format in the test file works with every report without an edit. The sample tests check all six outputs against the old ones and read your source to check the split.

Exercise · Medium · Python

Replace six report subclasses with composition

reports.py has three reports (sales, stock and staff) in two formats (CSV and HTML), and one subclass for each pair: six classes, and a third format would need three more. Split the two things that vary into two small hierarchies joined by composition:

1. Two format classes, CsvFormat and HtmlFormat, each with render(title, header, rows) returning the text. A row is a sequence of values, and header is a list of column names. 2. Three report classes, SalesReport, StockReport and StaffReport, each created with a format, SalesReport(CsvFormat()), and each with render(), which works out the report's title, header and rows and hands them to its format.

Every report must render exactly what its old subclass rendered, so move the code, do not rewrite the output. The sample tests compare all six outputs with the old ones, then render every report with a Markdown format that the test file defines, which must work without any edit to your report classes. They also read your source: the six old classes must be gone, the report classes may not contain CSV or HTML syntax (no string with a comma or <), and the format classes may not mention any report.

Starter code · reports.py

"""Three reports in two formats, one subclass for each pair: a new format needs three new classes."""

SALES = [("north", 120), ("south", 95), ("east", 143)]
STOCK = [("tea", 40, 250), ("biscuits", 12, 120)]  # item, quantity, unit price
STAFF = [("Asha", "support"), ("Ravi", "sales"), ("Meena", "accounts")]


class Report:
    def render(self):
        raise NotImplementedError


class SalesCsvReport(Report):
    def render(self):
        rows = [*SALES, ("total", sum(units for _, units in SALES))]
        return "\n".join(["Sales", "region,units", *(f"{a},{b}" for a, b in rows)])


class SalesHtmlReport(Report):
    def render(self):
        rows = [*SALES, ("total", sum(units for _, units in SALES))]
        cells = "".join(f"<tr><td>{a}</td><td>{b}</td></tr>" for a, b in rows)
        return f"<h2>Sales</h2><table><tr><th>region</th><th>units</th></tr>{cells}</table>"


class StockCsvReport(Report):
    def render(self):
        rows = [(item, qty, price, qty * price) for item, qty, price in STOCK]
        return "\n".join(["Stock", "item,qty,price,value", *(f"{a},{b},{c},{d}" for a, b, c, d in rows)])


class StockHtmlReport(Report):
    def render(self):
        rows = [(item, qty, price, qty * price) for item, qty, price in STOCK]
        cells = "".join(f"<tr><td>{a}</td><td>{b}</td><td>{c}</td><td>{d}</td></tr>" for a, b, c, d in rows)
        head = "".join(f"<th>{h}</th>" for h in ["item", "qty", "price", "value"])
        return f"<h2>Stock</h2><table><tr>{head}</tr>{cells}</table>"


class StaffCsvReport(Report):
    def render(self):
        return "\n".join(["Staff", "name,team", *(f"{a},{b}" for a, b in STAFF)])


class StaffHtmlReport(Report):
    def render(self):
        cells = "".join(f"<tr><td>{a}</td><td>{b}</td></tr>" for a, b in STAFF)
        return f"<h2>Staff</h2><table><tr><th>name</th><th>team</th></tr>{cells}</table>"
The sample tests · test_reports.py
import ast
import inspect

import reports
from reports import CsvFormat, HtmlFormat, SalesReport, StaffReport, StockReport

OLD = {
    ("sales", "csv"): "Sales\nregion,units\nnorth,120\nsouth,95\neast,143\ntotal,358",
    ("sales", "html"): "<h2>Sales</h2><table><tr><th>region</th><th>units</th></tr><tr><td>north</td><td>120</td></tr>"
    "<tr><td>south</td><td>95</td></tr><tr><td>east</td><td>143</td></tr><tr><td>total</td><td>358</td></tr></table>",
    ("stock", "csv"): "Stock\nitem,qty,price,value\ntea,40,250,10000\nbiscuits,12,120,1440",
    ("stock", "html"): "<h2>Stock</h2><table><tr><th>item</th><th>qty</th><th>price</th><th>value</th></tr>"
    "<tr><td>tea</td><td>40</td><td>250</td><td>10000</td></tr><tr><td>biscuits</td><td>12</td><td>120</td><td>1440</td></tr></table>",
    ("staff", "csv"): "Staff\nname,team\nAsha,support\nRavi,sales\nMeena,accounts",
    ("staff", "html"): "<h2>Staff</h2><table><tr><th>name</th><th>team</th></tr><tr><td>Asha</td><td>support</td></tr>"
    "<tr><td>Ravi</td><td>sales</td></tr><tr><td>Meena</td><td>accounts</td></tr></table>",
}
REPORTS = {"sales": SalesReport, "stock": StockReport, "staff": StaffReport}
FORMATS = {"csv": CsvFormat, "html": HtmlFormat}


class MarkdownFormat:
    """A third format, written here: it must work with every report and no edit to reports.py."""

    def render(self, title, header, rows):
        lines = [f"## {title}", "| " + " | ".join(header) + " |", "|" + " --- |" * len(header)]
        lines += ["| " + " | ".join(str(value) for value in row) + " |" for row in rows]
        return "\n".join(lines)


def test_same_output_as_the_six_old_classes():
    """every report renders exactly what its old subclass rendered, in both formats"""
    for (report, fmt), expected in OLD.items():
        assert REPORTS[report](FORMATS[fmt]()).render() == expected, f"{report} as {fmt}"


def test_a_third_format_needs_no_edit_to_the_reports():
    """the Markdown format of this test file works with all three reports"""
    assert SalesReport(MarkdownFormat()).render().splitlines()[-1] == "| total | 358 |"
    assert StockReport(MarkdownFormat()).render().splitlines()[1] == "| item | qty | price | value |"
    assert StaffReport(MarkdownFormat()).render() == (
        "## Staff\n| name | team |\n| --- | --- |\n| Asha | support |\n| Ravi | sales |\n| Meena | accounts |"
    )


def test_the_six_subclasses_are_gone():
    """one class per report and one per format, not one per pair"""
    old = [n for n in vars(reports) if n.endswith(("CsvReport", "HtmlReport"))]
    assert old == [], f"still defined: {old}"


def strings_in(cls):
    tree = ast.parse(inspect.getsource(cls))
    return [n.value for n in ast.walk(tree) if isinstance(n, ast.Constant) and isinstance(n.value, str)]


def test_reports_know_no_format_and_formats_know_no_report():
    """formatting lives only in the formats, data only in the reports"""
    for cls in REPORTS.values():
        syntax = [s for s in strings_in(cls) if "," in s or "<" in s]
        assert syntax == [], f"{cls.__name__} contains format syntax: {syntax}"
    for cls in FORMATS.values():
        source = inspect.getsource(cls)
        named = [word for word in ("Sales", "Stock", "Staff", "SALES", "STOCK", "STAFF") if word in source]
        assert named == [], f"{cls.__name__} mentions {named}"
A hint

Look at what each pair of old classes repeats: SalesCsvReport and SalesHtmlReport compute the same rows and differ only in how they print them. So a report's job is to produce three things, a title, a header and rows (the sales total and the stock values are rows too), and a format's job is to turn any title, header and rows into text. Start with CsvFormat.render, copying the string-building from a CSV class and replacing the sales data with its parameters.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Python Online Compiler Run the notifier and the contract test, and add your own shelf to the contract test. JavaScript & TypeScript Online Compiler Try the JavaScript notifier with a channel of your own.

Check yourself

9 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 9 In compose.py, what does "program to an interface, not an implementation" mean for OrderNotifier?

    Choose one answer.

    Show the answer to question 1

    Answer: It calls only send(to, text), so any object with that method works, including a test double

    The notifier depends on one operation, not on a class. Inheriting from an abstract base class is one way to state that interface, but the protocol here needs no inheritance at all; creating an EmailChannel inside, or checking classes, would tie the notifier to implementations again.

  2. Question 2 of 9 What can the composed notifier do that the inheriting one cannot?

    Choose one answer.

    Show the answer to question 2

    Answer: Get its channel from data while the program runs, such as each customer's preference

    Inheritance fixes the behaviour when the class is written: an OrderNotifier is an EmailSender, and SMS needs another subclass. Composition picks the behaviour by passing an object, which can come from a dictionary, a setting or a test.

  3. Question 3 of 9 UniqueShelf.put("mug") does nothing when a mug is already on the shelf. Why does that break the Liskov substitution principle?

    Choose one answer.

    Show the answer to question 3

    Answer: It promises less than the Shelf contract, which says count() grows by one after every put()

    Code written against Shelf may rely on count() growing after put(), and UniqueShelf quietly breaks that promise: a weaker postcondition. How a subtype stores its items is its own business, and changing how an operation works is allowed, as SortedShelf shows, as long as the contract still holds.

  4. Question 4 of 9 FrozenShelf.put() raises TypeError. Which kind of contract break is that?

    Choose one answer.

    Show the answer to question 4

    Answer: A stronger precondition, because callers that may put() on any Shelf now fail

    The supertype accepts put() on any shelf; the subtype demands that you never call it. A read-only shelf is a fine type, but it is not a Shelf: give it an interface without put(), and let Shelf extend that interface.

  5. Question 5 of 9 Which line breaks the Law of Demeter?

    Read the code, then choose one answer.

    class Invoice:
        def __init__(self, order):
            self.order = order
    
        def heading(self):
            number = self.order.number                # line A
            city = self.order.customer.address.city   # line B
            stamp = self.make_stamp()                 # line C
            return f"{number} {city} {stamp}"
    Show the answer to question 5

    Answer: Line B, because it reaches through the order's customer and address

    The order is a direct friend of the invoice, and so is the invoice itself, so lines A and C are fine. Line B talks to strangers, the customer and the address, so a change to either breaks the invoice; asking the order, order.shipping_city(), keeps the knowledge in one place.

  6. Question 6 of 9 What did Parnas propose as the criterion for dividing a system into modules?

    Choose one answer.

    Show the answer to question 6

    Answer: Each module hides one design decision that is likely to change

    His paper compares a decomposition by processing steps with one by hidden decisions, and shows that the second keeps likely changes inside one module. "Encapsulate what varies" is the same idea.

  7. Question 7 of 9 Which pattern or technique applies the dependency inversion principle most directly?

    Choose one answer.

    Show the answer to question 7

    Answer: Dependency injection, where code receives the abstractions it needs instead of creating concrete classes

    Dependency inversion says high-level code depends on abstractions and the details depend on them too. Injecting a Channel into OrderNotifier is exactly that; a Singleton or a type switch ties code to concrete choices.

  8. Question 8 of 9 Four reports must each be available in three formats. How many concrete classes does the composition design need, one per report and one per format?

    Type a number.

    Show the answer to question 8

    Answer: 7

    Four report classes and three format classes: 4 + 3 = 7. One subclass per pair would need 4 × 3 = 12, and each new format would add four more instead of one.

  9. Question 9 of 9 Two modules contain a similar ten-line function, but one changes with tax rules and the other with shipping rules. What should you do?

    Choose one answer.

    Show the answer to question 9

    Answer: Keep the two copies, because merging them would tie two unrelated reasons to change together

    "Don't repeat yourself" is about one piece of knowledge, not about similar-looking code. The two functions encode different decisions that change for different reasons, so sharing them would couple the modules. Principles conflict, and the reasons for change decide.

Interview questions

Fresher · What does “favour composition over inheritance” mean in practice?

It means getting behaviour by holding an object that provides it, rather than by inheriting from a class that has it. A notifier that holds a channel can be given an email channel, an SMS channel or a test double, chosen while the program runs, while a notifier that inherits from an email sender is an email sender forever. Composition also keeps classes from multiplying: three reports in two formats need five classes when a report holds a format, and six subclasses, one per pair, with inheritance. It is a preference, not a ban. Inheritance is right when the subclass truly is a kind of its parent, keeps every promise the parent makes, and the parent is stable. The cost of composition is wiring: one more object to create and pass in.

Key takeaways

  • Program to an interface: depend on the operations you need, so that any object that provides them, a test double included, can be passed in.
  • Favour composition over inheritance: an object you hold can be chosen and replaced while the program runs, and things that vary independently stay in separate classes.
  • Encapsulate what varies: give each likely change its own module or interface, as Parnas proposed in 1972; most patterns do exactly this for one axis.
  • Implementations must keep their interface’s contract, by Liskov and Wing’s definition of subtyping; one contract test run against every implementation checks it.
  • SOLID and the Law of Demeter connect to concrete patterns, and the principles conflict, so weigh them case by case.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.