Skip to main content

📐 SOLID Principles

Five rules for maintainable code

What Is SOLID?

SOLID = Five design principles for better code

S - Single Responsibility Principle
O - Open/Closed Principle
L - Liskov Substitution Principle
I - Interface Segregation Principle
D - Dependency Inversion Principle

Goal: Maintainable, flexible, understandable code.

S - Single Responsibility Principle

The Analogy

A chef in a restaurant:

  • Good: Chef cooks food
  • Bad: Chef cooks, takes orders, cleans tables, manages finances

Too many responsibilities! When something breaks, you don't know where to look.

The Principle

A class should have only ONE reason to change.

Bad: UserManager
  - Saves to database
  - Sends emails
  - Validates input
  - Generates reports

Good:
  - UserRepository (database)
  - EmailService (emails)
  - UserValidator (validation)
  - ReportGenerator (reports)

Benefits

✓ Easier to understand
✓ Easier to test
✓ Easier to change
✓ Fewer surprises when modifying

O - Open/Closed Principle

The Analogy

A power strip:

  • Open for extension: Plug in any device
  • Closed for modification: Don't rewire the strip

Add new devices without changing the strip itself.

The Principle

Software entities should be:
  - Open for EXTENSION
  - Closed for MODIFICATION

Add new behavior by adding NEW code.
Don't change existing, working code.

Example

Bad: Modifying existing code for each new shape

if shape.type == "circle":
    area = π * r²
elif shape.type == "square":
    area = s²
elif shape.type == "triangle":  ← Must modify!
    area = ...

Good: Extend with new classes

Shape (interface) → getArea()
Circle extends Shape
Square extends Shape
Triangle extends Shape  ← Just add new class!

Benefits

✓ New features don't break old code
✓ Reduced regression risk
✓ Easier maintenance

L - Liskov Substitution Principle

The Analogy

A rental car:

  • You can drive any car in the rental fleet
  • They all have steering wheel, pedals, gear
  • Any car can substitute for another

If a specific car didn't have a steering wheel, it would break the contract!

The Principle

Subtypes must be substitutable for their base types.

If you use Parent class, any Child should work.
Child shouldn't surprise you with different behavior.

Example

Bad: Subclass breaks expectations

class Rectangle:
    setWidth(w)
    setHeight(h)

class Square extends Rectangle:
    setWidth(w):
        width = w
        height = w  ← Surprise! Also changes height?!

User expects rectangle behavior, gets square behavior.

Good: Don't inherit if contract differs.

Signs of Violation

✗ Overriding method to throw exception
✗ Overriding method to do nothing
✗ Subclass adds restrictions parent didn't have
✗ Client needs to check which type it actually is

I - Interface Segregation Principle

The Analogy

A Swiss Army knife vs specialized tools:

  • Swiss knife: Everything in one tool
  • Many features you rarely use
  • Awkward for specific tasks

Better to have specific tools for specific jobs.

The Principle

Clients shouldn't depend on interfaces they don't use.

Many small, specific interfaces
Better than one large, general interface.

Example

Bad: Fat interface

interface Worker:
    work()
    eat()
    sleep()

Robot implements Worker:
    work() ✓
    eat() → What?! Robots don't eat!
    sleep() → Robots don't sleep!

Good: Segregated interfaces

interface Workable:
    work()

interface Eatable:
    eat()

interface Sleepable:
    sleep()

Human implements Workable, Eatable, Sleepable
Robot implements Workable

Benefits

✓ Classes only implement what they need
✓ Smaller, focused interfaces
✓ Easier to understand and maintain

D - Dependency Inversion Principle

The Analogy

Electrical outlets (again):

  • Lamp depends on outlet (interface), not on power plant
  • Can change power source without changing lamp
  • Outlet is the abstraction between them

The Principle

High-level modules shouldn't depend on low-level modules.
Both should depend on abstractions.

Abstractions shouldn't depend on details.
Details should depend on abstractions.

Example

Bad: Direct dependency

class OrderService:
    def __init__(self):
        self.database = PostgresDatabase()  ← Concrete!
        self.emailer = GmailSender()        ← Concrete!

Can't change database without changing OrderService.

Good: Depend on abstractions

class OrderService:
    def __init__(self, database: Database, emailer: EmailService):
        self.database = database
        self.emailer = emailer

Inject any implementation!
PostgresDatabase or MongoDatabase
GmailSender or SendGridSender

Benefits

✓ Easy to swap implementations
✓ Easy to test (inject mocks)
✓ Decoupled modules
✓ Flexible architecture

SOLID Summary Table

PrincipleKey IdeaRemember
Single ResponsibilityOne reason to changeDo one thing well
Open/ClosedExtend, don't modifyAdd, don't change
Liskov SubstitutionSubtypes work everywhereChildren honor parent contracts
Interface SegregationSmall, focused interfacesDon't force unused methods
Dependency InversionDepend on abstractionsInject dependencies

Common Violations

God Class

One class does everything.
Violates: Single Responsibility

Fix: Split into focused classes.

Switch on Type

switch (object.type):
    case A: doA()
    case B: doB()
    case C: doC()

Violates: Open/Closed

Fix: Polymorphism. Each type implements behavior.

Square-Rectangle Problem

Square extends Rectangle but breaks width/height independence.

Violates: Liskov Substitution

Fix: Separate hierarchy or immutable shapes.

Monster Interface

interface DoEverything:
    method1(), method2(), ... method50()

Violates: Interface Segregation

Fix: Split into focused interfaces.

Creating Dependencies

class Service:
    def __init__(self):
        self.dep = ConcreteDependency()  ← Creating internally

Violates: Dependency Inversion

Fix: Inject dependencies through constructor.

When to Apply SOLID

Good Fit

✓ Long-lived applications
✓ Growing codebases
✓ Team development
✓ Need for testing
✓ Changing requirements

Might Be Overkill

✗ Scripts and one-offs
✗ Very small programs
✗ Throwaway code
✗ Performance-critical hot paths

FAQ

Q: Must I follow all five?

They're guidelines, not laws. Use judgment. Some situations warrant trade-offs.

Q: Does SOLID add complexity?

Initially, yes. Pays off as code grows and changes.

Q: SOLID for functional programming?

Principles apply differently. Dependency injection still relevant. SRP maps to function focus.

Q: How do I learn to apply SOLID?

Practice! Refactor existing code. Review others' code. Read design pattern books.


Summary

SOLID principles guide object-oriented design toward maintainable, flexible code.

Key Takeaways:

  • Single Responsibility: One job per class
  • Open/Closed: Extend don't modify
  • Liskov Substitution: Subtypes work interchangeably
  • Interface Segregation: Small focused interfaces
  • Dependency Inversion: Depend on abstractions

Apply SOLID for code that's easier to understand, test, and change!

Leave a Comment

Comments (0)

Be the first to comment on this concept.

Comments are approved automatically.