Low-Level Design (Object-Oriented Design) Module 4 – Design principles: SOLID and beyond
Dependency Inversion Principle (DIP)
The Dependency Inversion Principle in practice: make business rules own their abstractions, inject the details, and keep DIP, DI and containers apart.
What you will learn
- Design high-level policy that depends only on abstractions it owns and names in its own terms
- Build the object graph in one composition root, passing collaborators through constructors
- Distinguish dependency inversion, dependency injection and injection containers
- Test time-based rules exactly by passing in a clock and an id source
Before you start
What you’ll learn · Before you start
What you will learn
- Design high-level policy that depends only on abstractions it owns and names in its own terms
- Build the object graph in one composition root, passing collaborators through constructors
- Distinguish dependency inversion, dependency injection and injection containers
- Test time-based rules exactly by passing in a clock and an id source
Before you start
On this page
The Dependency Inversion Principle (DIP) says that the code holding your business rules should not depend on the code that talks to the outside world. Both should depend on an abstraction, and that abstraction belongs to the rules: it is written in their terms and lives next to them. The details, such as an SMS gateway, a database or the system clock, implement it. DIP is the last of the five SOLID principles. This lesson applies it to a service that no test can build, and then separates it from dependency injection and injection containers, which are often mistaken for it.
What the principle says
Robert C. Martin set out the principle in 1996, in a column for The C++ Report, as two rules. The first:
High level modules should not depend upon low level modules. Both should depend upon abstractions.
The second:
Abstractions should not depend upon details. Details should depend upon abstractions.
He chose the word “inversion” because the design methods of the time produced the opposite. A program was planned as a tree of calls, and each module depended on the modules it called: the rules at the top depended on the helpers at the bottom. Dependency is transitive, so a policy that uses a mechanism that uses a utility depends on that utility too, and a change at the bottom can force changes all the way up. Martin’s point was that the rules are what we want to keep and reuse, so they are the part that should depend on nothing volatile.
Inverting does not change which way the calls go: the rules still call the SMS code at run time. It changes which way the source code points. The rules define an interface, the SMS code imports that interface and implements it, and so the arrow in the source runs from the detail to the rules. Martin’s later one-line version is “Depend in the direction of abstraction.”
A service that builds its own details
A gym sends each member one SMS when their membership is about to end. The first version of the service does
everything itself: it creates its own SmsGateway, which needs the key of a live account, reads the members from a
module-level list and asks date.today() what day it is. Here is what happens when a test tries to build it:
before/try_to_test.py
"""A first attempt at testing the reminders: the test cannot even build the object."""
from reminders import RenewalReminders
try:
reminders = RenewalReminders()
except RuntimeError as error:
print("cannot build RenewalReminders in a test:")
print(" ", error) Output
cannot build RenewalReminders in a test: SMS_API_KEY is not set, and the gateway cannot start without a live account
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 try_to_test.py
before/reminders.py
"""Membership renewal reminders for a gym, first version: the service builds and finds everything it needs itself."""
from datetime import date
from sms_gateway import SmsGateway
# In the real program these rows come from the gym's database.
MEMBERS = [
("Asha", "+91 00000 11111", date(2025, 3, 15)),
("Ravi", "+91 00000 22222", date(2025, 3, 18)),
]
class RenewalReminders:
def __init__(self):
self.gateway = SmsGateway()
self.reminded = set()
def run(self) -> int:
today = date.today()
sent = 0
for name, phone, ends_on in MEMBERS:
days_left = (ends_on - today).days
if 0 <= days_left <= 3 and name not in self.reminded:
self.gateway.send_sms(phone, f"Hi {name}, your membership ends in {days_left} days.")
self.reminded.add(name)
sent += 1
return sent before/sms_gateway.py
"""A client for an SMS provider's web API. Building one needs a live account's key."""
import json
import os
class SmsGateway:
URL = "https://sms.example.com/v1/messages"
def __init__(self):
key = os.environ.get("SMS_API_KEY")
if not key:
raise RuntimeError("SMS_API_KEY is not set, and the gateway cannot start without a live account")
self._key = key
def send_sms(self, phone: str, text: str) -> None:
from urllib.request import Request, urlopen
body = json.dumps({"to": phone, "text": text}).encode()
request = Request(self.URL, data=body, headers={"Authorization": f"Bearer {self._key}"})
with urlopen(request, timeout=10):
pass 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
The constructor fails because the gateway wants a live key. Giving the test a real key would be worse: the test would send real messages to real members, and its result would depend on the day it runs, because the service reads the system clock. Three things the rules need are hidden inside the class, a gateway, a clock and the member data, and every one of them is a detail.
Inverting the dependency
In the second version, reminders.py holds the rules and the two abstractions they need. Notifier has one method,
remind(member, days_left), and Clock has today(). RenewalReminders receives a notifier, a clock and the
members in its constructor and knows nothing about SMS. The SMS code moves to sms_notifier.py, which imports
Member from reminders.py and implements Notifier with the gateway. A test can now pass a notifier that only
records what it was asked to send, and a clock fixed on one day:
after/test_reminders.py
"""Tests of the reminder policy with a fixed clock and a notifier that only records: no network, no key, no date."""
from datetime import date
from reminders import Member, RenewalReminders
class FixedClock:
def __init__(self, day):
self.day = day
def today(self):
return self.day
class RecordingNotifier:
def __init__(self):
self.sent = []
def remind(self, member, days_left):
self.sent.append((member.name, days_left))
members = [
Member("Asha", "+91 00000 11111", date(2025, 3, 15)), # ends in 3 days on the 12th
Member("Kabir", "+91 00000 33333", date(2025, 3, 12)), # ends on the 12th itself
Member("Meera", "+91 00000 44444", date(2025, 3, 11)), # ended the day before
Member("Ravi", "+91 00000 22222", date(2025, 3, 18)), # 6 days away on the 12th
]
clock = FixedClock(date(2025, 3, 12))
notifier = RecordingNotifier()
reminders = RenewalReminders(members, notifier, clock)
reminders.run()
print("on 2025-03-12:", notifier.sent)
assert notifier.sent == [("Asha", 3), ("Kabir", 0)]
notifier.sent.clear()
reminders.run()
print("the same day again:", notifier.sent)
assert notifier.sent == []
clock.day = date(2025, 3, 15)
reminders.run()
print("on 2025-03-15:", notifier.sent)
assert notifier.sent == [("Ravi", 3)]
print("all three checks passed") Output
on 2025-03-12: [('Asha', 3), ('Kabir', 0)]
the same day again: []
on 2025-03-15: [('Ravi', 3)]
all three checks passed
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 test_reminders.py
after/reminders.py
"""Membership renewal reminders: the policy, and the two abstractions it needs, which it owns."""
from collections.abc import Iterable
from dataclasses import dataclass
from datetime import date
from typing import Protocol
@dataclass(frozen=True)
class Member:
name: str
phone: str
ends_on: date
class Notifier(Protocol):
"""Tells a member that their membership is about to end. Named in the policy's words, not a channel's."""
def remind(self, member: Member, days_left: int) -> None: ...
class Clock(Protocol):
def today(self) -> date: ...
class RenewalReminders:
"""Reminds each member once, when their membership ends within `days_ahead` days (today included)."""
def __init__(self, members: Iterable[Member], notifier: Notifier, clock: Clock, days_ahead: int = 3):
self._members = list(members)
self._notifier = notifier
self._clock = clock
self._days_ahead = days_ahead
self._reminded = set()
def run(self) -> int:
today = self._clock.today()
sent = 0
for member in self._members:
days_left = (member.ends_on - today).days
if 0 <= days_left <= self._days_ahead and member.name not in self._reminded:
self._notifier.remind(member, days_left)
self._reminded.add(member.name)
sent += 1
return sent after/sms_notifier.py
"""The SMS detail. It implements the Notifier that reminders.py defines, so it depends on the policy."""
from reminders import Member
from sms_gateway import SmsGateway
class SmsNotifier:
def __init__(self, gateway: SmsGateway):
self._gateway = gateway
def remind(self, member: Member, days_left: int) -> None:
when = "today" if days_left == 0 else f"in {days_left} days"
self._gateway.send_sms(member.phone, f"Hi {member.name}, your gym membership ends {when}.") after/system_clock.py
"""The real clock, a detail like the SMS gateway."""
from datetime import date
class SystemClock:
def today(self) -> date:
return date.today() after/sms_gateway.py
"""A client for an SMS provider's web API. Building one needs a live account's key."""
import json
import os
class SmsGateway:
URL = "https://sms.example.com/v1/messages"
def __init__(self):
key = os.environ.get("SMS_API_KEY")
if not key:
raise RuntimeError("SMS_API_KEY is not set, and the gateway cannot start without a live account")
self._key = key
def send_sms(self, phone: str, text: str) -> None:
from urllib.request import Request, urlopen
body = json.dumps({"to": phone, "text": text}).encode()
request = Request(self.URL, data=body, headers={"Authorization": f"Bearer {self._key}"})
with urlopen(request, timeout=10):
pass after/main.py
"""The composition root: the one place that builds the real objects and wires them together."""
from datetime import date
from reminders import Member, RenewalReminders
from sms_gateway import SmsGateway
from sms_notifier import SmsNotifier
from system_clock import SystemClock
def load_members() -> list[Member]:
# In the real program, a query against the gym's database.
return [Member("Asha", "+91 00000 11111", date(2025, 3, 15)), Member("Ravi", "+91 00000 22222", date(2025, 3, 18))]
def build_reminders() -> RenewalReminders:
return RenewalReminders(load_members(), notifier=SmsNotifier(SmsGateway()), clock=SystemClock())
if __name__ == "__main__":
print("reminders sent:", build_reminders().run()) 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
The three checks pin down the rules exactly: a membership that ends in three days or today gets a reminder, one that ended yesterday or is six days away does not, and nobody is reminded twice. None of this needs a network, a key or a particular date. This program reads the imports of both versions and prints which of the program’s own modules each one imports:
"""Prints which of the program's own modules import which, before and after the inversion (standard library left out)."""
import ast
from pathlib import Path
def own_imports(folder):
modules = {path.stem: path for path in sorted(Path(folder).glob("*.py")) if "test" not in path.stem}
graph = {}
for name, path in modules.items():
tree = ast.parse(path.read_text(encoding="utf-8"))
found = set()
for node in ast.walk(tree):
if isinstance(node, ast.ImportFrom) and node.module in modules:
found.add(node.module)
elif isinstance(node, ast.Import):
found.update(alias.name for alias in node.names if alias.name in modules)
graph[name] = sorted(found)
return graph
for folder in ("before", "after"):
print(f"{folder}:")
for module, imported in own_imports(folder).items():
print(f" {module} -> {', '.join(imported) or '(none of ours)'}") Output
before: reminders -> sms_gateway sms_gateway -> (none of ours) after: main -> reminders, sms_gateway, sms_notifier, system_clock reminders -> (none of ours) sms_gateway -> (none of ours) sms_notifier -> reminders, sms_gateway system_clock -> (none of ours)
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 imports.py
Before, reminders imports sms_gateway: the rules depend on the detail. After, reminders imports none of the
program’s other modules, and sms_notifier imports reminders. That reversed arrow is the inversion the principle is
named after.
Which module imports which, before and after the inversion
Text description of the diagram
The diagram shows the imports of the reminder program in two versions, one above the other. Each arrow points from a module to a module it imports.
- Before: reminders.py, which holds the RenewalReminders policy, imports sms_gateway.py and builds an SmsGateway itself. The policy depends on the detail.
- After: reminders.py holds RenewalReminders and the two abstractions it needs, Notifier and Clock, and imports none of the program's other modules. sms_notifier.py imports reminders.py, because SmsNotifier implements Notifier, and it imports sms_gateway.py to send the messages. system_clock.py holds the real clock. main.py, the composition root, imports all four modules and wires the real objects together. The arrow between the policy and the SMS detail now points from the detail to the policy.
Who owns the abstraction
The second rule is the one most often missed. An interface that copies the SMS provider’s API, with a phone number, a sender
id and a template id, is an abstraction that depends on a detail. The rules would still change when the provider
changes, and a fake or an email channel would have to pretend to send SMS messages. Notifier.remind(member, days_left) is named in the rules’ own words, so the SMS channel, an email channel and a test fake all fit it, and
each one decides for itself how to word its message.
That is also why the interface sits in reminders.py. The Go project gives the same advice for Go code: declare an
interface in the package that consumes it, and let the package that provides the implementation return a concrete
type. Put the interface with the code that needs it, and shape it by that code’s needs, as the previous lesson did
for the gate devices.
Dependency injection and the composition root
Passing a collaborator in, instead of letting a class create it, is called dependency injection. Martin Fowler named it in 2004, after he and others found “inversion of control” too generic a term for what the new frameworks did. He described three forms: constructor injection, setter injection and interface injection. This lesson uses constructor injection throughout, because an object that receives everything in its constructor is complete as soon as it exists, and its constructor lists everything it depends on.
Something still has to create the real gateway, notifier and clock. Mark Seemann calls that place the composition
root: one place, as close as possible to the program’s entry point, where the objects are composed. Here it is
build_reminders() in main.py, the only code that names SmsGateway and SystemClock. Seemann adds that only
applications have a composition root. A library receives its dependencies and leaves the wiring to the program that
uses it.
Inversion, injection and containers are different things
The three ideas are often blurred together, so it helps to set them side by side:
| Criterion | Dependency inversion | Dependency injection | Injection container |
|---|---|---|---|
| What it is | A design principle about which way source dependencies point | A technique: collaborators are passed in | A library that builds objects from registrations |
| Can exist without the others | Yes: an abstract class whose subclasses supply the details | Yes: passing a concrete SmsGateway in still inverts nothing | Rarely useful alone; it automates injection |
| Needed in a design interview | Yes, at the boundaries that change | Yes, through constructors | No: wire the objects by hand in main |
| When to choose | Rules that must not depend on volatile details | Every collaborator that a test may want to replace | Large applications with many lifetimes and scopes |
Injection without inversion is common: a constructor that takes an SmsGateway lets a test pass a subclass, but the
rules still import the gateway’s module and use its methods. Inversion without injection also exists, and Martin
gave it as an example of the principle: in the template method pattern, a base class holds the rules and calls
abstract methods that its subclasses fill in. Most designs combine the two, and none of them needs a container.
Inject the clock and the id source too
The clock deserves its own section, because tests often try to fake it by patching instead. In Python that does not even work for the standard clock:
"""Why tests cannot simply swap the standard library's clock for a fixed one."""
import datetime
def fixed_day():
return datetime.date(2025, 3, 12)
try:
datetime.date.today = fixed_day
except TypeError as error:
print("patching date.today():", error)
print("date.today() is still the real clock:", datetime.date.today is not fixed_day) Output
patching date.today(): cannot set 'today' attribute of immutable type 'datetime.date' date.today() is still the real clock: True
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 patch_the_clock.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
datetime.date is a built-in type, and Python refuses to set its attributes. Libraries exist that patch time in other
ways, but each test then depends on how the code under test happens to read the clock. A Clock passed in, with
today() or now(), is plain code that any test controls. The same goes for anything else that differs between
runs. Pass an id generator instead of calling uuid4() inside the rules, and pass a random.Random instance, which
the Python documentation describes as a generator that does not share state with the module’s functions, instead of
calling random.random(). A test can then seed it.
The principle is not tied to Python. The same rules in JavaScript receive any object with remind() and today(),
and the same three checks pass:
js/check.mjs
// The same three checks as test_reminders.py, with a fixed clock and a notifier that only records.
import { RenewalReminders } from './reminders.mjs';
const march = (day) => Date.UTC(2025, 2, day); // months count from 0 in JavaScript
const clock = { day: march(12), today() { return this.day; } };
const sent = [];
const notifier = { remind(member, daysLeft) { sent.push(`${member.name} (${daysLeft})`); } };
const reminders = new RenewalReminders({
members: [
{ name: 'Asha', endsOn: march(15) },
{ name: 'Kabir', endsOn: march(12) },
{ name: 'Meera', endsOn: march(11) },
{ name: 'Ravi', endsOn: march(18) },
],
notifier,
clock,
});
const runOn = (label) => {
sent.length = 0;
reminders.run();
console.log(`${label}: ${sent.join(', ') || 'nobody'}`);
return sent.join(', ');
};
const results = [runOn('on 2025-03-12'), runOn('the same day again')];
clock.day = march(15);
results.push(runOn('on 2025-03-15'));
const expected = ['Asha (3), Kabir (0)', '', 'Ravi (3)'];
console.log(results.every((r, i) => r === expected[i]) ? 'all three checks passed' : 'a check failed'); Output
on 2025-03-12: Asha (3), Kabir (0) the same day again: nobody on 2025-03-15: Ravi (3) all three checks passed
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node check.mjs
js/reminders.mjs
// The reminder policy in JavaScript. It receives its notifier and its clock; any object with remind() and today()
// will do, which is how JavaScript expresses the two abstractions.
const DAY_MS = 24 * 60 * 60 * 1000;
export class RenewalReminders {
#members;
#notifier;
#clock;
#daysAhead;
#reminded = new Set();
constructor({ members, notifier, clock, daysAhead = 3 }) {
this.#members = [...members];
this.#notifier = notifier;
this.#clock = clock;
this.#daysAhead = daysAhead;
}
run() {
const today = this.#clock.today();
let sent = 0;
for (const member of this.#members) {
const daysLeft = Math.round((member.endsOn - today) / DAY_MS);
if (daysLeft >= 0 && daysLeft <= this.#daysAhead && !this.#reminded.has(member.name)) {
this.#notifier.remind(member, daysLeft);
this.#reminded.add(member.name);
sent += 1;
}
}
return sent;
}
} Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
When not to invert
Not every dependency needs an abstraction. Code that is stable and has no side effects, such as the standard
library’s data structures, a Money value object or a pure pricing function, can be used directly: an interface in
front of it adds a name and a file and protects against nothing. Invert where the detail is likely to change or is
awkward in a test: the network, files, databases, the clock, randomness and third-party software. The Go guidance
quoted above adds a useful brake: do not define an interface before there is a real use for it.
In a design interview
Draw the rules in the middle and the details around them, with the arrows pointing inwards, and say which interface each detail implements: “the booking service owns a PaymentGateway interface; the card adapter implements it; main wires them”. Then write the tests against fakes. When the interviewer asks for a second payment provider, the answer is one new adapter and one line in main.
Interview questions
Warm-up (fresher to mid level): what is the difference between dependency inversion and dependency injection? Dependency inversion is a design principle about the direction of source-code dependencies: high-level rules and low-level details both depend on an abstraction, and the abstraction is owned by the rules, so the details import the rules’ interface rather than the other way round. Dependency injection is a technique: an object receives its collaborators, usually through its constructor, instead of creating them. Injection is the usual way to apply inversion, because the rules receive an implementation of their own interface, but the two are independent. Passing a concrete gateway class into a constructor is injection that inverts nothing, and a base class whose subclasses supply the details inverts without injecting anything. An injection container is a third thing, a library that automates the wiring; neither idea needs one.
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to senior
Your
InvoiceServicecallsdatetime.now(),uuid4()and a tax-rate web API directly. Its tests fail around midnight and on the last day of each month, and they need the network. How do you make it testable without a dependency injection framework? -
Mid to senior
A teammate puts an interface in front of every class "for DIP":
IUserService,IUserRepository,IDateFormatterandIMoney, each with exactly one implementation. Where is the inversion worth its cost, and where is it noise? Give a rule you would defend in a design discussion.
Key takeaways
- DIP: rules and details both depend on an abstraction, and the abstraction belongs to the rules, named in their terms and kept next to them.
- The calls still go from the rules to the details; inversion reverses the source dependency, so the details import the rules’ interface.
- Inject collaborators through constructors, and build the real objects in one composition root near the entry point.
- Inversion, injection and containers are different: you can have either of the first two without the other, and neither needs a container.
- Inject clocks, id sources and random generators; invert at volatile boundaries, not in front of stable code.
Exercise
Exercise · Medium · Python
Inject the gateway, clock and ids of an order service
A canteen's OrderService builds its own LiveCardGateway, reads the real clock with datetime.now() and makes a random order id with uuid4(). No test can check one of its receipts: the gateway cannot be reached from a test, and the time and the id change on every run.
Change it so that all three are passed in:
OrderService(gateway, clock, new_id):gatewayhascharge(amount, card_token), which returns a payment id;clockhasnow(), which returns adatetime;new_idis a function with no arguments that returns the next order id. Keep the attribute namesgateway,clockandnew_id.place_order()keeps its receipt format,order <id> placed at <YYYY-MM-DD HH:MM> UTC: <total> charged, payment <payment id>, and still refuses an empty order withValueErrorbefore anything is charged.- Add
build_live_service(), the composition root: it returns anOrderServicewired withLiveCardGateway, a clock that returnsdatetime.now(timezone.utc)and the randomORD-ids. Building it must not charge anything.
The sample tests pass a fake gateway that records its charges, a clock that always returns the same moment and a fixed list of ids, and compare the receipts exactly.
Starter code · order_service.py
"""A canteen's order service. It builds its own payment gateway, reads the real clock and makes random ids, so no
test can check a receipt. Inject all three (see the prompt)."""
import uuid
from datetime import datetime, timezone
class LiveCardGateway:
"""Charges a card through the payment provider (not reachable from a test or from this page)."""
def charge(self, amount: int, card_token: str) -> str:
raise ConnectionError("the payment provider cannot be reached from here")
class OrderService:
def __init__(self):
self.gateway = LiveCardGateway()
def place_order(self, items: list[tuple[str, int, int]], card_token: str) -> str:
"""items: (name, quantity, unit price) rows. Returns the receipt line."""
if not items:
raise ValueError("an order needs at least one item")
total = sum(quantity * price for _, quantity, price in items)
payment_id = self.gateway.charge(total, card_token)
order_id = "ORD-" + uuid.uuid4().hex[:6].upper()
placed_at = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
return f"order {order_id} placed at {placed_at}: {total} charged, payment {payment_id}" The sample tests · test_order_service.py
from datetime import datetime, timezone
import order_service
from order_service import LiveCardGateway, OrderService
class FakeGateway:
def __init__(self):
self.charges = []
def charge(self, amount, card_token):
self.charges.append((amount, card_token))
return f"pay_{len(self.charges)}"
class FixedClock:
def now(self):
return datetime(2025, 3, 12, 9, 30, tzinfo=timezone.utc)
def make_service(gateway=None):
ids = iter(["ORD-0001", "ORD-0002"])
return OrderService(gateway=gateway or FakeGateway(), clock=FixedClock(), new_id=lambda: next(ids))
def test_receipt_is_exact():
"""with a fake gateway, a fixed clock and fixed ids, the receipt is known in advance"""
service = make_service()
receipt = service.place_order([("dosa", 2, 60), ("filter coffee", 1, 30)], "tok_asha")
assert receipt == "order ORD-0001 placed at 2025-03-12 09:30 UTC: 150 charged, payment pay_1"
def test_second_order_uses_the_next_id():
"""each order takes the next id from the injected id source"""
service = make_service()
service.place_order([("idli", 3, 20)], "tok_asha")
receipt = service.place_order([("vada", 1, 25)], "tok_ravi")
assert receipt == "order ORD-0002 placed at 2025-03-12 09:30 UTC: 25 charged, payment pay_2"
def test_gateway_is_charged_the_total():
"""the gateway receives the order total and the card token"""
gateway = FakeGateway()
make_service(gateway).place_order([("dosa", 2, 60), ("filter coffee", 1, 30)], "tok_asha")
assert gateway.charges == [(150, "tok_asha")]
def test_empty_order_charges_nothing():
"""an empty order is refused before anything is charged"""
gateway = FakeGateway()
try:
make_service(gateway).place_order([], "tok_asha")
except ValueError:
pass
else:
raise AssertionError("an empty order must raise ValueError")
assert gateway.charges == []
def test_composition_root_wires_the_live_parts():
"""build_live_service() wires the live gateway, a UTC clock and ORD- ids, without charging anything"""
service = order_service.build_live_service()
assert isinstance(service, OrderService)
assert isinstance(service.gateway, LiveCardGateway)
assert service.clock.now().tzinfo == timezone.utc
order_id = service.new_id()
assert order_id.startswith("ORD-") and len(order_id) == 10 A hint
Move each call that reaches outside the service into a constructor parameter: LiveCardGateway() becomes gateway, datetime.now(timezone.utc) becomes clock.now(), and the uuid expression becomes new_id(). The code you take out of place_order() does not disappear: the real clock and the random id move into a small class and a function that build_live_service() passes in.
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
6 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.
References
- The Dependency Inversion Principle (The C++ Report column, archived copy) (Robert C. Martin, via the Internet Archive)
- The Principles of OOD (archived copy) (Robert C. Martin, via the Internet Archive)
- Solid Relevance (Robert C. Martin, The Clean Code Blog)
- Inversion of Control Containers and the Dependency Injection pattern (Martin Fowler)
- Composition Root (Mark Seemann)
- Go Wiki: Go Code Review Comments, interfaces (The Go Authors)
- typing: Protocol (Python Software Foundation)
- random: instances of random.Random (Python Software Foundation)
- Date.UTC() (MDN Web Docs (Mozilla))
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress