Hey there, 👋 Awesome Developers! 🚀
Today, let's dive into the basics of SOLID principles. If you're ready to level up your coding game! 👇 Let's roll!
In the fast-changing world of coding, making clean and powerful code is crucial for building strong and flexible apps. join us as we explore SOLID Principles - the secret recipe of crafting code that's easy to maintain and works amazingly well! 🌟✨
Introduction 🚀
SOLID, introduced by Robert C. Martin (Uncle Bob), is a set of five design principles for creating a clean and effective object-oriented code. Here we'll break down each SOLID principle and see how they work in the context of coding with javascript.
1. Single Responsibility principle (SRP) 🎯
This is one of the SOLID Principles of object-oriented design. simply put, it suggests that a class should have only one reason to change, or in other words, it should have only one responsibility. SRP says, "Hey, stick to just one job, and do it really well."
Why is this more important ?
Maintainability: When a class has a single responsibility, it becomes easier to understand, modify, and maintain. if you need to make a change, you know exactly where to look.
Reusability: Smaller, focused classes are often more reusable in different parts of your application. you can use them like building blocks to assemble different functionalities.
Flexibility: With a clear and single responsibility, classes become more adaptable to change. if requirements shift, you can modify or extend individual classes without affecting the entire codebase.
Example
Let's look at the difference between the code before and after applying the Single Responsibility Principle (SRP) clearer:
Before applying SRP:
class Report {
constructor(data) {
this.data = data;
}
generateReport() {
console.log(`Generating report for ${this.data}`);
}
saveToFile() {
console.log(`Saving report to file: ${this.data}`);
// logic for saving report to a file
}
}
const report = new Report("Sales Data");
report.generateReport();
report.saveToFile();
In this version, the Report
class is doing two things: Generating a report and saving it to a file. It's responsible for both creating the report content and managing file operations.
After applying SRP:
class Report {
constructor(data) {
this.data = data;
}
generateReport() {
console.log(`Generating report for ${this.data}`);
}
}
class ReportSaver {
saveToFile(report) {
console.log(`Saving report to file: ${report.data}`);
// logic for saving report to a file
}
}
const report = new Report("Sales Data");
report.generateReport();
const reportSaver = new ReportSaver();
reportSaver.saveToFile(report);
In the improved version, we've applied SRP. The Report
class now focuses solely on generating the report, and a new class, ReportSaver
, takes care of saving the report to a file. Each class has a single responsibility, making the code more modular and easier to understand and maintain. This separation adheres to the Single Responsibility Principle, ensuring that each class has only one reason to change.
2. Open/Closed Principle (OCP) 🚪
This Suggests that a class should be open for extension but closed for modifications. In simpler terms, this means you should be able to add a new feature or functionalities to a system without altering the existing code.
If the concept is still unclear, let me explain it differently.
The Open/Closed Principle (OCP) is like a rule in a programming that says you can add new things in your code without changing the old stuff. imagine your code is like a LEGO set - you can keep adding new pieces without breaking the ones you already snapped together.
Why is OCP Important ?
Maintainability: OCP helps make code easier to handle. When you want to add new things, you don't have to touch the parts that are already working well. This way, there's less chance of making mistakes and messing up the existing code.
Scalability: When the software grows (upgrades), being able to add new features without messing with the current code becomes vital for its growth. This way, your code can expand and adapt to new requirements.
Reduced Risk: Modifying the existing code can bring unexpected problems. OCP helps avoid this by keeping any changes in new code, making it easier to test and fix issues.
Team Collaboration: When multiple developers work on a project, OCP Allows them to add new features independently without interfering with each other's work.
Example
Consider a system that calculates the area of shapes.
Before applying OCP:
class Circle {
radius;
constructor {
this.radius = radius;
}
calculateArea(){
return Math.PI * this.radius ** 2;
}
}
// Adding a new shape violates OCP
class Square {
side;
constructor(side) {
this.side = side;
}
calculateArea() {
return this.side ** 2;
}
}
// Object instantiation
const circle = new Circle(5);
const square = new Square(4);
circle.calculateArea(); // 78.54
square.calculateArea(); // 16
In this case, if you want to add anew shape like a square, you have to go back and changing the existing code. This Breaks the Open/Closed principle.
After applying OCP:
Now, Let's make it better with the Open/Closed Principle:
class Shape(){
calculateArea(){
throw new Error("This method should be overridden by subclasses")
}
}
class Circle extends Shape {
radius;
constructor(){
super();
this.radius = radius;
}
calculateArea(){
return Math.PI * this.radius ** 2
}
}
class Square extends Shape {
side;
constructor(side){
super();
this.side = side;
}
calculateArea(){
return this.side ** 2;
}
}
// Object instantiation
const circle = new Circle(5);
const square = new Square(4);
circle.calculateArea(); // 78.54
square.calculateArea(); // 16
In this code, Shape
is the parent class, and Circle
and Square
are its subclasses. when you create an instance of Circle
or Square
, the super()
statement is used to call the constructor of the Shape
class. This is important because it allows the initialization of any properties or setup defined in the Shape
class before adding the specific properties and behavior in the constructor of the subclass.
In Simpler terms, super()
ensures that both the parent (Shape
) and the Child (Circle
or Square
) classes get properly initialized. It's a way to extend the behavior of the parent class while keeping everything consistent.
3. Liskov Substitution Principle (LSP) 🔄
The Liskov Substitution Principle states that objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program.
In the simpler terms, if a class is subclass of another class, you should be able to use objects of the subclass wherever objects of the superclass are used, without introducing errors.
If you're still not getting it, Let me simplify it further.
Imagine you have a big category of things (a superclass) and some more specific things that belong to that category (subclasses). LSP suggests that you can use the specific things whenever you use the big category things messing up your program.
Why is this more important ?
Code Flexibility: LSP promotes the interchangeability of objects, allowing for flexibility in code design.
Consistent Behavior: It ensures that subclasses maintain consistent behavior with the superclasses, reducing unexpected surprises.
Example
Let's use a straightforward code example. 🦅🐧
Before applying LSP:
class Bird {
fly(){
console.log("Flying High!");
}
}
class Penguin extends Bird {
// Penguins can't fly, violation LSP
fly(){
console.log("I can't fly!");
}
}
// Object Instantiation
const bird = new Bird();
const penguin = new Penguin();
bird.fly(); // "Flying High!"
penguin.fly(); // "I can't fly!"
In this, The Penguin
class violates LSP by not being able to fly, which is expected from its superclass Bird
.
After applying LSP:
class Bird {
fly() {
console.log("Flying high!");
}
}
class Penguin extends Bird {
// Penguins should override fly appropriately
swim() {
console.log("Swimming gracefully!");
}
}
// Object instantiation
const bird = new Bird();
const penguin = new Penguin();
bird.fly(); // Output: "Flying high!"
penguin.fly(); // Output: "Flying high!"
penguin.swim(); // Output: "Swimming gracefully!"
In this, The Penguin
class adheres to LSP by introducing a new behavior swim
without changing the expected behavior of flying. Both Bird
and Penguin
instances can be used interchangeably where a Bird
is expected, ensuring consistency.
4. Interface Segregation Principle (ISP) 🤝
The Interface Segregation Principle suggests that a class should not be forced to implement interfaces it doesn't use. In simpler terms, it's better to have several small, specific interfaces than one large, all-encompassing interface.
If the concept is still unclear, Let me explain it differently,
The Interface Segregation Principle (ISP) is like a rule in coding that says: "Don't make a class do things it doesn't need to do. It's better to have many small and specific sets of tasks (interfaces) for a class rather than one big set that does everything."
It's like telling a chef to focus on their specialty dishes instead of making them handle every type of cuisine. 🍳🍕
Why is this more Important ?
Flexibility: It allows for more flexibility in implementing interfaces, preventing unnecessary dependencies on methods that aren't relevant.
Avoiding Bloat: Classes don't need to implement methods they don't use, keeping the codebase clean and avoiding unnecessary bloat.
In JavaScript, lacking an explicit interface
keyword, I'm using TypeScript in this example instead of JavaScript. If you want to implement a similar approach in JavaScript, you can rely on implicit interfaces, where classes or objects share a common set of methods.
Example
Let's create a Example in Typescript for Implementation of Interface Segregation Principle (ISP).
Before applying ISP:
interface Worker {
work(): void;
eat(): void;
}
class Engineer implements Worker {
work() {
console.log("Engineer working...");
}
eat() {
console.log("Engineer eating...");
}
}
class Manager implements Worker {
work() {
console.log("Manager working...");
}
eat() {
console.log("Manager eating...");
}
}
// Object instantiation
const engineer = new Engineer();
const manager = new Manager();
// Example usage
engineer.work(); // Output: "Engineer working..."
engineer.eat(); // Output: "Engineer eating..."
manager.work(); // Output: "Manager working..."
manager.eat(); // Output: "Manager eating..."
In this, the Worker
interface has both work and eat methods, and both Engineer
and Manager
are forced to implement both methods.
After applying ISP:
// With ISP
interface Workable {
work(): void;
}
interface Eatable {
eat(): void;
}
class Worker implements Workable, Eatable {
work() {
console.log("Working...");
}
eat() {
console.log("Eating...");
}
}
class Engineer implements Workable {
work() {
console.log("Engineer working...");
}
}
class Manager implements Workable, Eatable {
work() {
console.log("Manager working...");
}
eat() {
console.log("Manager eating...");
}
}
// Object instantiation
const worker = new Worker();
const engineer = new Engineer();
const manager = new Manager();
// Example usage
worker.work(); // Output: "Working..."
worker.eat(); // Output: "Eating..."
engineer.work(); // Output: "Engineer working..."
manager.work(); // Output: "Manager working..."
manager.eat(); // Output: "Manager eating..."
In this, we split the interface into Workable
and Eatable
, allowing classes to implement only the interfaces relevant to their functionality.
5. Dependency Inversion Principle (DIP) 🔄
The Dependency Inversion Principle emphasizes high-level modules should not depend on low-level modules but rather both should depend on abstractions. Additionally, it advocates that abstractions should not depend on details; details should depend on abstractions.
If you're still not getting it, Let's simplify it further.
The Dependency Inversion Principle (DIP) is like a rule in coding that says: "Don't have important parts of your code rely too much on each other. Instead, make them both rely on general plans (abstractions). And remember, these general plans shouldn't worry about specific details; the details should take their cues from the general plans."
It's like building with LEGO bricks, where each brick follows a common design, and the specifics of each brick don't bother the overall structure. 🧱🌐
Why is this more Important ?
Flexibility: It promotes a flexible and extensible design by decoupling high-level and low-level components.
Easy Maintenance: Changes in low-level details don't impact high-level policies, making the system easier to maintain.
Example
Let's consider a system called Smart Home Control.
Before applying DIP:
class LightBulb {
turnOn() {
console.log("LightBulb: Turning on...");
}
turnOff() {
console.log("LightBulb: Turning off...");
}
}
class Switch {
constructor(bulb) {
this.bulb = bulb;
}
operate() {
this.bulb.turnOn();
// Some other operations
this.bulb.turnOff();
}
}
// Object instantiation
const bulb = new LightBulb();
const switch = new Switch(bulb);
switch.operate();
In this, Switch
directly depends on LightBulb
and that violates the Dependency Inversion Principle.
After applying DIP:
// Interface-like abstraction
class Switchable {
turnOn() {
throw new Error("Method not implemented");
}
turnOff() {
throw new Error("Method not implemented");
}
}
class LightBulb extends Switchable {
turnOn() {
console.log("LightBulb: Turning on...");
}
turnOff() {
console.log("LightBulb: Turning off...");
}
}
class Switch {
constructor(device) {
this.device = device;
}
operate() {
this.device.turnOn();
// Some other operations
this.device.turnOff();
}
}
// Object instantiation
const bulb = new LightBulb();
const switch = new Switch(bulb);
switch.operate();
In this, an abstraction (Switchable
) is used, and both LightBulb
and Switch
depend on this abstraction, following the Dependency Inversion Principle.
Note that JavaScript doesn't have explicit interfaces, so we use a class as an abstraction here.
Conclusion 🌟
In a nutshell, This post introduces you to the SOLID principles—a set of rules for writing cleaner and more maintainable code. While we've touched on the basics here, there's more to explore for a complete grasp.
That's it 😁
Thanks for diving into this blog 🙏. If you found it helpful, share your thoughts in the comments 📩.
And remember to give a "💖 🦄 🤯 🙌 🔥" if you enjoyed it!
Top comments (12)
Very very good explanation! I have a question, so we need to make very specific class for what we want, but is there a limit that says "Ok enough, I'm unpacking too much and I'm messing myself up, I'm not helping myself"? Thank you
Great question! It's important to create specific classes, but too much detail can make things complicated. Check if the extra details really make your code clearer or just add confusion. Aim for a balance that makes your code easy to manage.
Thanks for your comment 💖.
very good
Thanks @christianpaez 💖
Great post ! Every example is super useful !
Thanks Man! 💖
You explained SoC instead of SRP, common mistake 😅
edit: and the ISP example does not point out the concept of the principle.
edit2: All the examples aren't related to their principles 🫠
Thanks for the feedback man! I'll fix the confusion in explaining SRP and make the ISP example clearer. I'll also make sure each example fits its principle better. Your input helps a lot! 🙏
Wow so fresh and great article!
Appreciate it! ✨
Great post
Thanks! George 💖