SOLID - The First 5 Principles of Object Oriented Software Design Principles in 2024

7 min read
Last updated: Dec 1, 2024

S.O.L.I.D design principle comes from Object oriented programming guidelines. It is designed to develop software that can be easily maintained and extended; prevents code smells; easy to refractor; promotes agility and finally incorporates rapid + frequent changes quickly without bugs.

Generally, technical debt is the result of prioritizing speedy delivery over perfect code. To keep it under control - use SOLID principles, during development.

solid-design-principle-1

Robert Martin, is credited with writing the SOLID principles and stated 4 major software issues if S.O.L.I.D is not followed diligently. They are :

  • Rigidity:

    • Implementing even a small change is difficult since it’s likely to translate into a cascade of changes.
  • Fragility:

    • Any change tends to break the software in many places, even in areas not conceptually related to the change.
  • Immobility:

    • We’re unable to reuse modules from other projects or within the same project because those modules have lots of dependencies.
  • Viscosity:

    • Difficult to implement new features the right way.

SOLID is a guideline and not a rule. It is important to understand the crux of it and incorporate it with a crisp judgement. There can be a case when only few principles out of all is required.

S.O.L.I.D stands for:

  • Single Responsibility Principle (SRP);
  • Open Closed Principle (OCP);
  • Liskov Substitution Principle (LSP);
  • Interface Segregation Principle (ISP);
  • Dependency Inversion Principle (DIP);

Explanatory Video

Single Responsibility Principle (SRP)

Every function, class or module should have one, and only one reason to change, implies should have only one job and encapsulated within the class (stronger cohesion within the class).

It supports "Separation of concerns" — do one thing, and do it well!"

For example, consider this class:

class Menu {
  constructor(dish: string) {}
  getDishName() {}
  saveDish(a: Dish) {}
}

This class violates SRP. Here is why. It is managing the properties of menu and also handling the database. If there is any update in database management functions then it will affect the properties management functions as well, hence resulting in coupling.

More cohesive and Less Coupled class Instance.

// Responsible for menu management
class Menu {
  constructor(dish: string) {}
  getDishName() {}
}

// Responsible for Menu management
class MenuDB {
  getDishes(a: Dish) {}
  saveDishes(a: Dish) {}
}

Open Closed Principle (OCP)

Classes, functions, or modules should be opened for extensibility, but closed for modification. If you created and published a class - changes in this class, it can break the implementation of those, who are started using this class. Abstraction is the key to getting OCP right.

For example, consider this class:

class Menu {
  constructor(dish: string) {}
  getDishName() {}
}

We want to iterate through a list of dishes and return their cuisine.

