Lesson 18B: OOP Deep Dive

Message passing, generalisation, the facade pattern, and top-down design

📅 April 2026 ⏱ ~50 min 🧱 OOP Paradigm — Year 11 🔗 Bridge to Lesson 19
← Back to Learning Hub
🔧

Four OOP gaps — patched before ML

Your Firebase progress shows four OOP concepts not yet marked off: message passing, generalisation, the facade pattern, and top-down design. Lesson 19 uses all four of them constantly — so we're covering them now. This is a short sharp lesson, not a full OOP review.

1. Message Passing 12 min

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.

Sender

Your code

model.fit(X, y)
← (trains internally)
Receiver

model object

Messages can return a reply

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.

Spot the messages

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_)
Reveal answer

Four messages (line 1 is instantiation, not a message):

  • Line 2: receiver = model, message = fit, args = X_train, y_train, no return value used
  • Line 3: receiver = model, message = predict, args = X_test, return = array stored in predictions
  • Line 4: receiver = model, message = score, args = X_test, y_test, return = float stored in score
  • Line 5: receiver = print (a built-in), message = print, args = model.coef_ — but accessing model.coef_ is reading an attribute, not sending a message

The 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.

Volcanic Pantheon: Every time your JavaScript sends 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).

2. Generalisation 12 min

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.

Every scikit-learn model is a generalisation in action

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)

Quick check

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?

Reveal answer

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().

3. The Facade Pattern 14 min

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.

Facade

MLPipeline

train(X, y) predict(X) evaluate(X, y)
↓ hides ↓
Subsystem 1

StandardScaler

fit_transform, transform

Subsystem 2

ML Model

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))

Facade vs just a function

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.

Real-world facades — name the pattern

For each example, identify what the facade is hiding:

Your TV remote

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.

Python's 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.

Volcanic Pantheon's Vercel deployment

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.

4. Top-Down & Bottom-Up Design 8 min

NESA These are two strategies for deciding where to start when designing a system.

Top-down

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.

Bottom-up

Start with the small pieces. Build and test individual components first, then combine them into larger subsystems, then combine those into the final system.

ML Pipeline

"Train a model and predict"

↓ decompose ↓

Data Preparation

split, scale

Model Training

fit, tune

Evaluation

score, predict

↓ decompose ↓

train_test_split

StandardScaler

LinearRegression

model.score()

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.

Which approach?

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?

Reveal answer

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.

5. How These Appear in Lesson 19 4 min

These four concepts aren't abstract theory — here's exactly where you'll see each one in the next lesson:

ConceptWhere 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

What you can explain right now

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


← Back to Learning Hub