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.
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:
Fragility:
Immobility:
Viscosity:
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) {}
}
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.
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);
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.
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();
}
}
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.