is a set of five design principles introduced by Robert C. Martin (Uncle Bob) to make software designs more maintainable, scalable, and robust. It stands for:
S ingle Responsibility Principle (SRP)
O pen/Closed Principle (OCP)
L iskov Substitution Principle (LSP)
I nterface Segregation Principle (ISP)
D ependency Inversion Principle (DIP)
Single Responsibility Principle (SRP)
Definition : A class (or struct) should have one and only one reason to change.
Before applying SRP:
Copy type Report struct{}
func (r Report) GenerateReport() {
fmt.Println("Generating report...")
}
func (r Report) SaveToFile(filename string) {
fmt.Println("Saving report to file:", filename)
}
After applying SRP:
Copy type ReportGenerator struct{}
func (r ReportGenerator) GenerateReport() {
fmt.Println("Generating report...")
}
type FileSaver struct{}
func (f FileSaver) SaveToFile(filename string) {
fmt.Println("Saving to file:", filename)
}
Explanation : Each struct has a single responsibility now: one generates the report, and another saves it.
Open/Closed Principle (OCP)
Definition : Software entities should be open for extension but closed for modification.
Before OCP:
Copy type AreaCalculator struct{}
func (a AreaCalculator) CalculateArea(shape string, dimensions ...float64) float64 {
if shape == "circle" {
return 3.14 * dimensions[0] * dimensions[0]
} else if shape == "rectangle" {
return dimensions[0] * dimensions[1]
}
return 0
}
After OCP (Extensible with interfaces):
Copy type Shape interface {
Area() float64
}
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return 3.14 * c.Radius * c.Radius
}
type Rectangle struct {
Length, Width float64
}
func (r Rectangle) Area() float64 {
return r.Length * r.Width
}
type AreaCalculator struct{}
func (a AreaCalculator) CalculateArea(s Shape) float64 {
return s.Area()
}
Explanation : New shapes can be added by implementing the Shape
interface without modifying AreaCalculator
.
Liskov Substitution Principle (LSP)
Definition : Subtypes must be substitutable for their base types without altering the correctness of the program.
Before LSP:
Copy type Bird struct{}
func (b Bird) Fly() {
fmt.Println("Flying")
}
type Penguin struct {
Bird
}
func (p Penguin) Fly() {
panic("Penguins cannot fly!")
}
After LSP:
Copy type Bird interface {
Move()
}
type Sparrow struct{}
func (s Sparrow) Move() {
fmt.Println("Flying")
}
type Penguin struct{}
func (p Penguin) Move() {
fmt.Println("Swimming")
}
func main() {
var bird Bird
bird = Sparrow{}
bird.Move() // Output: Flying
bird = Penguin{}
bird.Move() // Output: Swimming
}
Explanation : The Bird
interface represents general behavior, and specific birds implement it in ways that don't break substitutability.
Interface Segregation Principle (ISP)
Definition : Clients should not be forced to depend on methods they do not use.
Before ISP:
Copy type Worker interface {
Work()
Eat()
}
type HumanWorker struct{}
func (h HumanWorker) Work() {
fmt.Println("Working...")
}
func (h HumanWorker) Eat() {
fmt.Println("Eating lunch...")
}
type RobotWorker struct{}
func (r RobotWorker) Work() {
fmt.Println("Working...")
}
func (r RobotWorker) Eat() {
// Robots don't eat
panic("Robots don't eat!")
}
After ISP:
Copy type Worker interface {
Work()
}
type Eater interface {
Eat()
}
type HumanWorker struct{}
func (h HumanWorker) Work() {
fmt.Println("Working...")
}
func (h HumanWorker) Eat() {
fmt.Println("Eating lunch...")
}
type RobotWorker struct{}
func (r RobotWorker) Work() {
fmt.Println("Working...")
}
func main() {
human := HumanWorker{}
robot := RobotWorker{}
human.Work() // Output: Working...
human.Eat() // Output: Eating lunch...
robot.Work() // Output: Working...
// robot.Eat() // Error: Robots don't eat!
}
Explanation :
Interfaces are split based on specific needs, so RobotWorker
doesn’t need to implement Eat()
.
Dependency Inversion Principle (DIP)
Definition : High-level modules should not depend on low-level modules. Both should depend on abstractions.
Before DIP:
Copy type MySQLDatabase struct{}
func (db MySQLDatabase) Connect() {
fmt.Println("Connecting to MySQL...")
}
type Service struct {
Database MySQLDatabase
}
func (s Service) PerformTask() {
s.Database.Connect()
fmt.Println("Performing task...")
}
After DIP:
Copy type Database interface {
Connect()
}
type MySQLDatabase struct{}
func (db MySQLDatabase) Connect() {
fmt.Println("Connecting to MySQL...")
}
type PostgreSQLDatabase struct{}
func (db PostgreSQLDatabase) Connect() {
fmt.Println("Connecting to PostgreSQL...")
}
type Service struct {
Database Database
}
func (s Service) PerformTask() {
s.Database.Connect()
fmt.Println("Performing task...")
}
func main() {
mysql := MySQLDatabase{}
postgres := PostgreSQLDatabase{}
service := Service{Database: mysql}
service.PerformTask() // Output: Connecting to MySQL... Performing task...
service.Database = postgres
service.PerformTask() // Output: Connecting to PostgreSQL... Performing task...
}
Explanation : The Service
depends on the Database
interface, not specific implementations. This makes it easy to switch databases.
Summary
Principle
Description
Example
One responsibility per class/module
Separate report generation and saving logic
Open for extension, closed for modification
Add new shapes without changing the calculator
Subtypes replace base types without breaking behavior
Penguins swim instead of fly
Split interfaces to avoid unused methods
Separate Work
and Eat
interfaces
Depend on abstractions, not concretions
Service uses Database
interface, not specific DB
These principles make code cleaner, more maintainable, and easier to test.
Last updated 3 months ago