Message passing, generalisation, the facade pattern, and top-down design
NESA In OOP, objects don't share memory directly — they communicate by sending messages. A message is just a method call: you name the receiver, the method, and any arguments.
You've been doing this the whole time without the vocabulary for it.
cart.add_item("Sword") # message: add_item, sent to: cart, argument: "Sword"
cart.get_total() # message: get_total, sent to: cart, no arguments
history.push(last_action) # message: push, sent to: history, argument: last_action
Each line sends a message to an object. The object receives it and decides what to do — using its own internal state. You don't need to know how cart stores its items internally; you just send the message and trust it to respond.
Some messages just trigger an action (like cart.clear()). Others send a value back — that's the return message.
total = cart.get_total() # return message: the total price (a float)
prediction = model.predict(X) # return message: an array of predictions
score = model.score(X, y) # return message: a float between 0 and 1
The sender stores the reply in a variable and carries on. The receiver's internal state is its own business — you only see what it chooses to send back.
How many messages are being sent in this code? Name the receiver and the message for each one.
model = LinearRegression()
model.fit(X_train, y_train)
predictions = model.predict(X_test)
score = model.score(X_test, y_test)
print(model.coef_)
Four messages (line 1 is instantiation, not a message):
model, message = fit, args = X_train, y_train, no return value usedmodel, message = predict, args = X_test, return = array stored in predictionsmodel, message = score, args = X_test, y_test, return = float stored in scoreprint (a built-in), message = print, args = model.coef_ — but accessing model.coef_ is reading an attribute, not sending a messageThe key insight: model.coef_ is a direct attribute access — not a method call, not a message. The difference matters: attributes are data; messages trigger behaviour.
fetch('/api/characters'), that's message passing at the network level — your page object sends a message to the server object and waits for a return message (the JSON response).
NESA Generalisation is about recognising that different specific things share common behaviour — and designing your code around that shared interface rather than the specific details.
You already know inheritance: a Dog and a Cat are both Animal. That's a generalisation — "Animal" captures what they have in common.
Generalisation goes further: even without a shared parent class, objects can be treated the same if they respond to the same messages.
Three completely different algorithms — but you use them identically because they all respond to the same messages:
| Method | LinearRegression | LogisticRegression | MLPClassifier |
|---|---|---|---|
fit(X, y) |
✓ | ✓ | ✓ |
predict(X) |
✓ | ✓ | ✓ |
score(X, y) |
✓ | ✓ | ✓ |
Because they share this interface, you can write a single function that works with any of them — without knowing which one it is:
def evaluate_model(model, X_train, X_test, y_train, y_test):
model.fit(X_train, y_train)
return model.score(X_test, y_test)
# Works with any model — the function doesn't care which one
r2 = evaluate_model(LinearRegression(), X_train, X_test, y_train, y_test)
acc = evaluate_model(LogisticRegression(), X_train, X_test, y_train, y_test)
acc = evaluate_model(MLPClassifier(), X_train, X_test, y_train, y_test)
You have three classes: Dog, Cat, and Bird. Each has a speak() method. Someone writes this function:
def make_sound(animal):
animal.speak()
Why does make_sound() work with all three classes — even though there's no shared parent class?
Because all three classes share the same interface — they all have a speak() method. The function doesn't care what type the object is, only that it responds to speak(). That's generalisation: designing around the shared behaviour rather than the specific class.
This is exactly how evaluate_model() works above — it doesn't care which model you pass in, as long as it has fit() and score().
NESA A facade is a class that wraps one or more complex subsystems behind a simpler interface. The caller doesn't need to know what's happening inside — they just talk to the facade.
The name comes from architecture: a building's facade is its front face. You see a clean, simple exterior. Behind it is all the pipes, wiring, and structure — complex, but hidden.
fit_transform, transform
fit, predict, score
Without the facade, every time you use the model you have to remember to scale the data, use fit_transform on training data but only transform on test data, and keep the scaler object around. Easy to forget; easy to get wrong.
With a facade, all of that is handled internally — once, correctly. Callers just call train() and predict().
from sklearn.preprocessing import StandardScaler
class MLPipeline:
"""Facade: hides the scaler + model complexity."""
def __init__(self, model):
self._model = model # private — callers don't touch these directly
self._scaler = StandardScaler()
def train(self, X, y):
X_scaled = self._scaler.fit_transform(X) # fit AND transform on training data
self._model.fit(X_scaled, y)
def predict(self, X):
X_scaled = self._scaler.transform(X) # transform only — scaler already fitted
return self._model.predict(X_scaled)
def evaluate(self, X, y):
X_scaled = self._scaler.transform(X)
return self._model.score(X_scaled, y)
# The caller's view — clean, no scaler knowledge needed
pipeline = MLPipeline(LogisticRegression())
pipeline.train(X_train, y_train)
print(pipeline.evaluate(X_test, y_test))
You could put this logic in a standalone function instead — so why use a class? Because the MLPipeline object holds state: the fitted scaler and the trained model are stored as attributes. A function can't hold state between calls. The facade pattern specifically applies when you need to bundle stateful subsystems behind a simple interface.
For each example, identify what the facade is hiding:
Pressing "Volume Up" sends a simple message. The remote (facade) hides the infrared signal encoding, the TV's audio processing hardware, the digital-to-analogue conversion chain. You don't need to know any of that — you just press the button.
open("file.txt")One line. Behind it: the OS file system API, disk block allocation, file descriptor management, buffering. The open() function is a facade over the operating system's I/O subsystem. You send one message; the OS handles the rest.
git push → your site updates. The Vercel platform is a facade over: CDN distribution, DNS propagation, build pipelines, SSL certificate renewal, server provisioning. You send one message (the push); Vercel handles every subsystem.
NESA These are two strategies for deciding where to start when designing a system.
Start with the big picture. Define what the whole system does, then decompose it into smaller parts. Each part gets broken down further until you reach pieces small enough to implement.
Start with the small pieces. Build and test individual components first, then combine them into larger subsystems, then combine those into the final system.
"Train a model and predict"
split, scale
fit, tune
score, predict
In practice, most experienced developers do both: top-down to understand the structure and decide what to build, bottom-up to actually build and test it. You sketch the big picture first so you know what the small pieces need to do — then you implement from the bottom up because you can test each piece before combining them.
You're building a Volcanic Pantheon feature: a search bar that finds characters by name and shows their lore. Which design approach would you start with, and why?
Start top-down: Define the full feature first — "user types a name, gets a character card." Then decompose: you need (1) an input handler, (2) a search function, (3) a character card renderer. Now you know exactly what pieces to build.
Then build bottom-up: Implement and test the search function first (pure logic, easy to unit-test). Then build the renderer. Then wire up the input handler. Each piece is independently testable before you combine them.
Neither approach alone is ideal. Top-down without bottom-up gives you a great plan but untested components. Bottom-up without top-down gives you working parts that may not fit together.
These four concepts aren't abstract theory — here's exactly where you'll see each one in the next lesson:
| Concept | Where it appears in Lesson 19 |
|---|---|
| Message passing | Every model.fit(), model.predict(), model.score() call is a message sent to an ML model object |
| Generalisation | The evaluate_model() function works with LinearRegression, LogisticRegression, and MLPClassifier because they all share the same interface |
| Facade pattern | The MLPipeline class wraps the scaler + model complexity behind train() / predict() / evaluate() |
| Top-down design | The ML pipeline is designed top-down: Pipeline → Model + Scaler → individual methods |
Message passing: what a message is, how it's sent, and what a return message is
Generalisation: why objects with the same interface can be used interchangeably
Facade pattern: what it hides, why it's useful, and how it differs from a plain function
Top-down vs bottom-up: what each means and why you'd use both together