Design principles
Universal ideas that most of the patterns in this catalog are just careful applications of.
SOLID gets the acronym and the interview questions, but three older, blunter principles do most of the actual work. Every creational pattern is "encapsulate what varies" with a specific answer for what varies. Almost every structural and behavioral pattern is "program to an interface" plus a shape. Learn these and the pattern catalog stops looking like twenty-three unrelated tricks.
DRY, YAGNI, KISS
Three older rules, blunter still, that fit on an index card and prevent most of the damage the fancier principles exist to clean up.
DRY - Don't Repeat Yourself
Every piece of knowledge in a system should have one authoritative place to live. Not one
copy of the code - one copy of the fact. A tax rate hardcoded as 0.07 in twelve
files is not twelve pieces of logic; it is one fact that forgot to pick a home, and the
day it changes to 0.075 you get to find all twelve by hand, or you get to find eleven.
The trap is applying DRY to code that merely looks alike instead of code that means
the same thing. Two validation functions with identical five lines today, checking a zip
code and checking a product SKU, are coincidentally identical - not duplicated. Merge them
into one validateFormat() and the day the SKU rule needs a checksum digit, you are
untangling a shared function that never should have been shared. Repetition is cheap.
The wrong abstraction is not.
YAGNI - You Aren't Gonna Need It
Build the thing the current requirement asks for, not the thing you can imagine a future
requirement asking for. A PaymentProcessor interface makes sense the day you add a
second payment provider - not the day you write the first one, on the theory that surely
there will be a second one eventually. Until that day, the interface is a file you
maintain for a caller that does not exist.
The cost is not just the extra code. It is the extra code getting in the way of the design you would have picked if you had waited for the second real requirement to tell you its actual shape, which is almost never the shape you guessed.
KISS - Keep It Simple, Stupid
Given two designs that solve the same problem, the simpler one wins, where "simpler" means fewer moving parts a reader has to hold in their head at once - not fewer characters. A fifteen-line function with an early return reads simpler than a clever six-line one-liner that needs a comment to explain it. Complexity that the problem does not require is a cost with no offsetting benefit, paid every time someone has to change the code.
None of these three is a design pattern, and none of them shows up as an interview buzzword the way SOLID does. They are the reason most of the patterns below are worth reaching for only sometimes: a pattern is a deliberate, named complexity, and KISS is the voice asking whether you actually need it yet.
What good design buys you
Two things, and they are both about the future rather than today.
Code reuse
Cost and time are the two metrics that decide whether a product ships. Reuse is the obvious lever: stop writing the same thing over and over. The obviousness is a trap, though - making existing code work in a new context usually costs more than expected, and the reason is always the same. Tight coupling, dependencies on concrete classes rather than interfaces, hardcoded operations. The logic was reusable; its wiring was not.
Erich Gamma, one of the four authors of the original pattern catalog, describes reuse as three levels, and patterns sit deliberately in the middle:
Reuse and risk climb together. Patterns are the affordable middle.
A framework distills your design decisions for you - it identifies the key abstractions, names them, and calls your code when it is ready ("don't call us, we'll call you"). That is enormous leverage and an enormous bet. Patterns give you the design idea without the commitment to somebody else's concrete code.
Extensibility
Change is the only constant in this job. You shipped for Windows and now they want macOS. You built a GUI framework with square buttons and round buttons became fashionable. You designed a clean e-commerce architecture and a month later somebody wants to take orders over the phone.
Three reasons this always happens:
- You understand the problem better once you have solved it. By the time version one is done you are ready to rewrite it, because now you know what it should have been.
- The ground moves. Something outside your control changes - a browser drops Flash, a vendor deprecates an API - and your plans go with it.
- The goalposts move. The client loved version one so much that they now see eleven more things it could do. These are not frivolous requests; your good first version caused them.
The bright side: if someone asks you to change your app, someone still cares about your app. That is why experienced developers design for change they cannot yet name.
Encapsulate what varies
Identify the aspects of your application that vary and separate them from what stays the same.
The goal is damage control. Picture the program as a ship and changes as mines floating under the surface. A single-compartment hull sinks on the first hit. Divide the hull into sealed compartments and a hit costs you one compartment, not the voyage.
At the method level
Somewhere in an e-commerce codebase, getOrderTotal sums the line items and applies tax.
The summing part is arithmetic and will outlive us all. The tax part depends on country,
state, sometimes city, and on legislatures. Two lifetimes, one method.
At the class level
Methods accumulate. The tax rule that was one if becomes a rate table, then per-product
categories, then exemptions - each with its own helper fields and methods, all of them
blurring what the Order class is supposed to be about. When the volatile part has grown
its own gravity, give it a class.
Order class.Objects of the Order class delegate all tax-related work to an object that does just that.
Separation of Concerns
Split a program along its concerns, and let no concern know how another one does its job.
"Encapsulate what varies" tells you to wall off the part of a class that changes from the part that doesn't. Separation of Concerns is the older, blunter ancestor of that idea: split any program, not just a single class, into pieces where each piece answers exactly one question, and none of them has to understand another's job to do its own. The Single Responsibility Principle, later in this catalog under SOLID, is Separation of Concerns applied specifically to one class at a time.
Picture an OrderImportJob that reads a CSV off disk, parses each row into an Order,
checks that the totals add up, and writes the survivors to the database, all inside one
run() method. It works, right up until the file format changes, or validation needs a new
rule, or storage swaps from a database to a queue. Every one of those changes touches the
same method, because the method never separated "where the bytes come from" from "what a
valid row looks like" from "where a valid row goes."
Split each question into its own object and each one gets a name that describes exactly what it does and nothing else.
None of the four new classes got smaller in total - the line count barely moved. What changed is that a bug in date parsing now points you at one file with one job, instead of a grep through a method that also happens to open sockets and write SQL.
Coupling and Cohesion
Every module should mind its own business tightly, and mind everyone else's loosely.
Two measurements, and the whole catalog spends its life trying to improve both at once. Cohesion asks how well a module's own responsibilities belong together - a class whose methods all touch the same fields for the same reason is highly cohesive. Coupling asks how much one module has to know about another to work - two classes that share only a narrow interface are loosely coupled; two classes that reach into each other's fields are tightly coupled. The target has a name: high cohesion, low coupling.
You already built an example of both moves earlier on this page. TaxCalculator, above, is
highly cohesive - every method on it is about tax, and nothing else - and Order is now
loosely coupled to it, holding only a reference and calling one method. Before the split,
Order was low cohesion (arithmetic and tax law sharing one class) and the tax logic was
tightly coupled to whatever fields Order happened to have lying around. Same behavior,
opposite score on both measurements, which is exactly why that split, and the OrderImportJob
one just above, were worth making.
The two pull in the same direction more often than not: shrinking a class down to one cohesive job is usually what gives you a narrow enough interface to loosen its coupling to everyone else. They stay two separate dials, though - a class can be perfectly cohesive and still tightly coupled if it hands out its internal state to anyone who asks, which is exactly the failure Law of Demeter, further down, is named for.
Program to an interface, not an implementation
Depend on abstractions, not on concrete classes.
A Cat that eats any Food is more flexible than a Cat that eats Sausage, and it costs
you nothing at the call site - a sausage is still food. That is the whole principle: name the
category, depend on the category.
The mechanical version, when you want two classes to collaborate:
- Work out what one object actually needs from the other. Which methods does it call?
- Describe those methods in a new interface or abstract class.
- Make the dependency implement that interface.
- Point the first class at the interface instead of the concrete class.
Take a software company simulator. Company.createSoftware() instantiates a Designer, a
Programmer and a Tester and calls each one's specific method. It works, and it is welded
shut - a new employee type means editing the company.
Generalize the three methods into one - doWork() - and the company stops caring who turns
up. Push the creation of employees into an abstract getEmployees() that concrete
companies implement, and it stops caring which employees exist at all.
You have just watched the factory method pattern assemble itself out of one principle. Most of the catalog works like this.
The honest cost: the code got more complicated and, on the day you write it, no more capable. Spend the indirection where you expect variation, not everywhere.
Law of Demeter
Talk to your immediate friends. Don't talk to strangers you only met through a friend.
A method may call operations on itself, on its own fields, on anything handed to it as a parameter, and on anything it creates on the spot. It may not call an operation on whatever some other call happened to hand back - that return value is a stranger, and reaching past it into a third object's business is the "train wreck" this principle is named for.
The fix is never "add more getters so the caller can keep reaching." It's the opposite: push the behavior down to whichever object already holds the data, and let the caller ask for the result instead of the parts. This is sometimes called "tell, don't ask" - tell the wallet to pay, don't ask it for its cash so you can subtract from it yourself.
The train wreck is also a coupling problem wearing a different name: a chain of four dots is four separate objects that a change anywhere can break, which is exactly the low-coupling target from two sections back, failed in one specific and very grep-able way.
Favor composition over inheritance
Inheritance is the obvious way to reuse code, which is exactly why it gets overused. The caveats only show up once you have hundreds of classes and no appetite for moving them:
- A subclass cannot shrink its parent's interface. Unused abstract methods still need bodies.
- Overrides must stay compatible with the base behavior, because client code holding a parent reference has no idea it got a child.
- Inheritance leaks the superclass's internals into the subclass, which is encapsulation quietly going out the back door.
- Subclasses are welded to superclasses. A change upstairs breaks things downstairs.
- Worst of all, inheritance extends in exactly one dimension. Model two or three independent dimensions with it and the class count multiplies instead of adding.
Composition is the alternative. Inheritance is an "is a" relationship - a car is a transport. Composition is "has a" - a car has an engine. Instead of baking a behavior into the type, hold an object that provides it, and swap that object whenever you like, including at runtime.
Aggregation counts too, the looser variant where the container holds a reference without owning the lifecycle: a car has a driver, but the driver can walk away and take a bus.
If that shape looks familiar later, good: it is the Strategy pattern, arrived at from first principles rather than from a catalog. Most patterns are this move applied to a specific kind of pain.
None of this makes inheritance a mistake. A single, shallow, genuinely "is a" hierarchy is clearer than three interfaces and a constructor full of wiring. Reach for composition when a second dimension of variation shows up, not before.
Composing Objects Principle
Assemble complex behavior from small objects with one job each, rather than growing one object that learns to do everything itself.
"Favor composition over inheritance," above, is about one specific fork in the road: when a new variant shows up, do you extend a parent or hold a reference? The Composing Objects Principle is the more general version, and it doesn't need an inheritance tree to be in the picture at all. It is a warning against the "god object" - a single class that has grown a method for every responsibility the system needs, because adding one more method to the class you already have was always the path of least resistance.
Go back to the software company. Nothing here needs a class hierarchy - there is no
Company subclass in sight - but a Company that implements hiring, payroll, tax filing,
client pitches, and office leasing itself, all as its own methods touching its own sprawling
field list, has the same disease inheritance gets blamed for: one object doing five jobs,
none of them cohesive with the others.
Company keeps every one of its public methods - callers notice nothing - but each method
is now one line of delegation to an object whose entire reason to exist is that one job. Add
a sixth responsibility later and it is a sixth small class, wired in next to the other five,
not a sixth method competing for space in an already-overloaded one.
Check yourself
What actually makes a piece of code hard to reuse in a new project?