You have probably been asked at least once in an interview, “Could you explain the SOLID principles?”
But honestly, although people can recite all five principles, few can continue with, “So how do I apply them to my code?”
This time, I’ll explain SOLID through practical situations rather than textbook definitions. Because there is a lot to cover, I’m splitting it into two parts. Today covers the first three letters: SRP, OCP, and LSP.
SOLID is an acronym for the five principles of object-oriented design organized by Robert Martin (Uncle Bob).
- SRP — Single Responsibility Principle
- OCP — Open-Closed Principle
- LSP — Liskov Substitution Principle
- ISP — Interface Segregation Principle
- DIP — Dependency Inversion Principle
The five principles may seem unrelated, but they share one core message: reduce the scope of code that must be changed when change happens.
SRP — Single Responsibility Principle
The textbook definition is “a class should have only one responsibility,” but Uncle Bob later rephrased it more effectively.
A module should be responsible to one, and only one, actor.
Here, an actor is the person or department requesting changes to the code. Suppose we have a class like this.
class Employee {
func calculatePay() { ... } // The accounting team asked for a change
func reportHours() { ... } // The HR team asked for a change
func save() { ... } // The development team(DBA)asked for a change
}
The three methods all have different owners. If changing calculatePayat the accounting team’s request touches shared logic, the HR report can silently become incorrect. When this happens in practice, finding the cause is difficult.
SRP summarizes this as: “If the people requesting changes are different, separate the code too.” Splitting payroll calculation, work reports, and persistence into separate classes prevents an accounting request from affecting HR functionality.
OCP — Open-Closed Principle
The definition is “open for extension, closed for modification.” It may sound cryptic at first, but it becomes clear in code.
// If this function must be modified every time a payment method is added
func pay(method: String, amount: Int) {
if method == "card" { ... }
else if method == "kakaopay" { ... }
else if method == "naverpay" { ... }
// New payment method = modify else if = existing code
}
With this structure, adding Toss Pay requires opening and modifying a function that already works. Every modification carries the risk of breaking existing payment methods.
// A structure where adding a new payment method only requires “adding” it
let payments: [String: Payment] = [
"card": CardPayment(),
"kakaopay": KakaoPayment(),
"naverpay": NaverPayment(),
// Adding Toss Pay = one line to register a new class
]
func pay(method: String, amount: Int) {
payments[method]?.process(amount: amount)
}
The existing code remains untouched (closed to modification), and functionality grows simply by adding a new class (open to extension). The trick is to use this structure only at points that change frequently. Applying it everywhere would violate YAGNI, as discussed in the previous article.
LSP — Liskov Substitution Principle
The name sounds the most intimidating, but the meaning is simple.
A child class must be usable wherever its parent class is used without breaking the program.
The famous counterexample is the rectangle-square problem. If we use inheritance based on the mathematical notion that “a square is a rectangle,” this happens.
class Rectangle {
var width = 0
var height = 0
func setWidth(_ w: Int) { width = w }
func setHeight(_ h: Int) { height = h }
}
class Square: Rectangle {
override func setWidth(_ w: Int) { width = w; height = w } // Because it is a square
override func setHeight(_ h: Int) { width = h; height = h }
}
// Code expecting a rectangle
func test(_ rect: Rectangle) {
rect.setWidth(5)
rect.setHeight(4)
print(rect.width * rect.height) // 20expects Squarebut if it is a square 16
}
The moment you pass in Square, the obvious expectation that “a width of 5 and a height of 4 gives an area of 20” breaks. LSP says that even if an is-a relationship appears valid, you should not use inheritance when it violates behavioral contracts.
The practical signal is this: if a child class overrides a parent method to throw an exception or leaves it empty, that inheritance is highly likely to violate LSP.
Closing out Part One
Let’s leave the three principles covered today in one line each.
- SRP — If the people requesting changes differ, separate the code too.
- OCP — Make frequently changing points respond through additions instead of modifications.
- LSP — A child must not break the parent’s contracts. If it must, the inheritance is wrong.
In the next article, I’ll cover the remaining two letters: ISP (Interface Segregation) and DIP (Dependency Inversion). DIP will be especially interesting because it directly connects to why modern frameworks all use dependency injection.

![Cover image for [SOLID #1] SOLID in Five Letters: Understand It Instead of Memorizing It (SRP · OCP · LSP)](/assets/images/posts/7b95a0fb-b586-4f62-b56e-4a8b31a29dfd/1.jpg)