Understanding SOLID Principles: Coding the Uncle Bob Way

Gopal SharmaGopal Sharma
7 min read

Why do we need SOLID Principles?

Before SOLID, developers had a messy OOP life:

  1. Fat Classes Doing Everything 🍔 → Hard to read & modify.

    • SRP: “Hey, do one job, not five!” (Splits big classes into focused ones).
  2. One Change, Everything Breaks 💥 → A nightmare to maintain.

    • OCP: “Extend me, don’t edit me!” (Adds features without touching old code).
  3. Tightly Coupled Code = No Flexibility 🤕

    • DIP: “Depend on interfaces, not details!” (Loose coupling = happy devs).
  4. Testing Was a Horror Show 👻

    • LSP: “You should be able to replace a base class reference with any of its derived class objects without breaking functionality!”.
  5. Mega Interfaces = Too Many Unused Methods

    • ISP: “Keep interfaces small, no unnecessary baggage!” (Less bloated code).

Result? Unhappy developers, messy code, more bugs, frustrated testers 😔, and scaling was a nightmare

SOLID Principles

SOLID is an acronym coined by Uncle Bob (Robert C. Martin) that stands for:

  • Single Responsibility Principle (SRP)

  • Open/Closed Principle (OCP)

  • Liskov Substitution Principle (LSP)

  • Interface Segregation Principle (ISP)

  • Dependency Inversion Principle (DIP)

