Real World Analogies and Code Example
The Open/Closed Principle in one sentence
The Open/Closed Principle (OCP) says software entities — classes, modules, functions — should be open for extension but closed for modification. You should be able to add new behavior by writing new code, not by editing code that already works and is already tested. Coined by Bertrand Meyer in 1988 and later reframed by Robert C. Martin around polymorphism, it is the second of the five SOLID principles.
The intuition is risk management. Every time you reopen a class that is in production, you risk breaking the behavior other callers already depend on. OCP pushes you toward a shape where the stable, tested core never has to change and all variation lives in new, isolated pieces.
A real-world analogy: the power socket
Think of the wall socket in your home. The socket is a fixed contract: two or three holes in a known shape at a known voltage. You never rewire the wall to plug in a new device. Instead, every appliance — a lamp, a charger, a vacuum — is built to that contract and simply plugs in. The wall is closed for modification; the set of things you can plug in is open for extension.
A second analogy: a board game's rulebook versus its pieces. The rules engine knows "on your turn, each piece moves according to its own movement rule." Adding a new piece type means writing that piece's movement rule — it does not mean rewriting the turn loop. The engine depends on the abstraction "a piece that can move," not on a hard-coded list of piece types.
Both analogies share the same skeleton that maps directly to code: a stable abstraction (the socket, the "movable piece") plus interchangeable implementations (appliances, piece types) that the core calls without knowing their concrete identity.
Step 1 — the code that violates OCP
We start with a single Invoice class that owns every kind of invoice. Generating a basic invoice and an international one are switched on a type string. The smell is the growing if/else chain: every new invoice type forces you to reopen and edit a class that already works.
public class Invoice {
private double amount;
public Invoice(double amount) {
this.amount = amount;
}
// Every new type means editing this method — violates OCP
public void generateInvoice(String type) {
if (type.equals("basic")) {
System.out.println("Generating basic invoice for amount: " + amount);
} else if (type.equals("international")) {
System.out.println("Generating international invoice for amount: " + amount);
}
// ...and another branch every time a new type appears
}
}Each added branch raises the chance of breaking an existing branch, bloats one method, and couples unrelated invoice logic together in a single file.
Step 2 — refactor to an abstraction (Java)
We extract the varying behavior behind an interface. Invoice now depends on the abstraction InvoiceGenerator and delegates to it. Each invoice type becomes its own small class. Adding a new type means writing a new class — Invoice is never reopened.
// The stable contract — the "wall socket"
public interface InvoiceGenerator {
void generateInvoice(double amount);
}
// Interchangeable implementations — the "appliances"
public class BasicInvoice implements InvoiceGenerator {
@Override public void generateInvoice(double amount) {
System.out.println("Generating basic invoice for amount: " + amount);
}
}
public class InternationalInvoice implements InvoiceGenerator {
@Override public void generateInvoice(double amount) {
System.out.println("Generating international invoice for amount: " + amount);
}
}
// The closed core — depends only on the abstraction
public class Invoice {
private final double amount;
private final InvoiceGenerator generator;
public Invoice(double amount, InvoiceGenerator generator) {
this.amount = amount;
this.generator = generator;
}
public void generateInvoice() {
generator.generateInvoice(amount); // no if/else, ever
}
}Adding a DetailedInvoice later is purely additive:
public class DetailedInvoice implements InvoiceGenerator {
@Override public void generateInvoice(double amount) {
System.out.println("Generating detailed invoice with breakdown for amount: " + amount);
}
}The same refactor in Go
Go has no inheritance, so OCP is expressed purely through interfaces and composition — which makes the principle especially clear. Invoice holds an InvoiceGenerator and calls it; any type that implements the method satisfies the interface implicitly (structural typing), so new generators need no registration and no edits to Invoice.
package billing
import "fmt"
// The stable contract
type InvoiceGenerator interface {
GenerateInvoice(amount float64)
}
// Interchangeable implementations
type BasicInvoice struct{}
func (BasicInvoice) GenerateInvoice(amount float64) {
fmt.Printf("Generating basic invoice for amount: %.2f\n", amount)
}
type InternationalInvoice struct{}
func (InternationalInvoice) GenerateInvoice(amount float64) {
fmt.Printf("Generating international invoice for amount: %.2f\n", amount)
}
// The closed core — depends only on the interface
type Invoice struct {
Amount float64
Generator InvoiceGenerator
}
func (inv Invoice) Generate() {
inv.Generator.GenerateInvoice(inv.Amount) // no type switch
}
// Adding a new type is purely additive
type DetailedInvoice struct{}
func (DetailedInvoice) GenerateInvoice(amount float64) {
fmt.Printf("Generating detailed invoice with breakdown for amount: %.2f\n", amount)
}Notice the anti-pattern OCP steers you away from in Go: a single function with a switch invoice.Type { case "basic": ...; case "international": ... }. That switch is the Go equivalent of the Java if/else chain, and it must be reopened for every new type.
Trade-offs and when to apply OCP
OCP is not free, and applying it everywhere is its own anti-pattern (often called speculative generality). Weigh these costs and the named alternatives before reaching for an interface.
Costs
- Indirection. Every abstraction adds a layer a reader must trace through. One interface with three implementations is three files where there was one.
- Premature abstraction. If you guess the wrong axis of variation, the interface is worse than the
if/elseit replaced — it locks in the wrong seam and still has to be reopened. - Discoverability. With polymorphism the full set of behaviors is no longer visible in one place; you trade a readable switch for a scattered class hierarchy.
vs Template Method
Strategy-style composition (what we used above: inject an InvoiceGenerator) is one way to achieve OCP, but it is not the only one. The Template Method pattern achieves OCP through inheritance instead: a base class defines the fixed skeleton of an algorithm in one method and leaves the varying steps as abstract hooks that subclasses override. Prefer Template Method when the overall sequence of steps is fixed and only a few well-defined steps vary — for example, an invoice always validates, then renders a body, then appends a footer, and only the body differs by type; the base class guarantees no subtype can reorder or skip the steps. Prefer Strategy/composition (the interface approach shown here) when the whole behavior varies, when one object must swap behavior at runtime, or in a language like Go that has no implementation inheritance. Strategy keeps types decoupled and independently testable; Template Method gives you tighter control of the algorithm's shape at the cost of an inheritance bond between base and subclasses.
vs the humble if/else
When there are exactly two cases that will realistically never grow — and especially when they live in code you fully own and can retest cheaply — a plain conditional is clearer and OCP-by-interface is over-engineering. Reach for OCP when the axis of change is real and recurring: you are adding the third variant, or the variants come from outside your module (plugins, payment providers, file formats).
Interview traps (staff probes)
- Why does
if (g instanceof DetailedInvoice)break OCP? Because the closed core re-opens for every new subtype: each new generator forces a new branch (or a new case in a switch) in code that was supposed to be done. Polymorphism was the point of the interface;instanceofis the oldif/elsechain wearing a type badge. The correct extension is a new class that implementsInvoiceGenerator— no caller should inspect the concrete type. - When is a sealed-type switch better than an open interface? When the set of variants is closed by design and you want exhaustiveness checking (e.g. a domain algebra of three known invoice kinds that the compiler forces you to handle). Sealed + switch trades open extension for compile-time completeness. Prefer the open interface when third parties or future teams will add types without editing the core; prefer sealed when missing a case is a bug you want the compiler to catch today.
- Where does the remaining factory switch live?
Construction still has to pick a concrete type somewhere — a composition root, a Simple Factory, DI config, or a GoF Factory Method subclass. That "switch" is acceptable at the edge (wiring), not inside the closed business method. The OCP win is that
Invoice.generateInvoice()never reopens; the composition root may still map "detailed" →new DetailedInvoice(). Do not pretend factories disappear; confine them to the boundary.
Self-check: (a) Refactor a codebase that does instanceof on strategy types — what changes? (b) Name one domain where sealed+switch beats open polymorphism. (c) Sketch where your app's composition root creates invoice generators.
Wrap-up and source
The Open/Closed Principle turns "edit the thing that works" into "add a thing that plugs in." Identify the axis along which your code keeps changing, pull that variation behind a stable abstraction, and let the tested core depend only on the abstraction. Adding a new invoice type — basic, international, detailed, or one you have not imagined yet — then becomes a new class that plugs into the same socket, with the core untouched.
Apply it where change is real and recurring; skip it where a two-branch conditional will do. Choose Strategy/composition when whole behaviors vary or you need runtime swapping, and Template Method when a fixed algorithm has a few varying steps. Keep construction-time selection at the edge; never re-open the closed core with instanceof.
Source: Adapted and expanded from the Knowledge Guide lesson "Real World Analogies and Code Example" (OO & Low-Level Design → SOLID Principles), with the original Invoice/InvoiceGenerator example. Principle origin: Bertrand Meyer, Object-Oriented Software Construction (1988); polymorphic reformulation: Robert C. Martin, Agile Software Development: Principles, Patterns, and Practices (2002). Template Method and Strategy patterns: Gamma, Helm, Johnson & Vlissides, Design Patterns (1994).
🤖 Don't fully get this? Learn it with Claude
Stuck on Real World Analogies and Code Example? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.
Build the mental picture, not memorization.
I just read a lesson on **Real World Analogies and Code Example** (OO & Low-Level Design) and want to truly understand it. Explain Real World Analogies and Code Example from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Socratic — adapts to where you're stuck.
Teach me **Real World Analogies and Code Example** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Active recall exposes what you missed.
Quiz me on **Real World Analogies and Code Example** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Intuition + hook + flashcards for long-term memory.
Help me remember **Real World Analogies and Code Example** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.