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.
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 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
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
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:
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
Runs on this device, in your browser. The first run of each language downloads it: Python (about 13.5 MB) or JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
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:
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, 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
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
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.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
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.
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.
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
- Design Patterns: Elements of Reusable Object-Oriented Software (chapter 1) (Addison-Wesley (InformIT))
- On the Criteria To Be Used in Decomposing Systems into Modules (Communications of the ACM (copy at Eindhoven University of Technology))
- A behavioral notion of subtyping (ACM Transactions on Programming Languages and Systems (copy at Carnegie Mellon University))
- The Principles of OOD (archived copy) (Robert C. Martin, via the Internet Archive)
- Solid Relevance (Robert C. Martin, The Clean Code Blog)
- Law of Demeter: general formulation (Northeastern University)
- Object-Oriented Programming: An Objective Sense of Style (OOPSLA 1988) (ACM SIGPLAN Notices)
- typing.Protocol (Python Software Foundation)
- abc: Abstract Base Classes (Python Software Foundation)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress