SOLID
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:
- Single Responsibility Principle (SRP)
- Open/Closed Principle (OCP)
- Liskov Substitution Principle (LSP)
- Interface Segregation Principle (ISP)
- Dependency Inversion Principle (DIP)
Single Responsibility Principle (SRP)
Definition: A class (or struct) should have one and only one reason to change.
Before applying SRP:
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:
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:
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):
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:
type Bird struct{}
func (b Bird) Fly() {
fmt.Println("Flying")
}
type Penguin struct {
Bird
}
func (p Penguin) Fly() {
panic("Penguins cannot fly!")
}
After LSP:
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:
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:
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:
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:
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 |
---|---|---|
SRP | One responsibility per class/module | Separate report generation and saving logic |
OCP | Open for extension, closed for modification | Add new shapes without changing the calculator |
LSP | Subtypes replace base types without breaking behavior | Penguins swim instead of fly |
ISP | Split interfaces to avoid unused methods | Separate Work and Eat interfaces |
DIP | Depend on abstractions, not concretions | Service uses Database interface, not specific DB |
These principles make code cleaner, more maintainable, and easier to test.