Making software designs more understandable, flexible, and maintainable
π Overview β’ π― Principles β’ ποΈ Examples β’ π Benefits β’ π Getting Started
SOLID is an acronym for five design principles that help create robust, maintainable, and scalable software:
graph TD
A[π§± SOLID Principles] --> B[S - Single Responsibility]
A --> C[O - Open/Closed]
A --> D[L - Liskov Substitution]
A --> E[I - Interface Segregation]
A --> F[D - Dependency Inversion]
B --> B1[One reason to change]
C --> C1[Open for extension<br/>Closed for modification]
D --> D1[Subtypes substitutable<br/>for base types]
E --> E1[No forced dependencies<br/>on unused interfaces]
F --> F1[Depend on abstractions<br/>not concretions]
style A fill:#e1f5fe
style B fill:#f3e5f5
style C fill:#e8f5e8
style D fill:#fff3e0
style E fill:#fce4ec
style F fill:#e0f2f1
π‘ "A class should have only one reason to change"
π Click to expand example
// β This class has multiple responsibilities
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
saveToDB() {
// Database logic - Responsibility 1
console.log('Saving to database...');
}
sendEmail() {
// Email logic - Responsibility 2
console.log('Sending email...');
}
validateInput() {
// Validation logic - Responsibility 3
return this.name && this.email;
}
}// β
Each class has a single responsibility
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
}
class UserRepository {
saveToDB(user) {
console.log(`Saving ${user.name} to database...`);
}
}
class EmailService {
sendEmail(user) {
console.log(`Sending email to ${user.email}...`);
}
}
class UserValidator {
validate(user) {
return user.name && user.email;
}
}π― Real-world analogy: Think of a restaurant where the chef cooks, the waiter serves, and the cashier handles payments - each has one clear responsibility!
π‘ "Software entities should be open for extension, but closed for modification"
π Click to expand example
// β Need to modify existing code for new shapes
class AreaCalculator {
calculateArea(shape) {
if (shape.type === 'circle') {
return Math.PI * shape.radius * shape.radius;
}
else if (shape.type === 'square') {
return shape.side * shape.side;
}
// Need to add more if-else for new shapes π
}
}// β
Extensible without modification
class Shape {
calculateArea() {
throw new Error('calculateArea method must be implemented');
}
}
class Circle extends Shape {
constructor(radius) {
super();
this.radius = radius;
}
calculateArea() {
return Math.PI * this.radius * this.radius;
}
}
class Square extends Shape {
constructor(side) {
super();
this.side = side;
}
calculateArea() {
return this.side * this.side;
}
}
// Easy to add new shapes without modifying existing code! π
class Triangle extends Shape {
constructor(base, height) {
super();
this.base = base;
this.height = height;
}
calculateArea() {
return (this.base * this.height) / 2;
}
}
class AreaCalculator {
calculateArea(shape) {
return shape.calculateArea();
}
}π― Real-world analogy: Like a USB port - you can plug in new devices without changing the port itself!
π‘ "Subtypes must be substitutable for their base types"
π Click to expand example
// β Ostrich breaks the expected behavior
class Bird {
fly() {
return 'Flying high in the sky! ποΈ';
}
}
class Duck extends Bird {
fly() {
return 'Duck flying over the pond! π¦';
}
}
class Ostrich extends Bird {
fly() {
throw new Error('Ostriches cannot fly! π«'); // Breaks LSP
}
}
// This will fail for Ostrich
function makeBirdFly(bird) {
return bird.fly(); // Expects all birds to fly
}// β
Proper inheritance hierarchy
class Bird {
move() {
return 'Moving around... π¦';
}
makeSound() {
return 'Chirp chirp! π΅';
}
}
class FlyingBird extends Bird {
fly() {
return 'Soaring through the sky! βοΈ';
}
move() {
return this.fly();
}
}
class FlightlessBird extends Bird {
run() {
return 'Running fast on the ground! πββοΈ';
}
move() {
return this.run();
}
}
class Duck extends FlyingBird {
fly() {
return 'Duck flying gracefully! π¦';
}
makeSound() {
return 'Quack quack! π¦';
}
}
class Ostrich extends FlightlessBird {
run() {
return 'Ostrich running at 70 km/h! πββοΈπ¨';
}
makeSound() {
return 'Boom boom! π';
}
}
// Now this works for all birds
function moveBird(bird) {
return bird.move(); // All birds can move
}π― Real-world analogy: Like different types of vehicles - all can transport people, but cars drive on roads while boats sail on water!
π‘ "Clients should not be forced to depend on interfaces they do not use"
π Click to expand example
// β Fat interface forces unnecessary implementations
class AllInOneMachine {
print(document) {
throw new Error('Must implement print');
}
scan(document) {
throw new Error('Must implement scan');
}
fax(document) {
throw new Error('Must implement fax');
}
copy(document) {
throw new Error('Must implement copy');
}
}
class SimplePrinter extends AllInOneMachine {
print(document) {
console.log(`Printing: ${document} π¨οΈ`);
}
// Forced to implement methods it doesn't need π
scan(document) {
throw new Error('This printer cannot scan');
}
fax(document) {
throw new Error('This printer cannot fax');
}
copy(document) {
throw new Error('This printer cannot copy');
}
}// β
Segregated interfaces
class Printer {
print(document) {
throw new Error('Must implement print');
}
}
class Scanner {
scan(document) {
throw new Error('Must implement scan');
}
}
class FaxMachine {
fax(document) {
throw new Error('Must implement fax');
}
}
class PhotoCopier {
copy(document) {
throw new Error('Must implement copy');
}
}
// Simple printer only implements what it needs
class BasicPrinter extends Printer {
print(document) {
console.log(`Printing: ${document} π¨οΈ`);
}
}
// Multi-function printer implements multiple interfaces
class MultiFunctionPrinter extends Printer {
constructor() {
super();
this.scanner = new AdvancedScanner();
this.faxMachine = new ModernFaxMachine();
this.photoCopier = new DigitalPhotoCopier();
}
print(document) {
console.log(`High-quality printing: ${document} π¨οΈβ¨`);
}
scan(document) {
return this.scanner.scan(document);
}
fax(document) {
return this.faxMachine.fax(document);
}
copy(document) {
return this.photoCopier.copy(document);
}
}
class AdvancedScanner extends Scanner {
scan(document) {
console.log(`Scanning: ${document} π`);
}
}
class ModernFaxMachine extends FaxMachine {
fax(document) {
console.log(`Faxing: ${document} π `);
}
}
class DigitalPhotoCopier extends PhotoCopier {
copy(document) {
console.log(`Copying: ${document} π`);
}
}π― Real-world analogy: Like a Swiss Army knife vs. individual tools - sometimes you need just a knife, not all the tools!
π‘ "Depend on abstractions, not on concretions"
π Click to expand example
// β High-level module depends on low-level module
class MySQLDatabase {
save(data) {
console.log(`Saving to MySQL: ${data} ποΈ`);
}
}
class UserService {
constructor() {
this.database = new MySQLDatabase(); // Direct dependency
}
saveUser(user) {
// Tightly coupled to MySQL
this.database.save(user);
}
}// β
Depend on abstractions
class Database {
save(data) {
throw new Error('save method must be implemented');
}
find(id) {
throw new Error('find method must be implemented');
}
}
class MySQLDatabase extends Database {
save(data) {
console.log(`πΎ Saving to MySQL: ${JSON.stringify(data)}`);
return { id: Math.random(), ...data };
}
find(id) {
console.log(`π Finding in MySQL with ID: ${id}`);
return { id, name: 'John Doe' };
}
}
class PostgreSQLDatabase extends Database {
save(data) {
console.log(`π Saving to PostgreSQL: ${JSON.stringify(data)}`);
return { id: Math.random(), ...data };
}
find(id) {
console.log(`π Finding in PostgreSQL with ID: ${id}`);
return { id, name: 'Jane Smith' };
}
}
class MongoDatabase extends Database {
save(data) {
console.log(`π Saving to MongoDB: ${JSON.stringify(data)}`);
return { _id: Math.random(), ...data };
}
find(id) {
console.log(`π Finding in MongoDB with ID: ${id}`);
return { _id: id, name: 'Bob Johnson' };
}
}
class UserService {
constructor(database) {
this.database = database; // Dependency injection
}
saveUser(user) {
return this.database.save(user);
}
getUser(id) {
return this.database.find(id);
}
}
// Usage examples - Easy to switch databases! π
const mysqlService = new UserService(new MySQLDatabase());
const postgresService = new UserService(new PostgreSQLDatabase());
const mongoService = new UserService(new MongoDatabase());
// All work the same way
mysqlService.saveUser({ name: 'Alice', email: 'alice@example.com' });
postgresService.saveUser({ name: 'Bob', email: 'bob@example.com' });
mongoService.saveUser({ name: 'Charlie', email: 'charlie@example.com' });π― Real-world analogy: Like electrical outlets - your device works with any outlet because it depends on the standard interface, not the specific power source!
mindmap
root)π§± SOLID Benefits(
π§ Maintainability
Easy to modify
Clear structure
Reduced bugs
π Flexibility
Easy extensions
Adaptable design
Future-proof
π§ͺ Testability
Isolated testing
Mock dependencies
Unit tests
β»οΈ Reusability
Component reuse
Less duplication
Modular design
π Reduced Coupling
Independent modules
Flexible architecture
Robust system
| Principle | Primary Benefit | Impact |
|---|---|---|
| SRP | π― Clarity | Each class has a clear, single purpose |
| OCP | π Extensibility | Add features without changing existing code |
| LSP | π Reliability | Predictable behavior across inheritance |
| ISP | β‘ Efficiency | Use only what you need |
| DIP | π§ Flexibility | Easy to swap implementations |
# Clone the repository
git clone https://github.com/manish0502/SOLID_Principles.git
# Navigate to project directory
cd SOLID_Principles
# Install dependencies
npm install --save-dev typescript ts-node
# Run examples
npx ts-node srp.ts// Import the examples
import { UserService, MySQLDatabase } from './examples/dependency-inversion.js';
import { Circle, Square, AreaCalculator } from './examples/open-closed.js';
// Try out the examples
const userService = new UserService(new MySQLDatabase());
userService.saveUser({ name: 'John', email: 'john@example.com' });
const circle = new Circle(5);
const square = new Square(4);
const calculator = new AreaCalculator();
console.log(`Circle area: ${calculator.calculateArea(circle)}`);
console.log(`Square area: ${calculator.calculateArea(square)}`);- π Clean Code by Robert C. Martin
- π₯ SOLID Principles Video Series
- π Design Patterns in JavaScript
- π§ͺ Test-Driven Development Guide
We welcome contributions! Please see our Contributing Guide for details.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
This project is licensed under the MIT License - see the LICENSE file for details.
If this project helped you understand SOLID principles better, please β star this repository!
Made with β€οΈ for better software design