Week 23 · Phase 3 — The Architect
Programming against an idea, not against a thing.
Photo · Anas Ahmed / Unsplash
The other four pillars of OO — classes, encapsulation, inheritance, polymorphism — are mechanical. Abstraction is the one that sits in the head: what is the simplest, most useful description of the thing my code talks to?
Concretely, it means: program against an interface, not an implementation. Once you've done that, you can swap out the implementation later — for a faster one, a simpler one, a fake one for tests — and nothing else changes.
An abstract base class in C++ — a class with at least one pure virtual method (= 0) — describes what something can do, without saying how. Any class that inherits and implements those methods becomes a usable concrete version.
class Logger {
public:
virtual void log(const std::string& msg) = 0;
virtual ~Logger() = default;
};
class ConsoleLogger : public Logger {
public:
void log(const std::string& msg) override {
std::cout << "[LOG] " << msg << "\n";
}
};
class FileLogger : public Logger {
public:
void log(const std::string& msg) override {
/* write to file */
}
};
Logger is the abstraction. ConsoleLogger and FileLogger are implementations. Code that uses logging takes a Logger& or Logger* — not a specific subtype:
void authenticate(User& u, Logger& log) {
log.log("authenticating " + u.name());
// ...
}
authenticate doesn't know — and doesn't want to know — whether logs go to the console, a file, or a remote server. It just wants something that can do log(msg). Swap in a NullLogger for tests; a JsonLogger for structured logging; a RemoteLogger for production. authenticate never changes.
Robert Martin coined this in the early 2000s: "depend on abstractions, not on concretions." Translated: when class A needs to talk to class B, A shouldn't reach down and grab a specific kind of B — A should be handed something that satisfies the abstract interface and use it.
This is also the heart of dependency injection: you pass in the things a class depends on (typically as constructor arguments), rather than letting the class create them itself. The result is code that's easier to test, easier to reconfigure, and easier to refactor.
// bad — App is welded to ConsoleLogger
class App {
ConsoleLogger log_;
};
// good — App talks to ANY Logger
class App {
public:
App(Logger& log): log_(log) {}
private:
Logger& log_;
};
The "good" version is wildly more flexible at very little cost. This pattern — pass in the dependency through the interface — is the structural skeleton of basically every framework you'll ever use, including AI ones.
std::ifstream f("foo.txt"). Underneath, a chain of abstractions hands the request to NTFS, ext4, APFS, ZFS, or a network share. None of them care which.Tensor works the same on CPU, NVIDIA GPU, AMD GPU, and Apple Silicon. The chip-specific code lives behind the abstraction.Every successful piece of software has, at its core, a few well-chosen abstractions and a lot of replaceable implementations. The abstractions are the load-bearing decisions. Get them right and the system can evolve for decades. Get them wrong and the rewrites never end.
Abstractions are leaky. The more you abstract, the more behavior the abstraction has to promise to capture — and the harder it gets to keep the promise. There's a wonderful Joel Spolsky quote: "all non-trivial abstractions, to some degree, are leaky." Pretending the network is reliable, that disks never fail, that floats are real numbers — these are convenient lies that occasionally bite. Good engineers know which abstractions leak and where.
Premature abstraction is also a real cost. Inventing five interfaces for hypothetical future needs slows you down today and may turn out to be wrong tomorrow. The pragmatic rule: start concrete; extract an abstraction the third time you need it. Two implementations isn't enough evidence; three usually is.
Open the PyTorch source. Find at::Tensor. Notice it's a thin handle that points at a TensorImpl via std::shared_ptr. The TensorImpl owns a Storage. The Storage owns a DataPtr. The DataPtr uses an abstract Allocator. Each layer is an abstraction; each can be swapped (CPU allocator, CUDA allocator, MPS allocator, sparse-tensor variant) without disturbing the levels above.
That's also why "PyTorch on Apple Silicon" was a relatively contained engineering project — most of the framework didn't have to know that a new GPU backend existed. The abstractions held.
The right interface is the most valuable thing in a long-lived codebase.
Storage abstract class with save(string key, string value) and load(string key). Implement two concrete versions: InMemoryStorage (uses a std::unordered_map) and FileStorage (writes to disk). Pass either one into a function that uses it.You now have classes, encapsulation, inheritance, polymorphism, and abstraction — the entire OO toolbox. Time to look at object lifetimes: how they come into existence, how they go out of existence, and why C++ has the most quietly elegant answer to that question of any mainstream language.
Week 24 is Constructors & Destructors.
Photo free under the Unsplash license. Silhouette · Anas Ahmed.