class Menu {
constructor(dish: string){ }
getDishName() { // ... }

    getCuisines(dishName) {
      for(let index = 0; index <= dishName.length; index++) {
         if(dishName[index].name === "Burrito") {
            console.log("Mexican");
         }
         else if(dishName[index].name === "Pizza") {
            console.log("Italian");
         }
      }
    }

}

The function getCuisines() does not meet the open-closed principle because it cannot be closed against new kind of dishes.

If we add a new dish say Croissant, we need to change the function and add the new code like this.

class Menu {
constructor(dish: string){ }
getDishName() { // ... }

    getCuisines(dishName) {
      for(let index = 0; index <= dishName.length; index++) {
         if(dishName[index].name === "Burrito") {
            console.log("Mexican");
         }
         if(dishName[index].name === "Pizza") {
            console.log("Italian");
         }
         if(dishName[index].name === "Croissant") {
            console.log("French");
         }
      }
    }

}

If you observe, for every new dish, a new logic is added to the getCuisines() function. As per the open-closed principle, the function should be open for extension, not modification.

Here is how we can make the codebase meets the standard to OCP.

class Menu {
  constructor(dish: string) {}
  getCuisines() {}
}

class Burrito extends Menu {
  getCuisine() {
    return "Mexican";
  }
}

class Pizza extends Menu {
  getCuisine() {
    return "Italian";
  }
}

class Croissant extends Menu {
  getCuisine() {
    return "French";
  }
}

function getCuisines(a: Array<dishes>) {
  for (let index = 0; index <= a.length; index++) {
    console.log(a[index].getCuisine());
  }
}

getCuisines(dishes);

This way we do not need to modify the code whenever a new dish is required to add. We can just create a class and extends it with the base class.

Liskov Substitution Principle (LSP)

A sub-class must be substitutable for their base type, states that we can substitute a subclass for its base class without affecting behaviour and hence helps us conform to the “is-a” relationship.

In other words, subclasses must fulfil a contract defined by the base class. In this sense, it’s related to Design by Contract that was first described by Bertrand Meyer .

For example, Menu has a function getCuisines which is used by Burrito, Pizza, Croissant and didn’t created individual functions.

class Menu {
  constructor(dish: string) {}
  getCuisines(cuisineName: string) {
    return cuisineName;
  }
}

class Burrito extends Menu {
  constructor(cuisineName: string) {
    super();
    this.cuisine = cuisineName;
  }
}

class Pizza extends Menu {
  constructor(cuisineName: string) {
    super();
    this.cuisine = cuisineName;
  }
}

class Croissant extends Menu {
  constructor(cuisineName: string) {
    super();
    this.cuisine = cuisineName;
  }
}

const burrito = new Burrito();
const pizza = new Pizza();
burrito.getCuisines(burrito.cuisine);
pizza.getCuisines(pizza.cuisine);

Interface Segregation Principle (ISP)

A client should never be forced to implement an interface that it doesn’t use or clients shouldn’t be forced to depend on methods they do not use.

The “interface” word in the principle name does not strictly mean an interface, it could be an abstract class .

For example

interface ICuisines {
  mexican();
  italian();
  french();
}

class Burrito implements ICuisines {
  mexican() {}
  italian() {}
  french() {}
}

If we add a new method in the interface, all the other classes must declare that method or error will be thrown.

To solve it

interface BurritoCuisine {
  mexican();
}
interface PizzaCuisine {
  italian();
}

class Burrito implements BurritoCuisine {
  mexican();
}

Many client-specific interfaces are better than one general-purpose interface.

Dependency Inversion Principle (DIP)

Entities must depend on abstractions not on concretions. It states that the high level module must not depend on the low level module, decouple them and make use of abstractions.

High-level modules are part of an application that solve real problems and use cases. They are more abstract and map to the business domain (business logic); They tell us what the software should do (not how, but what);

Low-level modules contain implementation details that are required to execute the business policies; About how the software should do various tasks;

For Example

const pool = mysql.createPool({});
class MenuDB {
  constructor(private db: pool) {}
  saveDishes() {
    this.db.save();
  }
}

Here, class MenuDB is a high-level component whereas a pool variable is a low-level component. To solve it, we can separate Connection instance.

interface Connection {
  mysql.createPool({})
}

class MenuDB {
   constructor(private db: Connection) {}
   saveDishes() {
      this.db.save();
   }
}

Ending Note

Code that follows S.O.L.I.D. principles can be easily shared, extended, modified, tested, and refactored without any problems. With each real-world application of these principles benefits of the guidelines will become more apparent.

Anti-patterns and improper understanding can lead to STUPID code: Singleton, Tight Coupling, Un-testability, Premature Optimization, In-descriptive Naming, and Duplication. SOLID can help developers stay clear of these.

Any thoughts, let's discuss on twitter

Sharing this article is a great way to educate others like you just did.



If you’ve enjoyed this issue, do consider subscribing to my newsletter.


Subscribe to get more such interesting content !

Tech, Product, Money, Books, Life. Discover stuff, be inspired, and get ahead. Box Piper is on Twitter and Discord. Let's Connect!!

To read more such interesting topics, let's go Home

More Products from the maker of Box Piper:

Follow GitPiper Instagram account. GitPiper is the worlds biggest repository of programming and technology resources. There is nothing you can't find on GitPiper.

Follow SharkTankSeason.com. Dive into the riveting world of Shark Tank Seasons. Explore episodes, pitches, products, investment details, companies, seasons and stories of entrepreneurs seeking investment deals from sharks. Get inspired today!.


Scraper API

More Blogs from the house of Box Piper: