Skip to content
Computer Science Fundamentals
Source Code
← Home

Design patterns and SOLID

100 questions

Right
0
Wrong
0
Not answered
100
Difficulty
Number of questions

100 questions match this filter

Theory summary

Imagine you build things with LEGO. After a while you notice that you keep solving the same small problems: how to make a wall strong, how to build a door that opens, how to join two sets that do not fit together. Smart builders gave names to the good answers, so they can say "use the hinge trick" instead of explaining every brick. A design pattern is exactly that: a name for a good answer to a problem that keeps coming back when we write programs.

Before the patterns there are five rules of thumb called SOLID. They are not laws. They are advice about how to write classes (a is like a cookie cutter, and each cookie is an object) so that the program is easy to change later. Programs always change: a new rule from the boss, a new payment company, a new kind of customer. Code that is easy to change saves days of work and avoids new bugs.

You will meet all of this in real life: in the apps you use, in the frameworks that programmers download, and in every job interview about software. At the end of this page you will know the five SOLID principles, ten well-known patterns (Factory, Builder, Singleton, Adapter, Decorator, Facade, Strategy, Observer, Command and State), the Repository and dependency injection used in back-end systems, and the warning signs of bad design. You will also know when not to use a pattern, which is just as important.

The big picture and the vocabulary

Software is never finished. Somebody always asks for one more thing. So good design is not about making the code pretty. It is about making the next change cheap: the change should touch few places, and it should not break things that already work.

To talk about design we need a few words. Learn them now, because every section uses them.

The seven words of design
WordWhat it meansEveryday picture
CohesionHow well the parts inside one class belong together. High cohesion is good.A pencil case that holds only pencils and pens
CouplingHow much one class depends on other classes. Low coupling is good.Toys joined by one thin string, not welded together
InterfaceThe list of things a class promises to do, without saying how.The buttons of a remote control
ContractThe full promise of a method: what the caller must give, what the method gives back, and which errors it may raise.A pizza order: you give an address, you get a pizza
AbstractionA simple view that hides the details.You drive a car without knowing the engine
PatternA named, reusable answer to a problem that keeps appearing.A recipe with a name

There are three families of patterns. Creational patterns are about creating objects. Structural patterns are about how objects fit together. Behavioural patterns are about how objects talk to each other and share work. The famous book that listed 23 of them was written by four authors, nicknamed the "Gang of Four".

Watch the video: Design Patterns in Object Oriented Programming (playlist by Christopher Okhravi)↑ Back to the contents

Single responsibility principle (SRP)

Think of a school bag that is also a lunch box, a pillow and a bicycle helmet. When one part breaks, you cannot fix it without touching the others. The single responsibility principle says that a class should have only one reason to change.

A "reason to change" is a person or a group who may ask for a change. Look at the class below. The finance team may change the tax rule, the design team may change the receipt text, and the database team may change how data is saved. That is three reasons to change in one place.

class Invoice {
	calculateTax(): number { /* tax rules */ }
	saveToDatabase(): void { /* SQL */ }
	sendByEmail(): void { /* SMTP */ }
}
Three subjects in one class: it breaks the SRP

The cure is to split it: one class for the tax calculation, one for saving, one for e-mail. Now each change touches one small class, and the tests of the tax rule do not need a database.

Cohesion and two mirror smells

A class has methods and fields that all deal with a single concept. A hint to find a missing split: if some methods use only some fields and other methods use only the other fields, there are probably two classes hiding inside one. A class that keeps growing a new block for every new case (a new job role, a new customer kind) is also a sign.

The two smells that mirror each other
SmellWhat you seeWhat it says
Divergent ChangeOne class is edited for many different reasons (tax rule and report layout)Lack of cohesion: the class has several responsibilities
Shotgun SurgeryOne change forces small edits in many classesA responsibility was broken into too many pieces
↑ Back to the contents

Open-closed principle (OCP)

A game console has slots for new controllers. You do not open the console and solder wires to add one more. The console is closed for modification (nobody edits its insides) and open for extension (new controllers just plug in). The open-closed principle asks the same of code: new behaviour should be added as new code, without editing the code that already exists and is already tested.

The usual way is an . The class talks to an interface, and each new behaviour is a new class that implements it. This is called . The place where the class calls something it does not know in detail is its extension point.

interface DiscountRule {
	apply(total: number): number;
}

class PriceCalculator {
	constructor(private readonly rules: DiscountRule[]) {}

	total(base: number): number {
		return this.rules.reduce((value, rule) => rule.apply(value), base);
	}
}
A new discount is a new class that implements DiscountRule, added to the list where the program is assembled
BEFORE: area() has an if per shape
  area(shape) --> if circle ... if square ... (edit me!)

AFTER: each shape knows its own area
  area(shape) --> shape.area()
                   |-- Circle.area()
                   |-- Square.area()
                   '-- (new) Triangle.area()
A chain of ifs on a type is closed for extension

Where the change goes

No program is closed against every change. Somewhere, the new class must be created. A factory (a function that decides which class to create) can hold one switch for that. This is acceptable: the variation is confined to one point, and the rest of the code only knows the abstraction. The principle does not remove change. It chooses where change happens.

The same if (x instanceof ...) chain copied in a helper, in a report and in a screen is the real warning sign. When a new type appears, every copy must be found and edited, and a forgotten copy fails silently.

↑ Back to the contents

Liskov substitution principle (LSP)

Suppose you ask for "a pet" and you get a goldfish. Then you try to take it for a walk. It fails. A goldfish is a pet, but not a pet that can walk. The Liskov substitution principle says that a subtype (a child class, or any class that implements an interface) must work wherever its base type is expected. If code works with the parent, it must work with the child, and without knowing which child it got.

class Bird {
	fly(): string { return 'flying'; }
}
class Penguin extends Bird {
	fly(): string { throw new Error('penguins cannot fly'); }
}
function release(birds: Bird[]): string[] {
	return birds.map((bird) => bird.fly());
}
Penguin cannot replace Bird: release() blows up

In biology a penguin is a bird. In code, what decides inheritance is the contract of the methods, not the phrase "is a". The fix is to change the hierarchy, for example keeping fly only in a type for birds that fly.

The contract has two sides

A method has a and a . It also has a way of reporting failure, such as returning null or throwing an error. The compiler checks only the types of the signature. It does not check these promises, so tests of the contract must do it.

Ask for less, deliver more
Part of the contractWhat a subtype may do
Precondition (what it asks for)Keep it or weaken it (ask for less). Never make it stricter
Postcondition (what it delivers)Keep it or strengthen it (deliver more). Never weaken it
How failure is reportedUse the same way. If the base returns null for a missing user, the subtype must not throw

The classic example is Square extends Rectangle. A Rectangle promises that changing the width does not change the height. A Square cannot keep that promise, because changing one side changes the other. Another case: a base Account promises that after a withdrawal the balance is never negative, and an OverdraftAccount allows a negative balance. It weakened a postcondition.

The strongest symptom of a broken substitution is instanceof in the client code (if (account instanceof StudentAccount) ...). The client had to learn the subtypes, so every new subtype needs another branch. Then a Liskov violation becomes an open-closed violation too.

↑ Back to the contents

Interface segregation principle (ISP)

You borrow a Swiss army knife just to open a letter. You are given 30 tools you do not need, and if one of them is broken you worry about it too. The interface segregation principle says that clients should not be forced to depend on methods they do not use. An interface is a contract seen from the side of the one who uses it. If it mixes methods for different users, everybody is tied to everything.

interface Machine {
	print(doc: string): void;
	scan(): string;
	fax(number: string): void;
}

class SimplePrinter implements Machine {
	print(doc: string): void { /* prints */ }
	scan(): string { throw new Error('not supported'); }
	fax(number: string): void { throw new Error('not supported'); }
}
A fat interface: the printer must write methods it cannot honour

A method written as "not supported" is a loud sign: the interface asks for more than the class has. The fix is to split by capability: Printer, Scanner, Fax. Now code that needs to scan asks for a Scanner, and a simple printer never gets there. The type system works for you.

What a fat interface costs

  • Implementers must write methods they cannot honour, and they throw errors.
  • Clients depend on methods they never call, so a change in an unused method can still force them to change.
  • When a thirteenth method, used by one client, is added to an interface with twelve methods and eight implementers, all eight classes must change, even those whose clients never call it.
  • Test doubles (fake objects used in tests) must have all the methods. A report that only reads needs a fake with one method, not four.

A good example is a user repository. A report only needs to read. Give it a UserReader with findAll, and keep save and delete in another interface. The concrete class implements both, so it stays the same. The report cannot write or delete by mistake, and its fake needs a single method.

ISP and SRP are two views of cohesion. The SRP asks why the type itself changes. The ISP asks what each client needs from a type. A repository can have one responsibility (storing users) and still need separate reader and writer interfaces.

↑ Back to the contents

Dependency inversion principle (DIP)

A lamp has a plug. It does not have a wire fixed into one wall of one house. The lamp (the important thing) defines the shape of the plug, and any socket that fits can be used. The dependency inversion principle says: high-level modules and low-level modules should both depend on abstractions. The code must not import the details. Instead, the rule says what it needs through an interface, and the detail implements it.

class OrderService {
	private readonly database = new MySqlDatabase();

	place(order: Order): void {
		if (order.total() <= 0) throw new Error('empty order');
		this.database.insert('orders', order);
	}
}
The business rule creates its own detail with new: it changes when the database changes

The new inside the class is a double decision: the class chooses what it needs and which implementation it gets. The fix is to declare an interface in the language of the business (OrderStore) and receive it in the constructor. Then the rule can be tested with a fake in memory.

NORMAL                         INVERTED

OrderService                   OrderService --> OrderStore
     |                                              ^
     v                                              |
MySqlDatabase                  MySqlOrderStore -----'

source code arrow: rule -> detail   detail -> rule's interface
The arrows of the source code point towards the rule

Notice the difference between the flow of calls and the source code dependency. At run time OrderService still calls the database. In the code, both know only the interface. Also notice who owns the interface: it lives with the high-level module, and the database adapter implements it. That ownership is what "inversion" means. Putting the interface beside the database would not invert anything.

DIP is not the same as DI

They travel together, but they are different things
TopicDIP (dependency inversion)DI (dependency injection)
What it isA design rule: depend on abstractionsA technique: someone from outside hands the dependency in
Question it answersWhat should I depend on?Who creates it and gives it to me?
Can exist alone?YesYes

Interfaces are less risky than concrete classes because they change less often: they carry no detail and many classes depend on them. But the rule is not "put an interface on everything". The criterion is stability. The text class of the standard library (String) changes almost never, does no input or output and behaves the same every time, so you can use it directly. Things that change, talk to the outside world or give a different answer on each call (database, clock, network, random numbers) go behind an interface.

↑ Back to the contents

Creational patterns: Factory, Builder, Singleton

Making an object involves choices: which class, with which parts, in what starting state. If every user of the object repeats those choices, a change must be repeated everywhere. Creational patterns take the choices of creation out of the code that uses the object, so the user depends only on the abstract type.

Factory chooses which class. Builder assembles one object in steps.
PatternThe problemThe idea
FactoryThere are several classes for the same role, and who decides which one is copied aroundOne function or class decides which class to create
BuilderOne class with many data, some optional, in a long list of parametersNamed steps, then a final build that checks everything
SingletonWe want exactly one instance of a classOne instance plus a global way to reach it

Factory

function createNotifier(channel: string): Notifier {
	if (channel === 'email') return new EmailNotifier();
	if (channel === 'sms') return new SmsNotifier();
	throw new Error(`unknown channel: ${channel}`);
}
The code that uses a Notifier never writes new EmailNotifier

The value of a factory is what it hides. The client should depend on the interface of the product, not on its concrete class. If the client casts the result to the concrete class, the benefit is lost.

Builder

// hard to read: what do null, 30, true and false mean?
new HttpRequest('POST', '/orders', null, null, 30, true, false, body);

// with a builder, every value has a name
new RequestBuilder().method('POST').path('/orders').timeout(30).body(body).build();
Many positional parameters against named steps

The build method is the natural place for rules that cross fields, such as "a POST must have a body". The object is only born if the whole set makes sense. This is also why a rich constructor is better than an empty constructor followed by setters: with setters, between new and the last set the object exists in an invalid state. Another care: build must give the product a copy of mutable data, such as a list of headers. If it hands out the builder's own array, a later call on the builder changes a product that was already delivered.

Singleton

Singleton joins two decisions: there is only one instance, and it is reachable from anywhere. The first can be a fair need. The second is a global variable in disguise, and it is why people look at the pattern with suspicion today.

class InvoiceService {
	issue(order: Order): Invoice {
		const now = Clock.getInstance().now();
		const rate = TaxTable.getInstance().rateFor(order.state);
		return new Invoice(order, rate, now);
	}
}
The signature new InvoiceService() says it needs nothing. It lies.

A constructor signature is the most honest documentation of what a class needs. A hidden global access makes it lie. Worse, a Singleton with changeable state links all the code that uses it. In tests this shows up as order dependence: the first test increments a counter, the second test starts with the leftover value and fails, although each one passes alone. A constant exported from a module (export const config = loadConfig()) is the same thing in JavaScript: importers are tied to that instance.

↑ Back to the contents

Structural patterns: Adapter, Decorator, Facade

Structural patterns are about how to put classes together. Three of them look alike, because each one puts a class in front of other code. What differs is the intent.

A simple test: could the wrapped object be used in place of the wrapper? Decorator, yes. Adapter, no.
PatternInterface it showsIntent
AdapterA different one: it converts a class to the interface the client already expectsMake a mismatch fit
DecoratorThe same interface as the object it wrapsAdd behaviour (log, cache, retry)
FacadeA new, simpler one, for several classesOne easy entry point to a complex subsystem

Adapter

interface PaymentGateway {
	charge(cents: number): Receipt;
}

class AcmePayGateway implements PaymentGateway {
	constructor(private readonly sdk: AcmePaySdk) {}

	charge(cents: number): Receipt {
		const result = this.sdk.createTransaction({ amount: cents / 100, currency: 'BRL' });
		return { id: result.transactionId, approved: result.status === 'OK' };
	}
}
The adapter translates names, units (cents against whole currency) and answers

The system owns the stable contract (PaymentGateway). The vendor's types never leave the adapter. When the vendor is replaced, only the adapter changes and the business rules stay as they are. The cost of changing vendor becomes the cost of one class.

Decorator

A decorator implements an interface and receives, in its constructor, another object of the same interface. Because the shape is the same, decorators can be stacked in any combination, like putting layers of clothes on a doll.

// Logged writes every call, Cached remembers answers
const a: Users = new Logged(new Cached(new SqlUsers()));
const b: Users = new Cached(new Logged(new SqlUsers()));
Same pieces, different order, different behaviour

The order is part of the behaviour. In a, Logged is outside, so every call is recorded, even those answered by the cache. In b, Cached is outside, so the second find('42') is answered by the cache and never reaches Logged: it is not written to the log. Both are valid and they answer different questions.

Why not inheritance? With inheritance, every combination of features is a subclass. With n independent features that is 2ⁿ − 1 classes (counting each combination with at least one feature, ignoring order). With decorators you write n classes and combine them when the program is assembled.

Classes needed for logging, cache and retry features
Inheritance, 3 features
7 classes
Decorators, 3 features
3 classes
Inheritance, 6 features
63 classes
Decorators, 6 features
6 classes

A decorator is a subtype of what it wraps, so the Liskov principle applies. A cache decorator whose save does not clear the stored product makes find return old data after a write. It broke the contract of the interface. A contract test, run against the plain repository and against the decorated one, catches it.

Facade

A subsystem often has many classes that must be called in a certain order. The facade keeps that knowledge in one place, so clients depend on one class and not on ten. The subsystem can change inside without hurting them. The limit is clear: the facade knows the order of the calls, not the rules.

↑ Back to the contents

Behavioural patterns: Strategy, Observer, Command, State

Behavioural patterns are about how objects share work and talk to each other. In each of them a class stops deciding something itself, and hands the decision to another object that can be swapped.

Four ways to move a decision out of a class
PatternIntentSign in the code
StrategyPut interchangeable algorithms behind one interfaceAn if per kind, each branch an independent calculation
ObserverOne object tells many others when something happensA pair of calls: subscribe and publish
CommandKeep a request as an object, with its dataQueue, history, undo and redo
StateChange behaviour when the internal state changesThe same status field tested in every method

Strategy

Imagine going to school: by bus, by bike or on foot. The goal is the same, and the way changes. Strategy separates what varies (how to calculate) from what stays (who needs the result). Each shipping kind gets a class that can be tested alone, and a new kind does not edit the others.

