Skip to main content

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:

  1. Single Responsibility Principle (SRP)
  2. Open/Closed Principle (OCP)
  3. Liskov Substitution Principle (LSP)
  4. Interface Segregation Principle (ISP)
  5. 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

PrincipleDescriptionExample
SRPOne responsibility per class/moduleSeparate report generation and saving logic
OCPOpen for extension, closed for modificationAdd new shapes without changing the calculator
LSPSubtypes replace base types without breaking behaviorPenguins swim instead of fly
ISPSplit interfaces to avoid unused methodsSeparate Work and Eat interfaces
DIPDepend on abstractions, not concretionsService uses Database interface, not specific DB

These principles make code cleaner, more maintainable, and easier to test.