Let’s understand these one by one fun.

  1. Single Responsibility Principle (SRP): A class should have only one responsibility in other words there should only one reason to change class.

    What does this mean?

    Imagine you own a restaurant. Would you hire only one person to cook, clean, serve, and manage accounts? No! Because one day, he’ll either burn the kitchen down or run away. Each person should have a single responsibility.

    The code you write Bad Code:

     class Employee
     {
         public void CalculateSalary() { /* Heavy Calculations*/ }
         public void PrintPaySlip() { /* Print bill*/ }
         public void SaveToDatabase() { /* Save employee details */ }
     }
    

    remember, A class should have only one responsibility so separate them.

    The code Uncle Bob wants you to write:

     /* new class for every unrelated responsibility */
     class SalaryCalculator { public void CalculateSalary() { /* Logic */ } }
     class PaySlipPrinter { public void PrintPaySlip() { /* Logic */ } }
     class EmployeeRepository { public void SaveToDatabase() { /* Logic */ } }
    
  2. Open/Closed Principle: A class must be open for extension and closed to modification.

    What does this mean?

    When we try to add new functionality to our application, we shouldn't need to make changes in the existing class.

    Bad Code:

     class Animal
     {
         public void Speak(string animalType)
         {
             if (animalType == "Dog") Console.WriteLine("Bark");
             else if (animalType == "Cat") Console.WriteLine("Meow");
             else if (animalType == "Duck") Console.WriteLine("Quack");
            // 🚨 Violation of Open-Closed Principle (OCP)!
             // Adding new animals? More if-else? BAD IDEA! 😩
             // Every new animal forces us to modify this class.
             // Instead, use polymorphism with an Animal base class and derived classes!
         }
     }
    

    Good Code:

     abstract class Animal { public abstract void Speak(); } // it can be interface or a simple class 
     class Dog : Animal { public override void Speak() => Console.WriteLine("Bark"); }
     class Cat : Animal { public override void Speak() => Console.WriteLine("Meow"); }
     class Duck : Animal { public override void Speak() => Console.WriteLine("Quack"); }
    

    Now, if we need to add a Cow, we just need to extend Animal and override Speak(). Not touching existing code! just extension not modification.

  3. Liskov Substitution Principle (LSP): Derived or child classes must be substitutable for their base/parent classes.

    Bad Code:

     class Rectangle
     {
         public virtual void SetWidth(int w) { /* Logic */ }
         public virtual void SetHeight(int h) { /* Logic */ }
     }
    
     class Square : Rectangle
     {
         public override void SetWidth(int w) { /* ohhhh 🤦‍♂️, sets both width and height */ }
         public override void SetHeight(int h) { /* ohhh 🤦‍♂️, sets both width and height */ }
     }
    

    A Square is not a perfect substitute for Rectangle, or we can say Rectangle is not good inheritance(base class) for Square because setting one dimension changes both. Bad inheritance!

    Good Code:

     abstract class Shape { public abstract int Area(); } // common function or properties will be here
     class Rectangle : Shape { public int Width, Height; public override int Area() => Width * Height; }
     class Square : Shape { public int Side; public override int Area() => Side * Side; }
    
  4. Interface Segregation Principle (ISP): Classes should not be forced to implement a function they do not need.

    What does this mean?

    Ever seen a universal remote with 300 buttons, but you only use Power, Volume, and Channel? That’s a bad interface. Let’s avoid this mess in code.

    Bad Code:

     interface IMachine
     {
         void Print();
         void Scan();
         void Fax();
     }
    
     class BasicPrinter : IMachine
     {
         public void Print() { Console.WriteLine("Printing..."); }
         public void Scan() { throw new NotImplementedException(); }
         public void Fax() { throw new NotImplementedException(); }
         // classes should not be forced to implement functions they do not need like these scan and fax methods
     }
    
     class Scanner : IMachine
     {
         public void Print() { throw new NotImplementedException(); }
         public void Scan() {Console.WriteLine("Scanning..."); }
         public void Fax() { throw new NotImplementedException(); }
         // classes should not be forced to implement functions they do not need like these Print and fax methods
     }
    

    Good Code:

     interface IPrinter { void Print(); }
     interface IScanner { void Scan(); }
     interface IFax { void Fax(); }
    
     class BasicPrinter : IPrinter
     {
         public void Print() { Console.WriteLine("Printing..."); } // printer needs only Print functionality
     }
    
  5. Dependency Inversion Principle (DIP): High level classes should not depend on low level classes actually both of these should depend on abstractions or interfaces.

    Bad Code:

     class MySQLDatabase  // Low-level module
     {
         public void SaveData(string data)
         {
             Console.WriteLine($"Data saved to MySQL: {data}");
         }
     }
    
     class DataService  // High-level module
     {
         private MySQLDatabase _db = new MySQLDatabase();  // Direct dependency ❌
    
         public void Store(string data)
         {
             _db.SaveData(data); // Tightly coupled! If we switch DB, we must change this class.
         }
     }
    

    Problems:

    DataService is dependent on MySQLDatabase.
    ❌ If we need to use PostgreSQL, we must modify DataService.
    Not flexible or scalable – violates Open-Closed Principle (OCP) too.

    Good Code:

     // create an interface
     interface IDatabase  // Abstraction 🎯
     {
         void SaveData(string data);
     }
    
     // creating different databases
     class MySQLDatabase : IDatabase  // Concrete implementation
     {
         public void SaveData(string data)
         {
             Console.WriteLine($"Data saved to MySQL: {data}");
         }
     }
    
     class PostgreSQLDatabase : IDatabase  // Another implementation
     {
         public void SaveData(string data)
         {
             Console.WriteLine($"Data saved to PostgreSQL: {data}");
         }
     }
    
     // add dependency of abstraction(interface) in high level class
     class DataService  // High-level module
     {
         private readonly IDatabase _database; // Depend on abstraction ✅
    
         public DataService(IDatabase database)
         {
             _database = database; // Inject dependency
         }
    
         public void Store(string data)
         {
             _database.SaveData(data); // Works with any database! 🚀
         }
     }
    
     IDatabase db = new MySQLDatabase();  // Swap with PostgreSQLDatabase anytime!
     DataService service = new DataService(db);
     service.Store("User info");
    

    Why is this better?

    DataService doesn’t care which database is used.
    ✔ We can add a new database (e.g., MongoDB) without modifying DataService.
    Follows Open-Closed Principle (OCP) – Open for extension, closed for modification.
    Easier testing – We can mock IDatabase for unit tests.

    Note: Using a single IDatabase interface for both PostgreSQLDatabase and MySQLDatabase might not always be the best approach, especially if they have different behaviors. Instead, you can use two separate interfaces to adhere to the Interface Segregation Principle (ISP) from SOLID.

    Key Takeaways of DIP

    1. High-level modules (business logic) should rely on abstractions, not on specific classes.

    2. Low-level modules (implementations) should also rely on the same abstractions.

    3. This approach reduces tight coupling and makes the system more flexible, scalable, and easier to test.

Conclusion

By following SOLID principles, we: ✔ Write cleaner, maintainable code ✔ Avoid future debugging nightmares 😱 ✔ Make our code future-proof 🚀

Believe me, the testing team will respect you a lot more when you start following Uncle Bob's principles. From now on, your experience will really matter in the coding world. Just kidding, guys! 😎🤣

So, the next time you write code, make sure it doesn’t make your future self rage quit! 🤣

0
Subscribe to my newsletter

Read articles from Gopal Sharma directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Gopal Sharma
Gopal Sharma