type Pricing = (base: number) => number;

const regular: Pricing = (base) => base;
const blackFriday: Pricing = (base) => base * 0.7;

function checkout(base: number, pricing: Pricing): number {
	return pricing(base);
}
No class, still a Strategy

Patterns describe roles and relations, not syntax. When the strategy has one method and no state, a function type plays the role of the interface. Use a class when it needs data or more than one operation.

Observer

A YouTube channel does not know who its subscribers are. It just publishes, and everyone who subscribed is told. In the Observer pattern the publisher does not know who listens, so a new reaction (add loyalty points when an order is paid) is one more subscribe at start-up, and the order class is not edited. It is the open-closed principle applied to events.

publish(event: string): void {
	for (const listener of [...this.listeners]) {
		try { listener(event); } catch (error) { this.errors.push(error); }
	}
}
Safe delivery: a copy of the list, and each listener isolated
  • Iterate over a copy. If listener A cancels itself during delivery, a plain loop moves B to its place and skips it. Publishing over a snapshot means: who was subscribed when the event started receives it.
  • Isolate each listener. If B throws and nobody catches, C is never called and the error reaches the publisher, exactly what the pattern meant to decouple. Catch and record the error.
  • Cancel when you leave. The subject keeps a reference to each observer. A screen that closes without cancelling stays in memory and keeps being called. This leak is the "lapsed listener". A good subscribe returns the function that cancels.

Command

A normal method call happens and vanishes. A command is the same intention kept as data: which action, on whom, with which arguments. That lets you put it in a queue, repeat it, and undo it. If each command knows how to reverse itself, a history with two stacks gives undo and redo.

run(a) run(b) run(c)   history = [a, b, c]
undo()                 pops c  -> history = [a, b]
undo()                 pops b  -> history = [a]

Effects still applied: only a
Running a new command clears the redo stack
Undo goes in reverse order: the last one in is the first one out

State

An order goes from pending to paid to shipped. Each phase answers the same operation differently: you cannot ship a pending order. With conditionals, every method begins with a test on status. With the State pattern each phase is a class, and the allowed transitions are explicit. The rule that refuses ship for a pending order lives in the pending state class.

Count what grows. With 4 states and 5 operations written as methods with conditionals, a fifth state forces you to review all 5 methods. With State, it is one new class plus the transitions that lead to it. The reverse also holds: if operations grow more than states, conditionals can be the better choice.

Same structure, different intent
TopicStrategyState
What the alternatives areIndependent ways to do the same taskPhases of a life cycle
Who swaps themChosen from outsideSwapped by the operations themselves, as a consequence
↑ Back to the contents

Repository and dependency injection

When you ask a librarian for a book, you do not walk into the storage room and search the shelves. You say the title, and the librarian brings it. A Repository is that librarian for your data. It gives the business rules an interface that looks like a collection (add, find, save) and hides how the data is stored: SQL, files or memory.

interface OrderRepository {
	find(id: string): Order | null;
	save(order: Order): void;
}

class InMemoryOrders implements OrderRepository {
	private readonly items = new Map<string, Order>();
	find(id: string) { return this.items.get(id) ?? null; }
	save(order: Order) { this.items.set(order.id, order); }
}
The rule speaks of orders. Only the real repository knows SQL.

Because the rule only knows the interface, it can be tested with an in-memory repository: fast and the same every time. A second, slower group of tests checks the real database repository. A contract test that runs against both implementations makes sure they behave the same way.

Dependency injection

Dependency injection means a class stops creating or fetching what it needs and receives it from outside. The most common form is constructor injection: dependencies are visible in the signature, mandatory and can be read-only. Looking at the constructor tells you everything the class needs.

class RegisterUser {
	constructor(
		private readonly users: UserRepository,
		private readonly mailer: Mailer,
	) {}

	execute(email: string): void {
		this.users.add(new User(email));
		this.mailer.send(email, 'Welcome');
	}
}
Constructor injection
main()  <-- composition root: the only place with 'new'
  |  creates connection
  |  creates SqlOrderRepository(connection)
  |  creates RegisterUser(repo, mailer)
  v
the rest of the program only knows interfaces
Where the object graph is assembled

The place where everything is created and joined is called the composition root, near the entry point of the program. Writing new SqlOrderRepository(connection) there keeps the rest of the system dependent only on abstractions.

Two traps

  • Transaction boundary. To save an order and reduce the stock atomically (all or nothing), the transaction belongs to the use case, which opens a unit of work that covers both repositories. A repository takes care of one kind of object and does not know which larger operation it is part of.
  • Captive dependency. A service created once at start-up that receives, in its constructor, an object that should be created per request keeps the object of the first request forever. An object may depend only on objects that live as long as it does or longer. Per-request data comes as a method argument, or through a provider asked at each use.
↑ Back to the contents

Anti-patterns and when not to use a pattern

A code smell is a hint that the design may have a problem, like a strange smell in the fridge. An anti-pattern is a popular bad answer. Patterns also have a price: more types, more files and one more jump to understand the flow. They pay off only if the problem they solve really exists.

Six smells and their cost
SmellHow to recognise itWhy it hurts
God ClassA huge class that holds much of the logic and knows almost all othersLow cohesion, hard to test, everybody edits it and conflicts
Shotgun SurgeryOne change forces small edits in many classesSomething that changes together is not together
Feature EnvyA method reads many getters of another object to calculate something about that objectThe calculation belongs to the other object
Refused BequestA subclass overrides inherited methods so they do nothing or throwBreaks Liskov: it wanted the code, not the contract
Anaemic modelClasses with only fields, getters and setters, and all rules in servicesProcedural code in disguise: any service can leave an object invalid
Speculative generalityInterface, factory and builder, each with one implementation, "in case"Everybody pays for flexibility nobody used

Tell, Don't Ask and the Law of Demeter

// asks for everything, then decides
const city = order.getCustomer().getAddress().getCity().getName();

// tells the order what is needed
const city = order.shippingCity();
A chain of getters couples the caller to four classes

The Law of Demeter says a method should talk only to its immediate neighbours. A chain of getters crosses several objects, so if the address changes shape, every chain scattered through the system breaks. Asking the first object for the result keeps the knowledge of the structure in one place. The same idea, Tell, Don't Ask, cures Feature Envy: move the behaviour next to the data it uses.

About the anaemic model: an object-oriented design keeps each rule next to the data it protects (encapsulation). If the domain classes are just bags of fields, any service can put an order in an invalid state, and the same rule gets rewritten in many places.

When not to apply a pattern

  • With one algorithm and no sign of a second, Strategy is not justified.
  • A record screen with two fields built with an interface, an abstract factory and a Builder, each with one implementation, is speculative generality. Wait for the second real case: refactoring to a pattern when the need appears costs less than carrying the structure for years.
  • Applying a pattern as a precaution usually produces the wrong abstraction, and the wrong flexibility gets in the way of the change that really arrives.
↑ Back to the contents

Putting it all together

The principles and the patterns are connected. The patterns are often the way to follow a principle. When you see a problem, ask which pain you feel, then pick the principle or the pattern that removes it.

From the pain to the cure
Pain you feelPrinciple that explains itPattern or technique that helps
A class changes for many reasonsSRPExtract each subject into its own class
Every new case edits an old if chainOCPStrategy, Factory, polymorphism
A subtype throws or surprises its clientsLSPChange the hierarchy, or use composition
Implementers write "not supported" methodsISPSplit the interface by role
Business rules import database or e-mailDIPInterface owned by the rule, DI, Adapter, Repository
A constructor with many parameters(creation)Builder, rich constructor
A hidden global that breaks testsDIPInject the single instance instead of a Singleton
Behaviours that stack in any orderOCP, compositionDecorator
Many classes to call in order(simplicity)Facade
Reactions to an eventOCPObserver
Undo, queue, repeat(request as data)Command
Status tested in every methodOCPState

A fair note on cost: the refactored code has more types and more lines than the first version. That is the price. It pays off where changes like these really arrive, and it is waste where they never do. Change does not disappear. The designs only choose where it lands.

  1. Write simple code that works, with tests.
  2. Watch for pain: a class that changes for many reasons, an if chain that grows, a test that needs a database.
  3. Name the pain with a principle or a smell.
  4. Apply the smallest pattern that removes it, and no more.
  5. Keep the tests green during the refactoring.
↑ Back to the contents