SOLID Design Principles
Building Better Software
Building Better Software
In object-oriented programming, writing code that works is only half the battle. The real challenge is writing code that is easy to maintain, understand, and extends as your project grows. This is where SOLID comes in -a set of five design principles that serve as the gold standard for clean architecture.
1. Single Responsibility Principle
A class should have only one job to do, and everything within it should be closely related, thus promoting reusability. If a class handles too many things, it becomes fragile; changing one part of the logic might accidentally break another.
Ex:
A TodoList class that manages tasks and handles file saving. By splitting the responsibilities: the list logic in one class and the data persistence
in another, we're adhering to the principle.
class TodoList {
constructor() {
this.items = [];
}
addItem(text) { this.items.push(text); }
removeItem(index) { this.items.splice(index, 1); }
toString() { return this.items.toString(); }
}
class DatabaseManager {
saveToFile(data, filename) {
fs.writeFileSync(filename, data.toString());
}
}
Open-Closed Principle
Software entities should be open for extension, but closed for modification. We should be able to add new features or functionality without changing any of the existing code of the class.
Ex:
A DeveloperFilter class with specific methods like filterByName or filterByLanguage. Everytime we add a new prop on Developer class, we have to
update its method in the filter class. By refactoring the property-based filters, no longer need to modify the source code as the data structure evolves.
class DeveloperFilter {
// This method is "closed" to modification but "open" to any property you pass
filterByProp(array, propName, value) {
return array.filter((item) => item[propName] === value);
}
// filterByName √ removed
// filterByPosition √ removed
}
class Developer {
constructor(name, position) {
this.name = name;
// Adding position will not change DeveloperFilter
this.position = position;
}
}
Liskov Substitution Principle
A Child class should be able to do everything that a Parent class can. Objects of a superclass should be replaceable with objects of its subclasses without breaking the application. If Square is a subclass of Rectangle, we should be able to use Square anywhere we use Rectangle
Whenever extending a child's class does not fulfill the behaviour of its parent, rethink hierarchy. We might just need a more general parent class (e.g. Shape) instead of forcing a relationship that doesn't fit.
Interface Segregation Principle
They provide a contract for our classes and methods. No client should be forced to depend on methods it does not use. In TypeScript, we use interfaces to define the structure or shape of an object and specify the properties and methods that an object has or should have.
Ex:
A Vehicle interface with both drive() and fly() methods forces a standard Car class to implement a fly() method it doesn't need.
By splitting the Vehicle interface, we no longer have to implement interfaces we don't need.
interface ICar {
drive(): string;
}
interface IPlane {
fly(): string;
}
// Regular car
class Car implements ICar {
public drive(): string {
return 'Running Down A Dream';
}
}
// Car from the future
class CovertToPlane implements ICar, IPlane {
public drive(): string {
return 'Running Down A Dream'
}
public fly(): string {
return 'Learning To Fly & Free Falling';
}
}
Dependency Inversion Principle
High-level modules should not depend on low-level modules. Both should depend on abstractions. Our main logic shouldn't be too tightly coupled to specific tools or databases.
Ex:
A PersistenceManager class that uses if (db instanceof FileSystem) is too dependent on specific low-level classes.
If we add a new database type, we would have to rewrite the high-level manager class.
The PersistenceManager class doesn't need to know which type of data storage it is, so we standardise the interface of the low-level classes.
class PersistanceManager {
saveData(db, data) {
// It doesn't care if FileSystem, ExternalDB, LocalStorage
db.save(data);
}
}
