SOLID in Practice — Lessons From a C++ Emulator's Git History
The Problem With SOLID as Theory
Every developer has read about SOLID. Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion. The definitions are clean. The toy examples are intuitive. And then you go back to your real codebase where everything is already tangled and the principles feel like they belong to a different, tidier world.
The 8-Bit Machine’s git history has three consecutive commits that tell a different story. Not theory — actual surgery on a working codebase, each commit named after the principle it applied. Here’s what each one actually meant.
SRP: Split Application.cpp Into Focused Implementation Files
Application.cpp had become the catch-all. It handled input, managed the render loop, wired up the GUI, loaded presets, dispatched keyboard events, and held references to basically everything. It knew too much.
The Single Responsibility Principle says a class should have one reason to change. Application.cpp had a dozen. The fix wasn’t rewriting anything — it was extraction. Input handling moved to its own file. Preset loading moved to its own file. GUI layout to another. Each piece still existed; it just stopped being one file’s problem.
The result is that adding a new input device doesn’t mean touching the file that owns the render loop. Changing how presets are discovered doesn’t mean touching the file that handles keyboard events. The reasons to change a file are now scoped to what that file actually does.
The warning sign that SRP has been violated isn’t always obvious in code review. It shows up over time in merge conflicts — when two features that feel unrelated keep touching the same file. That’s the file telling you it has too many responsibilities.
OCP: Replace Preset Dispatch Chains With a PresetDriver Table
Before this commit, adding a new preset — say, a new machine architecture — meant finding the right place in a chain of if/else or switch branches and inserting a new case. The function that loaded presets needed to be opened and modified every time.
The Open/Closed Principle says software should be open for extension, closed for modification. The preset dispatch chain was the opposite: closed for extension, open for modification. Every new machine was a source edit in the same central function.
The fix was a PresetDriver table — a data structure mapping preset types to builder functions. Adding a new preset now means adding an entry to the table. The dispatch logic itself doesn’t change. The function that loads presets doesn’t know or care how many presets exist.
This pattern appears in a lot of forms — plugin registries, handler maps, strategy tables. The shape is always the same: instead of a function that knows about every case, you have a registry that knows where to look, and individual drivers that know how to handle their own case. The central logic stays stable; new behavior is added at the edges.
ISP: Extract IHasPanel From IBusDevice
This one is subtler. IBusDevice is the interface that every hardware device on the emulated bus implements. It defines how devices respond to reads and writes, how they get clocked, how they reset.
At some point, panel-rendering responsibility was added to the same interface. Every bus device was expected to implement drawPanel() — even devices that didn’t have a panel, even devices where the panel concept made no sense. The interface had grown beyond what any single implementer actually needed.
The Interface Segregation Principle says clients shouldn’t be forced to depend on interfaces they don’t use. IHasPanel was extracted as a separate interface. Devices that have panels implement it. Devices that don’t, don’t. IBusDevice went back to being about bus behavior.
The practical effect: adding a new bus device no longer means stubbing out a panel function that does nothing. The type system now accurately reflects which devices have panels and which don’t. Code that iterates over paneled devices works with IHasPanel directly and doesn’t need to know about bus behavior at all.
The Pattern Across All Three
Looking at these three commits together, the common thread is that each violation was invisible at the time the code was written. Application.cpp grew one feature at a time. The preset dispatch chain started with two presets. IBusDevice got panel methods incrementally. None of it felt wrong in the moment.
SOLID violations tend to announce themselves retroactively — when a change that should be simple requires touching more files than expected, when adding a feature means modifying existing logic, when an interface has methods that some implementers leave empty. The code tells you something is wrong; you have to know what to listen for.
The refactoring commits didn’t happen at a scheduled “cleanup sprint.” They happened right before a significant feature push — adding new presets, new CPU architectures, new panel types. The team recognized that the next wave of work would be painful under the current structure, and fixed the structure first. That sequencing — refactor before expand, not after — is its own lesson.