Understanding SOLID Principles in Software Development
May 20, 2024
|15 min read
SOLID is an acronym that represents five principles of object-oriented programming and design. These principles, when applied together, make it easier to create more maintainable, understandable, and flexible software. Let's dive into each principle and understand its significance.

SOLID stands for: - Single Responsibility Principle - Open-Closed Principle - Liskov Substitution Principle - Interface Segregation Principle - Dependency Inversion Principle
Single Responsibility Principle (SRP)
The Single Responsibility Principle states that a class should have only one reason to change. In other words, a class should have only one job or responsibility.
Example
// Bad: Class with multiple responsibilities
class User {
constructor(public name: string) {}
saveToDatabase() {
/* ... */
}
sendEmail() {
/* ... */
}
generateReport() {
/* ... */
}
}
// Good: Separate classes for different responsibilities
class User {
constructor(public name: string) {}
}
class UserPersistence {
saveToDatabase(user: User) {
/* ... */
}
}
class UserCommunication {
sendEmail(user: User) {
/* ... */
}
}
class UserReporting {
generateReport(user: User) {
/* ... */
}
}Open-Closed Principle (OCP)
The Open-Closed Principle states that software entities (classes, modules, functions, etc.) should be open for extension but closed for modification.
Example
class Rectangle {
constructor(public width: number, public height: number) {}
}
class AreaCalculator {
calculateArea(shapes: Rectangle[]) {
return shapes.reduce((sum, shape) => sum + shape.width * shape.height, 0);
}
}
// Good: Follows OCP
interface Shape {
getArea(): number;
}
class Rectangle implements Shape {
constructor(public width: number, public height: number) {}
getArea() {
return this.width * this.height;
}
}
class Circle implements Shape {
constructor(public radius: number) {}
getArea() {
return Math.PI * this.radius ** 2;
}
}
class AreaCalculator {
calculateArea(shapes: Shape[]) {
return shapes.reduce((sum, shape) => sum + shape.getArea(), 0);
}
}Liskov Substitution Principle (LSP)
The Liskov Substitution Principle states that objects of a superclass should be replaceable with objects of its subclasses without affecting the correctness of the program.
Example
// Bad: Violates LSP
class Bird {
fly() {
console.log("I can fly");
}
}
class Penguin extends Bird {
fly() {
throw new Error("I can't fly");
}
}
// Good: Follows LSP
interface Flyable {
fly(): void;
}
class Bird implements Flyable {
fly() {
console.log("I can fly");
}
}
class Penguin {
swim() {
console.log("I can swim");
}
}Interface Segregation Principle (ISP)
The Interface Segregation Principle states that clients should not be forced to depend on interfaces they do not use.
Example
// Bad: Violates ISP
interface Worker {
work(): void;
eat(): void;
sleep(): void;
}
// Good: Follows ISP
interface Workable {
work(): void;
}
interface Eatable {
eat(): void;
}
interface Sleepable {
sleep(): void;
}
class Human implements Workable, Eatable, Sleepable {
work() {
/* ... */
}
eat() {
/* ... */
}
sleep() {
/* ... */
}
}
class Robot implements Workable {
work() {
/* ... */
}
}Dependency Inversion Principle (DIP)
The Dependency Inversion Principle states that 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.
Example
// Bad: Violates DIP
class LightBulb {
turnOn() {
console.log("LightBulb turned on");
}
turnOff() {
console.log("LightBulb turned off");
}
}
class Switch {
private bulb: LightBulb;
constructor() {
this.bulb = new LightBulb();
}
operate() {
// ... determine if on or off
this.bulb.turnOn();
}
}
// Good: Follows DIP
interface Switchable {
turnOn(): void;
turnOff(): void;
}
class LightBulb implements Switchable {
turnOn() {
console.log("LightBulb turned on");
}
turnOff() {
console.log("LightBulb turned off");
}
}
class Fan implements Switchable {
turnOn() {
console.log("Fan turned on");
}
turnOff() {
console.log("Fan turned off");
}
}
class Switch {
constructor(private device: Switchable) {}
operate() {
// ... determine if on or off
this.device.turnOn();
}
}Conclusion
In summary, here is a quick overview of the SOLID principles:
- SRP (Single Responsibility): A class should have only one reason to change — one job or responsibility.
- OCP (Open-Closed): Software entities should be open for extension but closed for modification.
- LSP (Liskov Substitution): Subtypes must be replaceable for their base types without breaking correctness.
- ISP (Interface Segregation): Clients should not depend on interfaces they do not use.
- DIP (Dependency Inversion): High-level modules should depend on abstractions, not low-level details.
SOLID principles provide guidelines for creating more maintainable, flexible, and scalable software. They help developers write code that is easier to understand, modify, and extend over time. By following these principles, you can reduce the likelihood of introducing bugs when making changes to your codebase and improve overall software quality.
By applying these SOLID principles in your software development process, you can create more robust, maintainable, and flexible code. Remember that these principles are guidelines, not strict rules. Use them wisely and in context to improve your software design and architecture.
As you continue to practice and apply these principles, you'll find that your code becomes more modular, easier to test, and simpler to maintain over time.
TABLE OF CONTENTS