Darshan Jain

Dec 06, 2024 • 6 min read • 

Level Up Your Code: Understanding SOLID Principles with Real-World Examples

Level Up Your Code: Understanding SOLID Principles with Real-World Examples

SOLID principles are a set of five design guidelines intended to make software designs more understandable, flexible, and maintainable. They are crucial for writing clean, robust, and scalable code, especially in object-oriented programming.

These 5 principles are :
1. Single Responsibility Principle (SRP)
2. Open/Closed Principle (OCP)
3. Liskov’s Substitution Principle (LSP)
4. Interface Segregation Principle (ISP)
5. Dependency Inversion Principle (DIP)

These principles help to make your code loosely coupled instead of tightly coupled. Which helps you to update/change any dependency when needed.

Let's Design a BIRD and understand the need

"Design a Software System where we need to store all the species of Bird in an Object Oriented Manner".

class Bird { 
    String name;
    int age;
    int weight;
    int color;
    String type;

    void fly(...) { ... }
    void makeSound(...) { ... }
    void dance(...) { ... } 
    void walk(...) { ... }
}


Let's create some bird:

Bird b1 = new Bird();
b1.setType("crow");

Bird b2 = new Bird();
b2.setType("eagle");

Bird b3 = new Bird();
b3.setType("penguin");


Now, depending on the type of bird each fly method will have different behavior right? So fly method will look like this:

void fly(type) {
   if(type == "crow"){ ... }
   else if(type == "eagle"){ ... }
   else if(type == "penguin") { ... }
   ...
}


Too many if-else conditions right? Only these issues? NO!

1. Readability
2. Testing
3. Code Duplication - DRY (Don't repeat Yourself)
4. Less Code reusability

If you find nothing wrong with the above example, wait until the end of this article you will learn how to have better code by using these principles:

1. Single Responsibility Principle (SRP)

  • Definition: A class should have one, and only one, reason to change.

  • Means: A class should have a single responsibility. A class should not maintain multiple responsibilities.

  • Why: If a class has multiple responsibilities, changing to one might affect other methods. Teams won't be able to work properly.

  • Real-world example: Instead of having a single Bird class responsible for both bird attributes (name, colour) and behaviours (flying, singing), you could separate these concerns.

// Violating SRP
class Bird {
    String name;
    String color;

    void fly() { ... }
    void makeSound() { ... }
    void saveToDatabase() { ... } 
}

// Adhering to SRP
class BirdData {
    String name;
    String color;
}
class BirdBehavior {
    void fly() { ... }
    void makeSound() { ... }
}
class BirdRepository {
    void saveBird(BirdData birdData) { ... }
}

2. Open/Closed Principle (OCP)

  • Definition: Software entities (classes, modules, etc.) should be open for extension, but closed for modification.

  • Means: A class can be extended for its behaviour. But it can't be modified for specific use cases.

  • Why: Modifying existing code can introduce bugs or worse could break existing functionality.

  • Real-world example:

// Violating OCP

class Bird {
    void fly(String birdType) {
        if (birdType.equals("Crow")) { ... }
        else if (birdType.equals("Owl")) { ... } 
        // ... more if-else for other bird types
    }
}

// Adhering to OCP
interface Bird {
    void fly();
}
class Crow implements Bird {
    @Override
    public void fly() { ... }
}
class Owl implements Bird {
    @Override
    public void makeSound() { ... }
}

3. Liskov Substitution Principle (LSP)

  • Definition: Subtypes should be substitutable for their base types without altering the correctness of the program.

  • Means: We can use a subclass of Parent class wherever Parent is expected without causing problems.

  • Why: This makes the inheritance hierarchy well-designed. And makes your code flexible and dynamic.

  • Real-world example: Bird class with a fly method, and a Penguin class that inherits from a Bird, the Penguin's method shouldn't throw an exception or behave in a way that violates the general contract of flying (even though penguins can't fly). You might need to rethink your class hierarchy or use interfaces to achieve this.

// Violating OCP
class Bird {
    public void fly() { ... }
}
class Penguin extends Bird {
    @Override
    public void fly() {
        throw new UnsupportedOperationException("Penguins can't fly!");
    }
}
public static void main(String[] args) {
    Bird penguin = new Penguin();
    penguin.fly(); // Runtime exception: UnsupportedOperationException
}


// Adhering to OCP
interface Flyable { //can be implemented by Bird which fly.
    void fly();
}
abstract class Bird {
    public abstract void eat();
}
class Penguin extends Bird {
    @Override
    public void eat() { ... }
}
public class LSPAdherence {
    public static void main(String[] args) {
        Bird penguin = new Penguin();
        penguin.eat();
        penguin.fly(); //compile time error & editor will show red line
    }
}

4. Interface Segregation Principle (ISP)

  • Definition: Clients should not be forced to depend upon interfaces they don't use.

  • Means: Interface should be more specific.

  • Why: This will prevent the class from implementing methods they don't want to use saving us from complexity and errors.

  • Real-world example: Instead of having a single Bird interface with methods for flying, swimming, and singing, we could have separate interfaces like Flyable, Swimable, and Singable. This allows us to create bird classes that only implement the interfaces relevant to their abilities.

// Violating ISP

interface Bird {
    void fly();
    void swim();
    void sing();
}

// Adhering to ISP
interface Flyable {
    void fly();
}
interface Swimmable {
    void swim();
}
interface Singable {
    void sing();
}
class Duck implements Flyable, Swimmable {
    // ...
}

5. Dependency Inversion Principle (DIP)

  • Definition: High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions.

  • Means: The class should be dependent on the Interface and Abstract classes instead of another Class.

  • Why: This reduces tight coupling between two classes. making code easier to change

  • Real-world example: Suppose you have a BirdHouse class that needs to interact with different types of birds. Instead of directly depending on concrete bird classes like Sparrow, the BirdHouse could depend on an abstract Bird class or an interface. This allows us to add new bird types to the system without modifying the BirdHouse class.

// Violating DIP
class BirdHouse {
    private final Sparrow sparrow;
    public BirdHouse() { ... }
    public void provideShelter() { ... }
}

// Adhering to DIP
abstract class Bird {
    void liveInHouse(); 
}
class BirdHouse {
    private final Bird bird;
    public BirdHouse(Bird bird) { 
        this.bird = bird;
    }
    public void provideShelter() {
        bird.liveInHouse();
    }
}
class Sparrow implements Bird {
    @Override
    public void liveInHouse() { ... }
}

By following SOLID principles, you can write code that is:

  • More maintainable: Easier to understand, modify, and extend.

  • More testable: Smaller, more focused classes are easier to test.

  • More reusable: Decoupled components can be reused in different contexts.

  • More scalable: The codebase can adapt to changing requirements.

Remember, applying SOLID principles is an ongoing process. Start with the basics and gradually incorporate them into your development workflow to write cleaner, more robust, and scalable code.

And also one more thing. These are guidelines you should use but you will find yourself violating them when implementing multiple Design Patterns throughout your code. which is OKAY! but that doesn't mean you write bad code.

Hopefully, this helps you to understand SOLID principles, and why we need them. And for people who already knew it. It should act as a nice refresher.

Join Darshan on Peerlist!

Join amazing folks like Darshan and thousands of other people in tech.

Create Profile

Join with Darshan’s personal invite link.

2

10

0